`. A breakpoint stores only its differences from the one * above it, so the media queries are emitted widest first and the cascade settles the rest. * * Specified by `packages/core/src/compile.js`, which the editor uses for its live preview. * The two are held together by `tests/css-parity-wp-test.php`: it compiles every scene in * the kit corpus with both and fails on any difference. * * Node identifiers reach this file from a request body, so `selector()` is the only place * that turns one into a selector and it rejects anything outside its own character set. * * @package Aumframe */ namespace Aumframe\Css; defined('ABSPATH') || exit; final class Compiler { /** The key under which a style's own declarations are carried through the pipeline. */ private const RAW = '@raw'; /** Styles whose value replaces rather than merges, because half a size is not a size. */ private const ATOMIC_KEYS = ['width' => true, 'height' => true]; private const JUSTIFY = [ 'start' => 'flex-start', 'center' => 'center', 'end' => 'flex-end', 'between' => 'space-between', 'around' => 'space-around', ]; private const ALIGN = [ 'start' => 'flex-start', 'center' => 'center', 'end' => 'flex-end', 'stretch' => 'stretch', 'baseline' => 'baseline', ]; private const DIRECTION = [ 'vertical' => 'column', 'vertical-reverse' => 'column-reverse', 'horizontal-reverse' => 'row-reverse', ]; private const TRANSITION_MS = ['fast' => 120, 'base' => 200, 'slow' => 320]; public const STATE_SELECTORS = [ 'hover' => ':hover', 'pressed' => ':active', 'focus' => ':focus-visible', 'open' => '[data-af-open]', 'scrolled' => '[data-af-scrolled]', 'scrolledPast' => '[data-af-scrolled-past]', 'scrollDown' => '[data-af-scroll-down]', ]; private const TRANSFORM_PRE = [ 'x' => 'translateX', 'y' => 'translateY', 'scale' => 'scale', 'rotate' => 'rotate', ]; private const LINK_DECLS = ['color', 'text-decoration-line', 'text-underline-offset']; /** How many nodes the last compile skipped because their identifier was refused. */ private static int $skipped = 0; /** * A node identifier is only ever used to build `.af-`; everything else is refused. * * The editor allocates identifiers of the form `n<4>_`, so a refusal means the scene * was written by something else. A refused node loses its rules, and nothing on the page * says so — which is why the count is kept and `skipped()` reports it to the importer. */ public static function selector(string $id): ?string { if (preg_match('/^[A-Za-z][A-Za-z0-9_-]{0,63}$/', $id)) { return '.af-' . $id; } self::$skipped++; return null; } /** Nodes skipped since the counter was last read, which also resets it. */ public static function skipped(): int { $n = self::$skipped; self::$skipped = 0; return $n; } /** * Compile a style table into declarations. * * @param array $ctx `parentStyle`, which `fill` and `fit` need to know their axis. * @return array */ public static function compile_style($style, array $ctx = []): array { /* * A populated JSON array is not a style table and is discarded, as the editor's * compiler discards it. An empty one is not: `json_decode(…, true)` renders `{}` and * `[]` alike, and a node with no styling of its own is written `{}` — which still * compiles to `position:relative` by way of `compile_position`. */ if (!Value::isObjectish($style)) { return []; } $d = []; $layout = Value::get($style, 'layout'); $justify = Value::get($style, 'justify'); $align = Value::get($style, 'align'); /* Container: how this node lays out its children. */ if ($layout === 'stack') { $d['display'] = 'flex'; $d['flex-direction'] = self::DIRECTION[(string) Value::get($style, 'direction')] ?? 'row'; if (Value::get($style, 'wrap')) { $d['flex-wrap'] = 'wrap'; } if ($justify) { $d['justify-content'] = self::JUSTIFY[(string) $justify] ?? $justify; } if ($align) { $d['align-items'] = self::ALIGN[(string) $align] ?? $align; } } elseif ($layout === 'grid') { $d['display'] = 'grid'; $columns = Value::get($style, 'columns'); if (is_array($columns) && Value::isList($columns)) { $parts = []; foreach ($columns as $c) { $parts[] = 'minmax(0, ' . ((is_int($c) || is_float($c)) ? Value::str($c) . 'fr' : Value::str($c)) . ')'; } $d['grid-template-columns'] = implode(' ', $parts); } elseif (is_array($columns)) { /* * A table here is a mistake in the data. The editor's compiler produces * `[object Object]` for it and drops the declaration; this reaches the same * result by leaving the declaration out. */ $d['grid-template-columns'] = ''; } else { $d['grid-template-columns'] = 'repeat(' . Value::str($columns ?: 1) . ', minmax(0, 1fr))'; } if ($align) { $d['align-items'] = self::ALIGN[(string) $align] ?? $align; } } elseif ($layout === 'none') { $d['display'] = 'block'; $d['position'] = $d['position'] ?? 'relative'; } elseif ($layout === 'contents') { /* No box of its own: the children take part in the parent's layout directly. */ $d['display'] = 'contents'; } if (Value::get($style, 'gap') !== null) { $d['gap'] = Value::px(Tokens::resolve($style['gap'])); } /* Written after layout so that it always wins over the display set above. */ if (Value::get($style, 'hidden')) { $d['display'] = 'none'; } /* * Out of flow, size resolves along a different path: `fill` means all of the * positioned parent rather than a share of the free space. `fixed` and `absolute` * behave identically here. */ $position = Value::get($style, 'position'); $mode = is_array($position) ? Value::get($position, 'mode') : null; $is_absolute = $mode === 'absolute' || $mode === 'fixed'; self::merge($d, Sizing::compile_position($position)); self::merge($d, Sizing::compile_size('width', Value::get($style, 'width'), $ctx + ['isAbsolute' => $is_absolute])); self::merge($d, Sizing::compile_size('height', Value::get($style, 'height'), $ctx + ['isAbsolute' => $is_absolute])); /* Appearance. */ if (Value::get($style, 'padding')) { $d['padding'] = self::box_of($style['padding']); } if (Value::get($style, 'margin')) { $d['margin'] = self::box_of($style['margin']); } $fill = Value::get($style, 'fill'); if ($fill) { $type = Value::get($fill, 'type'); $value = Value::get($fill, 'value'); if ($type === 'color') { $d['background-color'] = Tokens::resolve($value); } elseif ($type === 'gradient') { $d['background-image'] = $value; } elseif ($type === 'image') { $d['background-image'] = 'url(' . Value::str($value) . ')'; $d['background-size'] = Value::get($fill, 'size') ?: 'cover'; $d['background-position'] = Value::get($fill, 'position') ?: 'center'; } } $var = Value::get($style, 'var'); if ($var !== null && self::safe_css_number($var) !== '') { $d['--af-var'] = trim(Value::str($var)); } $radius = Value::get($style, 'radius'); if ($radius !== null) { if (is_array($radius)) { $corners = []; foreach (['topLeft', 'topRight', 'bottomRight', 'bottomLeft'] as $corner) { $v = Value::get($radius, $corner); $corners[] = Value::px(Tokens::resolve($v ?? 0)); } $d['border-radius'] = implode(' ', $corners); } else { $d['border-radius'] = Value::px(Tokens::resolve($radius)); } } $border = Value::get($style, 'border'); foreach (['top', 'right', 'bottom', 'left'] as $side) { $b = is_array($border) ? Value::get($border, $side) : null; if (is_array($b)) { $c = Tokens::resolve(Value::get($b, 'color')); $c = ($c === null) ? 'currentColor' : $c; $width = Value::get($b, 'width'); $d['border-' . $side] = Value::px($width ?? 1) . ' ' . (Value::get($b, 'style') ?: 'solid') . ' ' . Value::str($c); } } if (is_array($border) && (Value::get($border, 'width') !== null || Value::get($border, 'color') || Value::get($border, 'style')) ) { $bc = Tokens::resolve(Value::get($border, 'color')); $bc = ($bc === null) ? 'currentColor' : $bc; $d['border'] = Value::px(Value::get($border, 'width') ?? 1) . ' ' . (Value::get($border, 'style') ?: 'solid') . ' ' . Value::str($bc); } $shadow = Value::get($style, 'shadow'); if (is_array($shadow) && $shadow !== []) { $parts = []; foreach ($shadow as $s) { $parts[] = Value::px(Value::get($s, 'x') ?? 0) . ' ' . Value::px(Value::get($s, 'y') ?? 0) . ' ' . Value::px(Value::get($s, 'blur') ?? 0) . ' ' . Value::px(Value::get($s, 'spread') ?? 0) . ' ' . Value::str(Value::get($s, 'color')); } $d['box-shadow'] = implode(', ', $parts); } if (Value::get($style, 'opacity') !== null) { $d['opacity'] = Value::str($style['opacity']); } if (Value::get($style, 'overflow')) { $d['overflow'] = $style['overflow']; } if (Value::get($style, 'objectFit')) { $d['object-fit'] = $style['objectFit']; } if (Value::get($style, 'objectPosition')) { $d['object-position'] = $style['objectPosition']; } if (Value::get($style, 'zIndex') !== null) { $d['z-index'] = Value::str($style['zIndex']); } if (Value::get($style, 'filter')) { $d['filter'] = $style['filter']; } if (Value::get($style, 'perspective') !== null) { $d['perspective'] = Value::px($style['perspective']); } if (Value::get($style, 'transformStyle')) { $d['transform-style'] = $style['transformStyle']; } if (Value::get($style, 'backdropFilter')) { /* Safari still needs the prefix. */ $d['-webkit-backdrop-filter'] = $style['backdropFilter']; $d['backdrop-filter'] = $style['backdropFilter']; } $collapse = Value::get($style, 'collapse'); if ($collapse === 'closed' || $collapse === 'open') { $d['display'] = 'grid'; $d['grid-template-rows'] = $collapse === 'open' ? '1fr' : '0fr'; $d['overflow'] = 'hidden'; } if (Value::get($style, 'cursor')) { $d['cursor'] = $style['cursor']; } $transition = Value::get($style, 'transition'); if ($transition) { $speed = is_array($transition) ? (Value::get($transition, 'speed') ?: 'base') : 'base'; $ms = self::TRANSITION_MS[(string) $speed] ?? null; if ($ms) { $d['transition-property'] = 'color, background-color, border-color, box-shadow, opacity, transform, text-decoration-color'; $d['transition-duration'] = $ms . 'ms'; $d['transition-timing-function'] = 'cubic-bezier(.2, 0, .2, 1)'; } } $t = Value::get($style, 'text'); if (is_array($t)) { if (Value::get($t, 'family')) { $family = Tokens::resolve($t['family']); $d['font-family'] = $family; $d['--af-font-heading'] = $family; } if (Value::get($t, 'size') !== null) { $d['font-size'] = Value::px(Tokens::resolve($t['size'])); } if (Value::get($t, 'weight') !== null) { $d['font-weight'] = Value::str($t['weight']); } if (Value::get($t, 'lineHeight') !== null) { $d['line-height'] = Value::str($t['lineHeight']); } if (Value::get($t, 'letterSpacing') !== null) { $d['letter-spacing'] = Value::px($t['letterSpacing']); } if (Value::get($t, 'color')) { $d['color'] = Tokens::resolve($t['color']); } if (Value::get($t, 'tabularNums')) { $d['font-variant-numeric'] = 'tabular-nums'; } if (Value::get($t, 'decoration')) { $d['text-decoration-line'] = $t['decoration']; } if (Value::get($t, 'underlineOffset') !== null) { $d['text-underline-offset'] = Value::px($t['underlineOffset']); } if (Value::get($t, 'align')) { $d['text-align'] = $t['align']; } if (Value::get($t, 'transform')) { $d['text-transform'] = $t['transform']; } if (Value::get($t, 'gradient')) { $d['background-image'] = $t['gradient']; $d['-webkit-background-clip'] = 'text'; $d['background-clip'] = 'text'; $d['color'] = 'transparent'; } } $center_prefix = $d['--af-center'] ?? null; unset($d['--af-center']); $tf = self::compile_transform(Value::get($style, 'transform'), $center_prefix); if ($tf !== null) { $d['transform'] = $tf; } $custom = Value::get($style, 'customCss'); if (is_string($custom) && trim($custom) !== '') { $d[self::RAW] = Rules::node($custom); } return $d; } public static function compile_transform($transform, ?string $center_prefix): ?string { $parts = []; if ($center_prefix) { $parts[] = $center_prefix; } if (is_array($transform)) { $x = Value::get($transform, 'x') ?? 0; $y = Value::get($transform, 'y') ?? 0; $scale = Value::get($transform, 'scale') ?? 1; $rotate = Value::get($transform, 'rotate') ?? 0; $rotate_x = Value::get($transform, 'rotateX') ?? 0; $rotate_y = Value::get($transform, 'rotateY') ?? 0; if ($x || $y) { $parts[] = 'translate(' . Value::px($x) . ', ' . Value::px($y) . ')'; } if ($rotate_x) { $parts[] = 'rotateX(' . Value::str($rotate_x) . 'deg)'; } if ($rotate_y) { $parts[] = 'rotateY(' . Value::str($rotate_y) . 'deg)'; } if ($rotate) { $parts[] = 'rotate(' . Value::str($rotate) . 'deg)'; } if ($scale != 1) { $parts[] = 'scale(' . Value::str($scale) . ')'; } } return $parts ? implode(' ', $parts) : null; } /** A single length, or the four sides of a box. */ private static function box_of($b): string { if (!is_array($b)) { return Value::px(Tokens::resolve($b)); } $sides = []; foreach (['top', 'right', 'bottom', 'left'] as $side) { $v = Value::get($b, $side); $sides[] = Value::px(Tokens::resolve($v ?? 0)); } return implode(' ', $sides); } /** A bare number with an optional unit, and nothing else. */ public static function safe_css_number($v): string { if (!is_string($v) && !is_int($v) && !is_float($v)) { return ''; } $s = trim(Value::str($v)); if ($s === '' || strlen($s) > 24) { return ''; } return preg_match('/^-?(\d+\.?\d*|\.\d+)(px|%|em|rem|vh|vw|deg|s|ms|fr)?$/', $s) ? $s : ''; } /** @return array */ public static function entrance_pre_decls($from): array { $d = []; $tf = []; if (is_array($from)) { foreach ($from as $k => $v) { if ($k === 'opacity') { $d['opacity'] = $v; } elseif ($k === 'filter') { $d['filter'] = $v; } elseif (isset(self::TRANSFORM_PRE[$k])) { $numeric = is_int($v) || is_float($v); $unit = ($numeric && ($k === 'x' || $k === 'y')) ? 'px' : (($numeric && $k === 'rotate') ? 'deg' : ''); $tf[] = self::TRANSFORM_PRE[$k] . '(' . Value::str($v) . $unit . ')'; } } } if ($tf) { $d['transform'] = implode(' ', $tf); } return $d; } /** Merge b into a, keeping a's ordering for keys they share. */ private static function merge(array &$a, array $b): void { foreach ($b as $k => $v) { $a[$k] = $v; } } /** A breakpoint patch merges into the style below it; a size replaces it outright. */ public static function deep_merge($a, $b) { if (!$b) { return $a; } if (!$a) { return $b; } if (!is_array($a) || !is_array($b)) { return $b; } $out = $a; foreach ($b as $k => $v) { $out[$k] = (Value::isObjectish($v) && !isset(self::ATOMIC_KEYS[$k])) ? self::deep_merge(Value::get($a, (string) $k), $v) : $v; } return $out; } /** What the breakpoint changes, plus `initial` for whatever it drops. */ private static function diff_decls(array $base, array $next): array { $out = []; foreach ($next as $k => $v) { if (!array_key_exists($k, $base) || $base[$k] !== $v) { $out[$k] = $v; } } foreach ($base as $k => $_) { if (!array_key_exists($k, $next) && $k !== self::RAW) { $out[$k] = 'initial'; } } return $out; } private static function decls_to_text(array $d): string { $out = []; foreach ($d as $k => $v) { if ($k === self::RAW) { $out[] = $v; continue; } if (is_int($v) || is_float($v) || (is_string($v) && $v !== '' && !str_contains($v, '[object'))) { $out[] = $k . ':' . Value::str($v) . ';'; } } return implode('', $out); } private static function parent_patch($nodes, string $child_id, string $bp_id) { if (!is_array($nodes)) { return null; } foreach ($nodes as $node) { $children = Value::get($node, 'children'); if (is_array($children) && in_array($child_id, $children, true)) { $responsive = Value::get($node, 'responsive'); return is_array($responsive) ? (Value::get($responsive, $bp_id) ?? null) : null; } } return null; } /** Compile a whole scene. */ public static function scene($scene): string { if (!is_array($scene)) { return ''; } $scope = Value::get($scene, 'tokenScope') ?: ':root'; $chunks = [Tokens::compile(Value::get($scene, 'tokens'), $scope)]; $chunks[] = self::BASE_CSS; $bps = Value::get($scene, 'breakpoints'); if (!is_array($bps) || $bps === []) { $bps = [['id' => 'desktop', 'max' => null]]; } $bps = array_values($bps); $base_id = (string) Value::get($bps[0], 'id'); $by_bp = []; foreach ($bps as $b) { $by_bp[(string) Value::get($b, 'id')] = []; } $state = [ 'scene' => $scene, 'bps' => $bps, 'base' => $base_id, 'out' => &$by_bp, ]; self::visit($state, Value::get($scene, 'root'), null, Value::get($scene, 'nodes')); $components = Value::get($scene, 'components'); if (is_array($components)) { foreach ($components as $cmp) { self::visit($state, Value::get($cmp, 'root'), null, Value::get($cmp, 'nodes')); } } $chunks[] = implode('', $by_bp[$base_id]); foreach (array_slice($bps, 1) as $bp) { $id = (string) Value::get($bp, 'id'); $rules = implode('', $by_bp[$id] ?? []); if ($rules !== '') { $chunks[] = '@media (max-width:' . Value::str(Value::get($bp, 'max')) . 'px){' . $rules . '}'; } } $custom = Rules::sheet(Value::str(Value::get($scene, 'customCss') ?? '')); if ($custom !== '') { $chunks[] = $custom; } return implode("\n", array_values(array_filter($chunks, static fn($c) => (string) $c !== ''))); } private static function visit(array &$state, $id, $parent_style, $nodes): void { if (!is_string($id) || !is_array($nodes)) { return; } $node = Value::get($nodes, $id); if (!is_array($node)) { return; } $sel = self::selector($id); if ($sel === null) { return; } $scene = $state['scene']; $bps = $state['bps']; $base_id = $state['base']; $style = Value::get($node, 'style'); $ctx = ['parentStyle' => $parent_style]; $base = self::compile_style($style, $ctx); if (Value::get($node, 'type') === 'Instance') { $props = Value::get($node, 'props'); $cmp = Value::get(Value::get($scene, 'components'), (string) Value::get($props, 'componentId')); if (Schema::has_params($cmp)) { $values = Schema::resolve_values($cmp, Value::get($props, 'values')); self::merge($base, Schema::css_vars_of($cmp, $values)); } } if (Value::get($node, 'type') === 'Text') { $content = Value::get(Value::get($node, 'props'), 'content'); $bindings = Value::get($node, 'bindings'); $bound = is_array($bindings) && Value::get($bindings, 'props.content'); if (is_string($content) && str_contains($content, "\n") && !$bound) { $base['white-space'] = 'pre-line'; } } if ($base !== []) { $state['out'][$base_id][] = $sel . '{' . self::decls_to_text($base) . '}'; } foreach (array_slice($bps, 1) as $bp) { $bp_id = (string) Value::get($bp, 'id'); $responsive = Value::get($node, 'responsive'); $patch = is_array($responsive) ? Value::get($responsive, $bp_id) : null; if (!$patch) { continue; } $merged_style = self::deep_merge($style, $patch); $merged_parent = $parent_style ? self::deep_merge($parent_style, self::parent_patch($nodes, $id, $bp_id)) : $parent_style; $decls = self::compile_style($merged_style, ['parentStyle' => $merged_parent]); $diff = self::diff_decls(self::compile_style($style, $ctx), $decls); if ($diff !== []) { $state['out'][$bp_id][] = $sel . '{' . self::decls_to_text($diff) . '}'; } } $motion = Value::get($node, 'motion'); $entrance = is_array($motion) ? Value::get($motion, 'entrance') : null; $split = is_array($motion) ? Value::get($motion, 'splitText') : null; $ent_from = is_array($entrance) ? Value::get($entrance, 'from') : null; $spl_from = is_array($split) ? Value::get($split, 'from') : null; if ($ent_from || $spl_from) { $pre = self::entrance_pre_decls($ent_from ?: ['opacity' => 0]); if ($pre !== []) { $target = (is_array($entrance) && Value::get($entrance, 'stagger')) ? $sel . ' > *' : $sel; $rule = 'html.af-anim ' . $target . '{' . self::decls_to_text($pre) . '}'; $mw = (float) (Value::get($motion, 'minWidth') ?? 0); $state['out'][$base_id][] = $mw > 0 ? '@media (min-width:' . Value::str((int) $mw) . 'px){' . $rule . '}' : $rule; } } $variants = Value::get($node, 'variants'); if (is_array($variants)) { foreach ($variants as $name => $variant) { $trigger = Value::get($variant, 'trigger'); $on = is_array($trigger) ? Value::get($trigger, 'on') : null; $state_sel = self::STATE_SELECTORS[(string) ($on ?: $name)] ?? null; if (!$state_sel) { continue; } $v_decls = self::compile_style(self::deep_merge($style, Value::get($variant, 'style')), $ctx); $diff = self::diff_decls($base, $v_decls); if ($diff !== []) { $state['out'][$base_id][] = $sel . $state_sel . '{' . self::decls_to_text($diff) . '}'; } } } $collapse = is_array($style) ? Value::get($style, 'collapse') : null; if ($collapse === 'closed' || $collapse === 'open') { $state['out'][$base_id][] = $sel . ' > *{min-height:0}'; } $text = is_array($style) ? Value::get($style, 'text') : null; $link = is_array($text) ? Value::get($text, 'link') : null; if ($link) { $only = static function (array $o): array { return array_intersect_key($o, array_flip(self::LINK_DECLS)); }; $rest = $only(self::compile_style(['text' => [ 'color' => Value::get($link, 'color'), 'decoration' => Value::get($link, 'decoration'), 'underlineOffset' => Value::get($link, 'underlineOffset'), ]], $ctx)); if ($rest !== []) { $state['out'][$base_id][] = $sel . ' a{' . self::decls_to_text($rest) . '}'; } $hov = $only(self::compile_style(['text' => [ 'color' => Value::get($link, 'hoverColor'), 'decoration' => Value::get($link, 'hoverDecoration'), ]], $ctx)); if ($hov !== []) { $state['out'][$base_id][] = $sel . ' a:hover{' . self::decls_to_text($hov) . '}'; } } $child_parent = (is_array($style) && Value::get($style, 'layout') === 'contents') ? $parent_style : $style; $children = Value::get($node, 'children'); if (is_array($children)) { foreach ($children as $child) { self::visit($state, $child, $child_parent, $nodes); } } } /** Resets and element defaults that every scene needs. */ private const BASE_CSS = ' :where(.af-root,.af-root *){box-sizing:border-box;} :where(.af-root){margin:0;} :where(.af-root p,.af-root h1,.af-root h2,.af-root h3,.af-root h4){margin:0;} :where(.af-root a){color:inherit;text-decoration:none;} :where(html){scroll-behavior:smooth;} :where(.af-root [id]){scroll-margin-top:var(--af-anchor-offset,24px);} :where(.af-root img){display:block;max-width:100%;} :where(.af-root .af-icon){width:1em;height:1em;display:inline-block;vertical-align:-.125em;flex:none;} :where(.af-root .af-vector){display:block;width:100%;height:auto;} :where(.af-root .af-canvas){display:block;width:100%;height:100%;} :where(.af-root a.af-node){display:flex;flex-direction:column;} :where(.af-root .af-carousel-dot){width:10px;height:10px;padding:0;border:0;border-radius:999px;cursor:pointer;background:color-mix(in srgb,currentColor 35%,transparent);transition:background .2s;} :where(.af-root .af-carousel-dot[data-af-current]){background:var(--af-color-brand,currentColor);} :where(.af-root [data-af-role=prev][disabled],.af-root [data-af-role=next][disabled]){opacity:.35;cursor:default;} .af-root [data-af-role=track]{overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;scrollbar-width:none;} .af-root [data-af-role=track]>*{flex:0 0 100%;scroll-snap-align:start;} :where(.af-root .af-embed){display:block;width:100%;border:0;} :where(.af-root .af-embed-consent){display:block;position:relative;overflow:hidden;background:color-mix(in srgb,currentColor 6%,transparent);} :where(.af-root .af-embed-load){display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;width:100%;height:100%;min-height:120px;padding:24px;text-align:center;cursor:pointer;} :where(.af-root .af-embed-label){font-weight:600;} :where(.af-root .af-embed-note){font-size:.82em;opacity:.7;max-width:34em;} :where(.af-node){transition:transform .25s cubic-bezier(.2,.7,.3,1),box-shadow .25s,background-color .25s,opacity .25s;} '; }