@font-face{ font-family:'BST Symbol Beta'; font-weight:400; font-style:normal;
  src:url(fonts/BSTSymbolBeta-Book.woff2) format('woff2'); font-display:swap; }
@font-face{ font-family:'BST Symbol Beta'; font-weight:300; font-style:normal;
  src:url(fonts/BSTSymbolBeta-Light.woff2) format('woff2'); font-display:swap; }

  /* ---------- palette ----------
     A scheme is the ~14 tokens in this block and nothing else. Everything downstream — CSS,
     the canvases, and the SVG assets — resolves its colour from here, so a scheme is defined
     once and applied everywhere (see main.js "theme engine").

     --accent-1/2/3 are the triad, and are the source of truth. --orange/--green/--gold alias
     them so the existing rules and .tag--* classes keep reading naturally under the default
     scheme; a cool scheme still fills slot 1, it just isn't orange any more. Schemes below set
     ONLY the raw tokens — never the aliases.

     Rule for anything added later: no literal colour outside this block. A hardcoded rgba() is
     invisible to the theme engine and will strand itself on the default scheme. Use
     color-mix(in srgb, var(--token) N%, transparent) for a tint of a token. */
  :root, [data-theme="default"]{
    --paper:#FBF7EC; --ink:#1c1d20; --ink-mid:#404040; --ink-soft:#979797; --dim:#8b9099;
    --accent-1:#FA792E; --accent-2:#419C57; --accent-3:#DCC061;
    --line:#e7e1d3; --panel:#F0E9D7;
    --grid-line:rgba(150,142,118,.55);   /* the self-drawing dashed grid (hero/footer canvas) */
    --dash:#403f3c;                      /* dashed ground plane under the isometric blocks */
    --shadow:rgba(28,26,18,.24);         /* modal card */
    --shadow-tile:rgba(0,0,0,.25);       /* board mosaic tiles */
    /* aliases — set --accent-N, never these */
    --orange:var(--accent-1); --green:var(--accent-2); --gold:var(--accent-3);
    /* type scale — tuned in type-playground.html */
    --fs-label:0.75rem;      --lh-label:1.2;        /* 12px */
    --fs-body:1rem;          --lh-body:1.2;         /* 16px */
    --fs-body-large:1.625rem;--lh-body-large:1.2;   /* 26px */
    /* heading + display scale fluidly; the tuned value is the desktop ceiling */
    --fs-heading:clamp(1.75rem, 1.1rem + 2.1vw, 2.625rem);  --lh-heading:1.1;  /* 28 → 42px */
    --fs-display:clamp(2.125rem, 1.3rem + 3vw, 3.25rem);    --lh-display:1.1;  /* 34 → 52px */
    /* measure: anything set at the heading font size caps at 32ch */
    --mw-heading:32ch;
    /* type roles — values the 5-step scale can't express as one number per step.
       `body` is set at two leadings: --lh-body for titles, --lh-prose for running copy. */
    --lh-prose:1.25; --lh-tight:1.05;
    --ls-label:0.14em; --ls-heading:-0.005em;
    /* spacing scale */
    --sp-1:.375rem; --sp-2:.75rem; --sp-3:1.5rem; --sp-4:3rem; --sp-5:4rem;
    /* fixed site header height */
    --header-h:4.75rem;
    /* ---------- motion ----------
       One system for hover / press / focus feedback, so no rule restates a raw duration or curve.
       Press is fast (it's feedback — the interface confirming it heard you); hover is a touch
       slower (it's an invitation). Both use a strong ease-out so the movement leads rather than
       eases in. --focus-ring is the single keyboard-focus signal, green to match the interaction
       accent the module rows and header CTA already use.

       --focus-colour exists so a scheme can order its accents by DESIGN intent without disabling
       keyboard focus as a side effect. It defaults to slot 2, which is what the ring has always
       been, so nothing changes unless a scheme says so. A scheme whose slot 2 is too pale to be
       seen overrides this one token — either to another of its own accents or to a darkened step
       of one. Focus visibility is not a palette decision; it is a floor every palette has to clear. */
    --ease-out:cubic-bezier(.23,1,.32,1);
    --dur-press:130ms; --dur-hover:220ms;
    --focus-colour:var(--accent-2);
    --focus-ring:2px solid var(--focus-colour);
  }

  /* ---------- schemes under test ----------
     TEMPORARY: these exist to be compared, not to ship. Once one is chosen, fold its values
     into :root above and delete this block along with the toggle (see main.js).

     Deliberately not :root-scoped — any element can opt into a scheme, which is what lets the
     toggle's swatches paint themselves in the scheme they switch to rather than restating its
     values. On :root these tie with the block above on specificity and win by coming later.

     ON A SCHEME'S HUE SPREAD, since it is easy to get backwards: the SVG recolourer decides which
     accent a pre-computed literal descends from by comparing it against SRC — the DEFAULT palette
     baked into the assets, orange 22deg / green 134deg / gold 46deg — and never against the
     scheme's own values (main.js:113). A scheme's hue spread therefore has no bearing on whether
     ancestry is attributed correctly. It only affects how VISIBLE a mis-attribution is: accents
     close together hide it, accents far apart expose it. Source orange and gold are 24deg apart,
     so literals descending from those two are already ambiguous — under every scheme equally, and
     nothing a new scheme does makes that better or worse. A scheme is free to put its accents
     wherever it likes. */

  /* Paper & Signal with the orange slot swapped for blue, and nothing else touched. It sets one
     token: everything a scheme doesn't override cascades from the block above, so this IS the
     default palette by construction rather than by a copy that can drift out of step with it.
     #2E8DFA is #FA792E's own saturation and lightness at a blue hue — a like-for-like swap, so
     hue is the only variable under test. (It carries more weight than the orange did: blue is
     the darker colour at equal HSL lightness, so it sits harder on the cream.) */
  [data-theme="blue-signal"]{
    --accent-1:#2E8DFA;
  }

  /* cool and technical — reads as instrumentation rather than craft. The paper is close to white
     but not white: enough blue left in it to stay a colour rather than an absence of one.
     --panel and --line are the paper stepped down by a fixed amount per channel, so they move
     with it — held apart, a whiter paper would just stretch the gap and turn the placeholder
     panels into grey boxes. */
  /* The selector carries blueprint-warm too, so that scheme inherits this substrate by SHARING it
     rather than by a copy — the same discipline blue-signal uses against the default. Edit these
     values and both schemes move together; there is no second set to keep in step. */
  [data-theme="blueprint"], [data-theme="blueprint-warm"]{
    --paper:#F7FAFD; --ink:#111A24; --ink-mid:#3A4654; --ink-soft:#8894A4; --dim:#7C8899;
    --accent-1:#2563EB; --accent-2:#0E9384; --accent-3:#7C5CFF;
    --line:#E5EBF2; --panel:#EBF1F7;
    --grid-line:rgba(120,138,160,.5);
    --dash:#3A4654;
    --shadow:rgba(17,26,36,.20); --shadow-tile:rgba(17,26,36,.28);
  }
  /* Blueprint's cool instrumentation base with one warm accent cutting across it. Ties with the
     block above on specificity and wins by coming later, so it needs to STAY after it.

     Blueprint's three accents span only 78deg (blue 221, teal 173, purple 252) — cool throughout,
     which is the scheme's whole character but leaves slot 3 barely distinguishable from slot 1.
     The orange breaks that: it puts 152deg between them, so slot 3 reads as a separate signal
     rather than a second blue. It costs contrast to do it, see below. */
  [data-theme="blueprint-warm"]{
    --accent-3:#FA5A2D;    /* 3.05:1 on the cool paper — large text and UI, not body copy. The purple
                              it replaces was 4.15:1, so slot 3 does give up contrast for the warmth. */
  }
  /* the same warm triad, inverted onto near-black. --ink is the TEXT and --paper the ground,
     so the whole sheet inverts on its own — .btn--primary lands light-on-dark without a rule. */
  [data-theme="dark"]{
    color-scheme:dark;
    --paper:#15171C; --ink:#F3EFE4; --ink-mid:#C3BDB0; --ink-soft:#8A8578; --dim:#726D62;
    --accent-1:#FF8A3D; --accent-2:#4FC46B; --accent-3:#E8C86A;
    --line:#2A2E37; --panel:#1E2128;
    --grid-line:rgba(150,142,118,.38);
    --dash:#6E6A60;
    --shadow:rgba(0,0,0,.5); --shadow-tile:rgba(0,0,0,.55);
  }
  /* the paper's warmth pulled out and the triad pushed up — highest contrast of the five */
  [data-theme="voltage"]{
    --paper:#F7F7F5; --ink:#0B0B0C; --ink-mid:#3D3D40; --ink-soft:#8E8E92; --dim:#7A7A80;
    --accent-1:#FF4A17; --accent-2:#00B37E; --accent-3:#FFC700;
    --line:#E4E4E1; --panel:#EDEDEA;
    --grid-line:rgba(120,120,124,.45);
    --dash:#3D3D40;
    --shadow:rgba(11,11,12,.22); --shadow-tile:rgba(11,11,12,.3);
  }
  /* Safety orange and a sharper yellow on concrete greys. From a five-band reference, mapped so
     that every band lands somewhere and nothing is invented: #FE4409 and #F9CB05 take slots 1 and
     3, and the three remaining bands become the structure (see each line below).

     The reference had no third accent — its two yellows sit 10.7deg apart, which reads as one
     colour at two saturations rather than two accents, so using both would have spent two slots on
     one idea. Slot 2 is left UNSET and inherits the default green instead. (An earlier version of
     this note justified that by the SVG recolourer needing a wide hue spread; it does not — see
     the header above. The reason is compositional, not technical.)

     Paper, ink and the ink ramp are unset for the same reason blue-signal sets one token: the
     substrate is unchanged, so inheriting it means it cannot drift from the default. */
  [data-theme="hi-vis"]{
    --accent-1:#FE4409;                  /* 3.25:1 on cream — MORE contrast than the default orange's 2.50 */
    --accent-3:#F9CB05;                  /* 1.45:1 — fill only, never text, as the gold already is */
    --dim:#948E75;                       /* the reference grey #BAB6A6 at its own hue and saturation, walked
                                            down to 3.07:1 — it sets 12px labels (nav, form, footer meta),
                                            and at its given lightness those land at 1.90:1 and stop reading */
    --line:#D9DAD3;                      /* cooler than the cream it rules on, unlike the default's warm
                                            #e7e1d3. Deliberate: concrete structure under warm signal */
    --panel:#F4F1C1;                     /* the pale yellow band, 44% into the paper. --panel backs every
                                            cardbox, form field and vizbox, so at full strength the whole
                                            substrate goes yellow; this keeps the tint and not the slab.
                                            Raise toward #EBEA8D for more of it. */
    --grid-line:rgba(186,182,166,.55);   /* the reference grey at full value — decorative, carries no text */
  }
  /* Fluorescent pink and violet on the same concrete greys — riso inks rather than signage. From a
     five-band reference sharing its first three bands with hi-vis above, so the structure tokens are
     identical and only the triad differs; the two schemes are a controlled comparison.

     Unlike hi-vis this one is a genuine triad — three colours doing three jobs, at 70/97/168deg.

     The roles map across one-for-one — pink takes orange's job as the hot signal, violet takes
     green's as the interaction and focus colour, pale yellow takes gold's as fill. Violet at 6.05:1
     is the first accent in the system that passes AA for body text; the green it replaces is
     3.21:1, so the focus ring gets materially more visible under this scheme, not less.

     The paper stays warm and is left unset. Cooling it changes contrast by 0.01 (checked), so it is
     purely a look — and cream under cool inks is the riso pairing rather than a clash. */
  [data-theme="riso"]{
    --accent-1:#F905A0;                  /* 3.53:1 on cream — large text and UI, not body copy */
    --accent-2:#5D3CE5;                  /* 6.05:1 — passes AA for body text, unlike any accent so far */
    --accent-3:#EBEA8D;                  /* 1.18:1 — fill only, the palest slot-3 yet. Never text. */
    --dim:#948E75;                       /* as hi-vis: the reference grey walked down to 3.07:1 so the
                                            12px labels hold. Warm, but it is the palette's own grey. */
    --line:#D9DAD3;
    --grid-line:rgba(186,182,166,.55);
    /* --panel is left to inherit. Under hi-vis the pale yellow had no accent slot so it became the
       panel wash; here it IS slot 3, and using it twice would put the gold on top of its own ground. */
  }
  /* Voltage's substrate warmed part of the way toward Paper & Signal, under a cyan signal.

     The warming is done in HSL with each token's LIGHTNESS HELD, and only hue and saturation moved:
     hue to 42deg, saturation 45% of the distance from voltage's value to paper & signal's. Warming
     by mixing the two bases instead would have dragged the ink lighter and given back exactly what
     voltage is for — this way ink-on-paper is 18.29:1 against voltage's own 18.34:1, where paper &
     signal sits at 15.74:1. Warmth gained, contrast kept.

     The values can't share voltage's selector the way blueprint-warm shares blueprint's, because
     every one of them differs — the substrate is the thing being changed, not the accent.

     ONE ACCENT, NOT THREE. The triad is three SLOTS, not three colours: what each slot owes the
     page is a contrast level, not a hue. So the scheme states a single colour and fills the slots
     with one hue at three tonal steps, each set where its job needs it:

       accent-3  #9BCBDE  1.64:1  fill only, decorative — mirrors the gold's 1.67:1
       accent-1  #30ABDB  2.46:1  the signal: large text, fills, UI — the orange's 2.50:1
       accent-2  #1A7598  4.86:1  focus ring and interaction — passes AA for body text

     All three are hue 196.8. The aliases still hold, so .tag--green renders the dark cyan and
     .tag--gold the pale one; they are tonal roles now, not colours. Restraint is the point — see
     the Poolside note in color-palettes.md, where the single accent IS the discipline. */
  /* the warmed substrate, shared by every one-accent scheme built on it. Accents follow in their
     own blocks, so a new colour on this base costs three lines and nothing is duplicated. */
  [data-theme="cyan-signal"], [data-theme="beacon"], [data-theme="drafting"], [data-theme="prism"],
  [data-theme="verdant"], [data-theme="poster"], [data-theme="orchard"], [data-theme="harbour"],
  [data-theme="jewel"]{
    --paper:#F9F7F3; --ink:#0C0C0B; --ink-mid:#3F3F3E; --ink-soft:#91908F; --dim:#827F78;
    --line:#E7E4DE; --panel:#F0EDE7;
    --grid-line:rgba(130,125,114,.45);
    --dash:#403F3D;
    --shadow:rgba(12,11,10,.22); --shadow-tile:rgba(12,11,10,.3);
  }
  [data-theme="cyan-signal"]{
    --accent-1:#30ABDB;                  /* 2.46:1 — the given colour, held exactly */
    --accent-2:#1A7598;                  /* 4.86:1 — same hue and saturation, darkened so the focus
                                            ring and interaction states carry body-text contrast */
    --accent-3:#9BCBDE;                  /* 1.64:1 — same hue, saturation eased, lightened; fill
                                            only, never text, exactly as the gold slot is */
  }
  /* One saturated yellow on the same light substrate. Yellow costs more here than the cyan did,
     and the cost lands entirely on slot 1:

       accent-3  #F6DA65  1.30:1  fill
       accent-1  #F9CB05  1.45:1  the signal — the given colour, held
       accent-2  #856C00  4.74:1  focus + interaction, passes AA

     1.45:1 is a FILL contrast being asked to do a signal's job. It is roughly where the gold sits
     (1.67:1) and no yellow at this hue does better while staying yellow — the ceiling on a near-
     white ground is about 1.5:1 before it stops reading as the colour given. What carries it is
     salience rather than luminance: at full saturation this yellow attracts the eye even though it
     barely separates from the paper. What it cannot do is carry text, and one place on the page
     leans on that — see the note under .deck-index below.

     Slot 2 is the same hue at lightness .26, which is a dark bronze. That is not a drift off the
     colour: it is what hue 48.7 looks like with the light taken out. It buys a focus ring at
     4.74:1, better than the 3.21:1 green that ships in the default today. */
  [data-theme="beacon"]{
    --accent-1:#F9CB05;
    --accent-2:#856C00;
    --accent-3:#F6DA65;
  }
  /* Blueprint-warm's triad lifted onto the warm substrate — the cool instrument palette off its own
     cool paper and onto neutral. Only the ground changes, so the two are a clean A/B on how much of
     blueprint's character was the accents and how much was the near-white blue paper under them.

     The triad barely notices the move: every slot loses 0.06-0.10 of contrast, because this paper is
     fractionally darker than blueprint's, and nothing else shifts. What does change is that the blue
     and teal stop being reinforced by a paper of their own temperature and have to carry the cool
     note alone, against a ground that is now very slightly against them.

     This is the best-balanced set on this base: three genuinely distinct signals, and every slot
     landing where its job wants it rather than one colour stretched to cover three. */
  [data-theme="drafting"]{
    --accent-1:#2563EB;    /* 4.83:1 — clears AA, so this is the first slot-1 on this base that can
                              carry body copy and links, not just fills and large text */
    --accent-2:#0E9384;    /* 3.55:1 — focus ring and interaction, ahead of the 3.21:1 green in the default */
    --accent-3:#FA5A2D;    /* 2.99:1 — large text, fills and UI. The warm break across the two cool slots. */
  }
  /* Violet, yellow and orange on the warm substrate. The one reference so far that needed no
     derivation at all: sorted by contrast the three land 1.45 / 3.25 / 5.00, which is the shape the
     three slots already want — a fill, a signal, and something legible enough to be a focus ring.
     Every value is the colour as given, in the slot its own contrast chose.

     Each also beats the default it replaces: 3.25 against the orange's 2.50, 5.00 against the
     green's 3.21, and the yellow sits just under the gold's 1.67 in the slot the gold occupies.
     Slot 2 clearing 4.5:1 means the focus ring carries body-text contrast.

     Worth noting this is the same #F9CB05 that beacon puts in slot 1. Here it is in slot 3, where
     1.45:1 is the right number rather than a stretched one — the colour did not change, the job
     did. Same lesson as the yellow on the dark ground: fit the slot to the colour's contrast, not
     the other way round. */
  [data-theme="prism"]{
    --accent-1:#FE4409;    /* 3.25:1 — the signal */
    --accent-2:#6E4FE6;    /* 5.00:1 — focus and interaction; clears AA for body text */
    --accent-3:#F9CB05;    /* 1.45:1 — fill only, never text */
  }
  /* A green family rather than a triad — analogous, 30-72deg across the whole set. They sort by
     contrast onto the slots the same way prism's did (1.19 / 1.79 / 4.59), so again nothing needed
     deriving; but where prism's spread was wide, this one is deliberately narrow and the page
     reads much quieter as a result.

     Two soft spots, both inherent to the reference rather than to the mapping:

       Slot 1 at 1.79:1 is the quietest SIGNAL of any scheme here — the default orange is 2.50 and
       prism's is 3.25. It is a light green, and a light green cannot be loud on near-white. The
       page will lean on the deep green for anything that has to be noticed.

       Slot 1 and slot 3 are only 1.50:1 apart FROM EACH OTHER and 30deg apart in hue, so in small
       or tinted use — the deck indices, the tags — they can read as one colour at two strengths
       rather than two signals. Wherever the design needs those two to be distinguishable, they
       will not carry it alone.

     The deep green does all the real work: 4.59:1 clears AA, so the focus ring and interaction
     states are the strongest thing on the page rather than an accent among equals. That is the
     scheme's character, and it is a legitimate one — restraint with a single point of emphasis. */
  [data-theme="verdant"]{
    --accent-1:#84CF46;    /* 1.79:1 — the signal, though a quiet one. See above. */
    --accent-2:#357F47;    /* 4.59:1 — focus and interaction; clears AA, and carries the scheme */
    --accent-3:#E5EB70;    /* 1.19:1 — fill only. The faintest slot 3 here; the gold is 1.67. */
  }

  /* ---- four references, mapped POSITIONALLY: band order is slot order, top to bottom.

         top band    (most surface)     -> --accent-1
         mid band                       -> --accent-2
         bottom band (accent of accent) -> --accent-3

     Settled deliberately, after three passes that each got it wrong a different way — sorting by
     contrast (which discards the authored order), and inverting the frame onto the slots (which
     assumed --accent-3 was the surface token because it is the quiet gold slot). Do not re-derive
     this from the slot semantics: they do not actually support the inference. Measured across the
     sheet, --accent-2 is the most-used accent by a wide margin (13 rules — header CTA, focus ring,
     module borders) against --accent-1's 3 and --accent-3's 2, and the isometric artwork carries
     all three in exactly equal amounts. There is no surface/mid/pop hierarchy in the tokens to
     read off. The frame order carries the intent; the slots take it in the order given.

     What that costs: --accent-1 is the site's signal slot, so the surface colour lands there and
     is asked to do the signalling. In practice that reads fine, because these references put their
     most-present colour first and the page's signalling is carried as much by --accent-2's
     interactive rules as by slot 1.

     The focus ring is slot 2, and three of the four put something too pale there. Those override
     --focus-colour rather than being remapped, so the palettes keep the order they were drawn in
     and keyboard focus still clears a usable floor. See --focus-colour in the palette block. */

  /* Cyan surface, yellow mid, orange accent. All three are light and fully saturated: 2.11:1 is
     the ceiling anywhere in the set, so nothing in this scheme can carry body copy. Bright and
     flat and poster-like, with the orange as the only thing approaching emphasis.

     DELIBERATE EXCEPTION to the positional rule above: slots 1 and 2 are swapped here, so the cyan
     sits in slot 2 and the yellow in slot 1. Positionally the cyan is the surface colour, but slot
     1 is not where a scheme's most-present colour ends up — slot 2 is, by a wide margin (13 rules
     against 3). Putting the cyan in slot 1 made the YELLOW the colour the page mostly wore, which
     inverts the reference. This is the same intent as the positional rule, applied to where the
     tokens actually land rather than to their numbering. */
  [data-theme="poster"]{
    --accent-1:#FADF57;    /* 1.25:1 — mid band */
    --accent-2:#6DD8FD;    /* 1.52:1 — the surface: slot 2 is the most-used token, so this is the
                              colour the page mostly wears */
    --accent-3:#F89527;    /* 2.11:1 — the accent's accent, the sharpest thing here */
    --focus-colour:#A85B05; /* slot 3 darkened to 4.71:1. The cyan at 1.52:1 is still too pale to
                               be a focus ring, so the override stands regardless of the swap. */
  }

  /* Green surface, pale green mid, salmon accent. The two greens are 1.50:1 apart and 30deg of hue
     from each other, so surface and mid read as one colour at two strengths — which suits the set,
     since the salmon is then unmistakably the thing being accented WITH. */
  [data-theme="orchard"]{
    --accent-1:#84CE4E;    /* 1.80:1 — the surface */
    --accent-2:#E1EB77;    /* 1.20:1 — mid */
    --accent-3:#F98961;    /* 2.24:1 — the accent's accent */
    --focus-colour:#A73106; /* slot 3 darkened to 6.36:1. Slot 2 at 1.20:1 cannot be a focus ring. */
  }

  /* Slate surface, grey-blue mid, salmon accent — the only muted reference of the four, and the
     one whose intent reads most clearly: a cool grey ground with a single warm note struck against
     it. The slate carrying the surface at 6.26:1 makes this the most strongly grounded scheme
     here, so everything else on the page has something solid to sit against. */
  [data-theme="harbour"]{
    --accent-1:#515C74;    /* 6.26:1 — the surface */
    --accent-2:#939EB9;    /* 2.51:1 — mid */
    --accent-3:#F97F68;    /* 2.38:1 — the accent's accent, the one warm note */
    --focus-colour:var(--accent-1); /* 2.51:1 is under the default green's 3.21, and the slate is
                                       already in the palette at 6.26 — no need to derive one */
  }

  /* Purple surface, blue mid, green accent. Every slot clears 3.5:1, which nothing else here
     manages, so the whole palette is legible — but the accent is a 3.55:1 green against a 4.99:1
     purple surface, so the thing meant to be sharpest is the quietest in the set. Of the four this
     is the one where the drawn intent and the contrast pull hardest against each other; worth
     deciding whether the green still reads as an accent or has become a third surface. */
  [data-theme="jewel"]{
    --accent-1:#805E92;    /* 4.99:1 — the surface */
    --accent-2:#295B89;    /* 6.65:1 — mid, and clears AA */
    --accent-3:#2B9554;    /* 3.55:1 — the accent's accent, though the quietest slot. See above. */
  }

  *{ box-sizing:border-box; margin:0; }
  html,body{ height:100%; }
  body{
    background:var(--paper); color:var(--ink);
    font-family:'BST Symbol Beta', Helvetica Neue, Arial, sans-serif; font-weight:400;
    -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
    text-rendering:optimizeLegibility;
    padding-top:var(--header-h);                  /* clear the fixed header */
  }

  /* ---------- fixed site header ----------
     wordmark left, CTA + section nav pinned top-right/centre */
  html{ scroll-padding-top:var(--header-h); }       /* anchor jumps land below the header */
  @media (prefers-reduced-motion:no-preference){ html{ scroll-behavior:smooth; } }
  .site-header{ position:fixed; inset:0 0 auto 0; z-index:50; height:var(--header-h);
    display:flex; align-items:flex-start; gap:var(--sp-4);
    padding:var(--sp-1); background:transparent;
    /* shared control height — nav pill and CTA button resolve to the same box */
    --ctl-h:calc(var(--fs-label) + 2 * var(--sp-2)); }
  /* transparent on desktop: the two children lay out straight in the header. Becomes a pill on
     mobile — see the max-width:900px block. */
  .header-bar{ display:contents; }
  /* the CTA carries a full label and a short one; desktop shows the full, mobile the short */
  .header-cta .cta-short{ display:none; }
  .site-header-logo{ display:flex; align-items:center; flex:none; border-radius:5px;
    transition:opacity var(--dur-hover) var(--ease-out), transform var(--dur-press) var(--ease-out); }
  @media (hover:hover){ .site-header-logo:hover{ opacity:.7; } }
  .site-header-logo:active{ transform:scale(.97); }
  .site-header-logo:focus-visible{ outline:var(--focus-ring); outline-offset:3px; }
  .site-header-logo img{ height:46px; width:auto; display:block; transition:height .3s ease; }
  .site-header.is-scrolled .site-header-logo img{ height:34px; }
  /* scroll DOWN retracts the wordmark up past the top edge; scroll UP brings it back (main.js
     toggles .logo-away by direction). -150% clears its own height plus the header's top padding.
     The slide transition lives here so it only applies to the translate, not the press-scale. */
  .site-header.logo-away .site-header-logo{ transform:translateY(-150%); opacity:0;
    pointer-events:none; transition:transform .4s var(--ease-out), opacity .3s var(--ease-out); }
  /* right-side CTA — a wash of the scheme's warm note (slot 3), text in the same accent */
  .header-cta{ flex:none; margin-left:auto;
    display:inline-flex; align-items:center; line-height:1; height:var(--ctl-h);
    font-size:var(--fs-label); font-weight:400; text-transform:uppercase; letter-spacing:.14em;
    padding:0 var(--sp-2);
    /* slot 3 is the scheme's warm note — the pale-orange salmon under Harbour — carried as a wash */
    border-radius:5px; color:var(--accent-3); background:color-mix(in srgb, var(--accent-3) 20%, transparent);
    -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
    text-decoration:none; white-space:nowrap;
    transition:background var(--dur-hover) var(--ease-out), transform var(--dur-press) var(--ease-out); }
  @media (hover:hover){
    .header-cta:hover{ background:color-mix(in srgb, var(--accent-3) 32%, transparent); transform:translateY(-2px); }
  }
  /* press cancels the lift and settles a hair in — the interface confirming the click */
  .header-cta:active{ transform:translateY(0) scale(.98); }
  .header-cta:focus-visible{ outline:var(--focus-ring); outline-offset:2px; }
  /* nav links float in a blurred pill fixed to the top-centre. The fill is the body's own paper at
     40% — the same colour the page is on, just sheer, so the blur behind it does the work and the
     island reads as a soft frosted panel of the page rather than a distinct chip. It follows the
     scheme through --paper, so it tints itself per colourway with no per-scheme rule. The links
     stay monochrome (ink / dim). */
  .site-nav{ position:fixed; left:50%; transform:translateX(-50%);
    z-index:51; display:flex; align-items:center; gap:var(--sp-1);
    height:var(--ctl-h); padding:0 var(--sp-1);
    border-radius:5px; overflow:hidden;
    background:color-mix(in srgb, var(--paper) 60%, transparent);
    -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px); }
  .site-nav a{ font-size:var(--fs-label); font-weight:400; text-transform:uppercase;
    letter-spacing:.14em; color:var(--dim); text-decoration:none; white-space:nowrap;
    padding:var(--sp-1) var(--sp-2); border-radius:5px;
    transition:color var(--dur-hover) var(--ease-out), background var(--dur-hover) var(--ease-out), transform var(--dur-press) var(--ease-out); }
  /* hover is a faint chip under brightened text — half the active fill, so the two never read
     the same, echoing the module-row hover. is-active is declared after and wins the tie. */
  @media (hover:hover){ .site-nav a:hover{ color:var(--ink); background:color-mix(in srgb, var(--ink) 6%, transparent); } }
  .site-nav a:active{ transform:scale(.96); }
  /* inset ring: the nav pill sets overflow:hidden, so an outset outline would be clipped */
  .site-nav a:focus-visible{ outline:var(--focus-ring); outline-offset:-2px; }
  /* active section — the matching link fills with the ink chip */
  .site-nav a.is-active{ color:var(--ink); background:color-mix(in srgb, var(--ink) 12%, transparent); }
  @media (max-width:900px){
    .site-nav{ display:none; }
    /* button (and so the wordmark) sit at the desktop control height */
    .site-header{ --ctl-h:calc(var(--fs-label) + 2 * var(--sp-2)); }
    /* wordmark + CTA share one frosted bar — nav-pill treatment, spanning the full header width
       with the wordmark at the left edge and the CTA at the right */
    .header-bar{ display:flex; align-items:center; width:100%;
      justify-content:space-between; gap:var(--sp-2);
      padding:var(--sp-2); border-radius:5px; overflow:hidden;
      background:color-mix(in srgb, var(--paper) 60%, transparent);
      -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px); }
    /* space-between does the placing now, so drop the desktop auto-push */
    .header-cta{ margin-left:0; }
    /* short label fits the full-width bar; the full phrase would clip on a phone */
    .header-cta .cta-long{ display:none; }
    .header-cta .cta-short{ display:inline; }
    /* wordmark shrinks to exactly the button's height and holds it whether scrolled or not */
    .site-header-logo img,
    .site-header.is-scrolled .site-header-logo img{ height:var(--ctl-h); }
    /* no slide-away on mobile — the wordmark stays put in the bar (JS still toggles the class) */
    .site-header.logo-away .site-header-logo{ transform:none; opacity:1; pointer-events:auto; }
  }

  .hero{ position:relative; min-height:100svh; display:grid; place-items:center;
         overflow:hidden; padding:5vh 4vw; }
  /* closing footer sits shorter than a full screen so there's less empty space below the lockup */
  footer.hero{ min-height:78svh; padding-bottom:3vh; }

  /* background grid — the self-drawing 'lines' animation, dashed, fading up the section */
  .grid{ position:absolute; inset:0; width:100%; height:100%; z-index:0; pointer-events:none;
    -webkit-mask-image:linear-gradient(to top,#000 0%,#000 28%,transparent 62%);
            mask-image:linear-gradient(to top,#000 0%,#000 28%,transparent 62%); }

  /* the lockup is one canvas: bubbles AND wordmark, so the frontmost bubble sits in
     front of the letterforms and still casts a contact shadow onto the cluster
     behind it. 1.449 is the stage's aspect (cluster bbox + shadow bleed). */
  .lockup{ position:relative; z-index:2; display:flex; flex-direction:column; align-items:center; }
  .stage{ aspect-ratio:3452 / 2383; width:min(1360px, 96vw, calc(72svh * 1.449)); }
  .bubbles{ display:block; width:100%; height:100%; user-select:none; }

  /* tagline sits under the stage; the wordmark above it lives in the canvas */
  .hero-tag{ font-size:var(--fs-body); line-height:var(--lh-body); color:var(--ink);
             margin:var(--sp-2) 0 0; text-align:center; }
  /* copyright pinned to the bottom of the closing section */
  .hero-copy{ position:absolute; left:50%; bottom:var(--sp-4); transform:translateX(-50%);
              margin:0; z-index:20; }

  /* the wordmark lives in the canvas; keep it in the document for machines */
  .sr-only{ position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }

  @media (max-width:640px){ .stage{ width:min(98vw, calc(58svh * 1.449)); } }

  /* ---------- content sections ---------- */
  /* spacing tier 5 pads all sides; content sits attached to that padding */
  .sec{ padding:var(--sp-5) var(--sp-3); }
  #enquire{ padding-top:calc(var(--sp-5) * 2); }   /* closing CTA — double top padding */
  /* tablet ≤900px: tighten section padding to tier 3 */
  @media (max-width:900px){ .sec{ padding:var(--sp-3); } }
  @media (max-width:600px){ .sec{ padding:var(--sp-4) var(--sp-2); } }  /* mobile: more vertical space between sections, sides stay tight */
  /* plain label (uppercase, tracked) — used for section kickers */
  .label{ font-size:var(--fs-label); font-weight:400; text-transform:uppercase;
          letter-spacing:var(--ls-label); line-height:var(--lh-label);
          color:var(--ink); margin:0 0 var(--sp-1); }
  /* tag — the ONE tag component. Sentence-case coloured chip: label-sized type,
     sp-1 padding, radius 5, own colour at 18% opacity. Same size & padding
     everywhere (this is the chip shown inside the placeholder boxes). */
  .tag{ display:inline-block; font-weight:300; font-size:var(--fs-label); line-height:var(--lh-tight);
        padding:var(--sp-1); border-radius:2px; }
  .tag--orange{ color:var(--orange); background:color-mix(in srgb, var(--orange) 18%, transparent); }
  .tag--gold  { color:var(--gold);   background:color-mix(in srgb, var(--gold) 18%, transparent); }
  .tag--green { color:var(--green);  background:color-mix(in srgb, var(--green) 18%, transparent); }
  .s-title{ font-weight:400; font-size:var(--fs-heading); line-height:var(--lh-heading);
            letter-spacing:var(--ls-heading); margin:0; max-width:var(--mw-heading); text-wrap:balance; }
  /* placeholder for a visualiser (to be filled later) — no border-radius */
  .vizbox{ margin-top:var(--sp-3); width:80%; aspect-ratio:16/9; background:var(--panel); position:relative; border-radius:5px; }
  /* placement only — pin the tag to the box's top-right, inset by sp-2.
     Size & padding come from .tag; never re-declare them here. */
  .vizbox .tag{ position:absolute; top:var(--sp-2); left:var(--sp-2); }

  /* box + stacked CTAs side by side */
  .viz-row{ margin-top:var(--sp-3); display:flex; gap:var(--sp-4); align-items:flex-start; }
  .viz-row .vizbox{ margin-top:0; flex:0 0 80%; }
  .btn-stack{ flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--sp-2); }
  /* ≤900px (tablet & down): buttons drop under the headline, above the placeholder */
  @media (max-width:900px){
    .viz-row{ flex-direction:column; gap:var(--sp-3); }
    .viz-row .btn-stack{ order:-1; flex-direction:row; flex-wrap:wrap;
                         width:100%; align-items:flex-start; }
    .viz-row .vizbox{ flex:none; width:100%; }
  }
  /* font-family:inherit because a <button class="btn"> (the contact form's submit) would otherwise
     fall back to the UA control font — anchors styled as .btn inherit it and look right, so the gap
     only shows on the one real button. */
  .btn{ display:inline-flex; align-items:center; justify-content:center; text-align:center;
        font-family:inherit;
        font-size:var(--fs-body); font-weight:400; line-height:var(--lh-tight); padding:var(--sp-2) var(--sp-3);
        border-radius:5px; text-decoration:none; cursor:pointer;
        transition:transform var(--dur-press) var(--ease-out), background var(--dur-hover) var(--ease-out); }
  /* a small, subtle lift on hover; the press below settles it flat and a touch smaller */
  @media (hover:hover){ .btn:hover{ transform:translateY(-2px); } }
  .btn:active{ transform:translateY(0) scale(.97); }
  .btn:focus-visible{ outline:var(--focus-ring); outline-offset:2px; }
  .btn--primary{ background:var(--ink); color:var(--paper); }
  /* translucent, so it blurs the cymatics grain behind it — same treatment as .header-cta */
  .btn--secondary{ background:color-mix(in srgb, var(--ink) 30%, transparent); color:var(--ink);
    -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px); }
  /* reduced motion: keep the colour / background / shadow feedback (it aids comprehension, and the
     guidance is "fewer and gentler", not zero) but drop every transform-based movement — the lifts
     and press-scales across the whole interactive set. */
  @media (prefers-reduced-motion:reduce){
    .btn:hover, .header-cta:hover, .site-header-logo:hover{ transform:none; }
    .btn:active, .header-cta:active, .site-nav a:active,
    .site-header-logo:active, .modal-close:active{ transform:none; }
  }

  /* centred section — kicker over a light heading over a square placeholder visual */
  .sec--center{ display:flex; flex-direction:column; align-items:center; text-align:center; }
  /* opening hero — full viewport height; text pinned bottom-left in flow (section
     padding respected), the pixel visual absolutely centred in the section's middle */
  .sec--hero{ position:relative; overflow:hidden; min-height:calc(100svh - var(--header-h)); padding:var(--sp-3);
              display:flex; flex-direction:column; justify-content:flex-end;
              align-items:flex-start; text-align:left; }
  /* hero headline — display-scale, tight leading, near-black (matches Figma 42px) */
  .sec--hero .s-title{ max-width:none; font-size:var(--fs-heading); line-height:var(--lh-tight); color:var(--ink); margin:0; }
  .sec--hero .lead{ margin-inline:0; }
  /* extra breathing room between the hero and the thesis section */
  #built-in{ margin-top:var(--sp-5); }
  /* bottom-left copy card — translucent frosted panel, wide; children stacked with a 24px rhythm */
  .hero-card{ position:relative; z-index:1; border-radius:4px; padding:var(--sp-3);
              width:min(611px,90vw);
              display:flex; flex-direction:column; align-items:flex-start; gap:var(--sp-3);
              background:color-mix(in srgb, var(--panel) 75%, transparent); backdrop-filter:blur(7px); -webkit-backdrop-filter:blur(7px); }
  /* eyebrow — softer ink, lighter tracking than the global label */
  .hero-card .label{ color:var(--ink-mid); letter-spacing:.06em; margin:0; }
  /* body — softer ink, ~346px measure so it sits narrower than the headline (per Figma) */
  .hero-card .lead{ margin:0; max-width:346px; color:var(--ink-mid); line-height:var(--lh-body); }
  /* gap handles the spacing above the button now */
  .hero-card .btn{ margin-top:0; }
  /* ink-threshold pixel reveal — centred in the section, behind/above the corner text */
  /* top 45% rather than 50%: the visual sits a twentieth of the hero's height above centre, lifting
     it a little clear of the copy card in the bottom-left. left/translate keep it horizontally centred. */
  .hero-pixels{ position:absolute; top:45%; left:50%; transform:translate(-50%,-50%);
                width:min(2200px,102vw); height:auto; aspect-ratio:900/432; margin:0; }
  /* On a narrow screen that 102vw is barely a band: the frame is 900:432, so fitting its WIDTH to
     the viewport leaves it about a fifth of the hero's height. Let it out past both edges instead —
     the section already clips (overflow:hidden), and it is centred, so the overflow is split evenly
     and the middle of the image stays on screen. Width is what drives it; the aspect-ratio takes the
     height with it, and the canvas holds a fixed 150 columns, so the cells simply grow rather than
     multiplying. Phones get the larger push since they are the ones the band shrank on. */
  @media (max-width:760px){ .hero-pixels{ width:135vw; } }
  @media (max-width:600px){ .hero-pixels{ width:190vw; } }
  /* square placeholder visual — stands in for a graphic (same panel colour as the rest) */
  .vizbox--square{ margin-top:var(--sp-4); width:min(480px,72vw); aspect-ratio:1/1; border-radius:5px; }
  .built-viz{ display:block; background:transparent; }

  /* centre the kicker + headline above the cards (cards keep their own left align) */
  #how-we-work > .label,
  #how-we-work > .s-title{ text-align:center; margin-inline:auto; }
  /* three left-aligned cards: portrait box → tag → title → text */
  .cards{ margin:var(--sp-4) auto 0; max-width:1200px; display:grid;
          grid-template-columns:repeat(3,1fr); gap:var(--sp-2); }
  .card{ text-align:left; }
  /* ≤900px: 3 cards → 2-col grid (third wraps under the first two); ≤600px: single stack */
  @media (max-width:900px){ .cards{ grid-template-columns:repeat(2,1fr); gap:var(--sp-3); } }
  @media (max-width:600px){ .cards{ grid-template-columns:1fr; } }
  .cardbox{ width:100%; aspect-ratio:3/4; background:var(--panel); margin:0 0 var(--sp-3); border-radius:5px; position:relative; overflow:hidden; }
  /* a visual fills the box; the tag sits on top of it */
  .cardbox img{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain; display:block; }
  /* tag sits inside the box, pinned to the top-left with sp-2 inset */
  .cardbox .tag{ position:absolute; top:var(--sp-2); left:var(--sp-2); margin:0; }
  /* body carries two line-heights: tight for titles, prose for running copy */
  .card-title{ font-size:var(--fs-body); font-weight:400; line-height:var(--lh-body);
               margin:0 0 var(--sp-1); text-wrap:balance; }
  .card-text{ font-size:var(--fs-body); line-height:var(--lh-prose); margin:0; color:var(--ink-soft); }

  /* ---------- why tuotto: a scaling deck of value-prop cards ----------
     Sits above the modules list. The cards are sticky and stack by themselves — later cards paint
     over earlier ones (source order), and a per-index step stairsteps their pinned tops so the back
     of the deck peeks above the front card. main.js writes --deck-s (scale) and --deck-o (opacity)
     per card so a covered card recedes and dims; with reduced motion that stays off and they rest at
     1 / 1, a plain stack. No literal colour — the index tints resolve from the accent triad. */
  .sec--deck{ --deck-card-h:min(560px, 72svh);                   /* pinned card height — also used to centre it */
              --deck-step:20px;                                   /* stairstep between pinned cards — with the graduated scale this reads as a fan */
              /* pin so the fan sits centred in the viewport: half the viewport minus half the card,
                 pulled back another 1.5 steps so the MIDDLE of the stack (not the top card) centres —
                 but never let the top card slip under the fixed header. */
              --deck-stick:max( calc(var(--header-h) + var(--sp-3)),
                                calc((100svh - var(--deck-card-h)) / 2 - 1.5 * var(--deck-step)) );
              --deck-gap:55vh; }                                  /* scroll travel between cards */
  /* centred heading above the deck — mirrors the other sections' label + s-title lockup. It scrolls
     away normally before the first card pins, so it doesn't interfere with the sticky stack. */
  .deck-intro{ text-align:center; max-width:1180px; margin:0 auto var(--sp-5); }
  .deck-intro .label{ color:var(--ink-mid); }
  .deck-intro .s-title{ margin-inline:auto; }
  /* capped to the what-we-build stage width (1180px) and centred, so the cards don't run full-bleed */
  .deck{ display:flex; flex-direction:column; gap:var(--deck-gap);
         max-width:1180px; margin-inline:auto; }
  /* padding lives on the text column, NOT the card, so the canvas can run to the card's edges.
     overflow:hidden clips the bleeding canvas to the rounded corners (safe on the sticky element
     itself — only a scroll-container ANCESTOR would break the pin). */
  .deck-card{ position:sticky; top:calc(var(--deck-stick) + var(--i) * var(--deck-step));
              display:grid; grid-template-columns:minmax(0,1fr) minmax(0,42%); gap:0;
              align-items:stretch;
              min-height:var(--deck-card-h);
              background:var(--panel); border-radius:5px; overflow:hidden;
              transform-origin:top center; will-change:transform;
              /* recede by SCALE only — the cards stay fully opaque so a covered card never shows the
                 one behind through it. (The opacity dim was dropped on request.) */
              transform:scale(var(--deck-s, 1)); }
  /* left column: padded; index floats to the top, copy anchors to the bottom */
  .deck-body{ display:flex; flex-direction:column; justify-content:space-between; gap:var(--sp-4);
              min-width:0; padding:var(--sp-4); }
  /* right column: the pixel canvas fills it edge-to-edge (stretches to the card height on desktop).
     The canvas is absolute so it contributes no intrinsic size — the grid drives the box. */
  .deck-media{ position:relative; align-self:stretch; min-height:0; }
  .deck-pixels{ position:absolute; inset:0; width:100%; height:100%; display:block; }
  /* every index is salmon (accent-3) — one accent across the whole deck, on request. 72% keeps them
     the soft, decorative weight they had rather than a hard fill. */
  .deck-index{ margin:0; font-weight:300; line-height:1; letter-spacing:-.02em;
               font-size:clamp(3.25rem, 2rem + 6vw, 6rem); font-variant-numeric:tabular-nums;
               color:color-mix(in srgb, var(--accent-3) 72%, transparent); }
  /* let the copy use the wider card: the headline caps at a display measure, the sub at the
     site's wide prose measure (60ch), rather than the old narrow 40ch column */
  .deck-copy{ max-width:none; }
  .deck-copy .label{ color:var(--ink-mid); margin:0 0 var(--sp-2); }
  .deck-head{ font-weight:400; font-size:var(--fs-display); line-height:var(--lh-display);
              letter-spacing:var(--ls-heading); margin:0; max-width:22ch; text-wrap:balance; }
  .deck-sub{ margin:var(--sp-3) 0 0; max-width:60ch; color:var(--ink-soft);
             font-size:var(--fs-body); line-height:var(--lh-prose); }
  /* stack the card: copy on top, the pixel canvas below it with its own aspect (it can no longer
     stretch to the card height once the columns collapse) */
  @media (max-width:760px){
    .deck-card{ grid-template-columns:1fr; min-height:auto; }
    .deck-media{ align-self:auto; aspect-ratio:16/10; }
  }
  @media (max-width:600px){
    .sec--deck{ --deck-gap:42vh; --deck-step:13px; }
    .deck-body{ padding:var(--sp-3); }
    .deck-media{ aspect-ratio:4/3; }
  }

  /* split section — text column on the left, a tall placeholder box on the right */
  .split{ display:grid; grid-template-columns:1fr 0.75fr; gap:var(--sp-5);
          align-items:stretch; min-height:85vh; }
  /* left column content is vertically centred within the section */
  .split-main{ display:flex; flex-direction:column; justify-content:center; }
  /* the steps list sits below the heading, generously spaced from it */
  .steps{ margin-top:var(--sp-5); display:flex; flex-direction:column; gap:var(--sp-4); }
  /* reuse card-title / card-text; give a touch more room between title and text here */
  .step .card-title{ margin:0 0 var(--sp-2); }
  /* body copy in this section is base body size, capped for readable line length */
  .step .card-text{ max-width:32ch; }
  /* the right-hand box stretches to fill its column's full width and height
     (no fixed ratio), up to — but respecting — the section padding */
  .split-viz{ margin-top:0; aspect-ratio:auto; width:auto;
              display:flex; align-items:center; justify-content:center; }
  .split-viz img{ height:auto; width:auto; max-height:88%; max-width:100%; object-fit:contain; }
  @media (max-width:760px){ .split{ grid-template-columns:1fr; }
                            .split-viz{ min-height:60vw; } }
  /* placement only — size & padding come from .btn */
  .sec--center .btn{ margin-top:var(--sp-3); }
  /* The full-width ink card. Only #what-we-build uses this now — the modules section used to
     share it via a --panel fill and no longer does. Never give .feature a ratio — its content
     drives the height (a fixed 5/2 starved it before). The inner layer stays in flow and
     only the grid canvas is absolute (it paints behind, clipped to the rounded corners). */
  .feature{ width:100%; background:var(--ink);
            border-radius:5px; position:relative; overflow:hidden; }
  /* ---------- modules: a tab list — click to select, plus a slow autoplay whose progress fills
     the rail beside the open module. No card and no pin: the list sits straight on the paper and
     only the cymatic field carries the panel fill. Full-bleed to the section's own padding, with
     no container cap: the list holds at 438 and the field takes everything left, so it scales
     with the viewport rather than sitting in gutters. */
  /* one statement over two lines: the claim in ink, the qualifier at half strength. No
     --mw-heading cap here — both lines are meant to run the full width of the row below. */
  .mod-head{ margin-block:0 var(--sp-3); color:var(--ink); font-weight:400;
             font-size:var(--fs-heading);
             line-height:var(--lh-tight); letter-spacing:var(--ls-heading); }
  .mod-head-sub{ display:block; color:color-mix(in srgb, var(--ink) 50%, transparent); }
  /* two real columns. Width-driven, the way the design draws it: the list is a fixed 438 and the
     field takes whatever is left, square. space-between rather than the default flex-start: once
     the field hits its ceiling the row has slack, and this parks it in the middle — the list's
     rail stays flush to the headline's left edge and the field stays flush to the right, instead
     of the pair huddling left with dead space trailing off the end. */
  .mod-row{ display:flex; gap:var(--sp-3); justify-content:space-between; align-items:flex-start; }
  /* flex-basis 0, not auto: auto would resolve to the canvas's intrinsic 1120px and shrink the
     list off its 438.
     The ceiling is max-WIDTH even though a height is what we're capping, and that's deliberate:
     flex owns the main axis, so the width is already resolved before aspect-ratio derives the
     height from it. A max-height clamps the height WITHOUT feeding back to the width — measured
     at 1920: 1410x1024, i.e. a rectangle with the round plate stretched to an ellipse inside it.
     Capping the width caps the side of the square, which at 1/1 is the same number. Without it
     the field just tracks the viewport: 930 at 1440, 1410 at 1920, 2050 at 2560.
     The fill and radius live on the canvas itself — JS overdraws the plate (zoom), and this box
     is what crops it. */
  .mod-cym{ flex:1 0 0; min-width:0; aspect-ratio:1/1; max-width:80svh;
            display:block; pointer-events:none;
            background:var(--panel); border-radius:8px; }
  /* --mod-dwell is the autoplay pace, and the ONLY place it's set: the fill animation below is
     the clock JS advances on, so there's no second number in JS to keep in step with it. */
  .mod-list{ --mod-dwell:5s;
             flex:0 0 438px; min-width:0; list-style:none; margin:0; padding:0; }
  /* flush, NOT gapped: the 2px border reads as one continuous rail down all six rows with only
     the active segment filled. A gap would break the rail into six dashes. The row's own padding
     carries the rhythm instead.
     The whole row is the hit area (JS listens on the li, not the button), so the cursor belongs
     here rather than on the label. The radius sits on the base, not just .is-active, so the hover
     tint and the open fill are the same shape. */
  .mod{ position:relative; cursor:pointer; border-left:2px solid color-mix(in srgb, var(--green) 25%, transparent);
        border-radius:0 8px 8px 0;
        padding:var(--sp-2) var(--sp-3);
        transition:background-color .3s ease, border-color .3s ease; }
  /* the autoplay's progress bar: it sits ON the rail (left:-2px — absolute children lay out
     against the padding box, so 0 would be inside the border) and fills it over one dwell. */
  .mod::before{ content:''; position:absolute; left:-2px; top:0; bottom:0; width:2px;
                background:var(--green); transform:scaleY(0); transform-origin:top; }
  .mod.is-active{ background:color-mix(in srgb, var(--green) 15%, transparent); cursor:default; }
  /* scaleY, not height: it composites rather than laying out. The animation is declared ONLY on
     the active row, so losing .is-active resets the bar instantly instead of unwinding it over a
     full dwell. */
  .mod.is-active::before{ animation:mod-fill var(--mod-dwell) linear forwards; }
  @keyframes mod-fill{ from{ transform:scaleY(0); } to{ transform:scaleY(1); } }
  /* Anything that should stop the autoplay stops this animation instead — the advance is driven by
     it ending, so the bar can never disagree with the carousel. Hover deliberately does NOT pause:
     it keeps running under the pointer. Focus-within still does, which is what leaves a keyboard
     user a way to stop it (and is the only pause mechanism left, so it earns its keep). Off-screen
     pauses via .is-onscreen, toggled by the same observer that gates the grain sim — otherwise
     you'd come back to a random module. */
  .mod-list:focus-within .mod.is-active::before{ animation-play-state:paused; }
  #modules-card:not(.is-onscreen) .mod.is-active::before{ animation-play-state:paused; }
  /* hover says "clickable" — a faint wash of the same green the open row uses, at a fraction of
     its strength so the two never read as the same state, plus the rail brightening. Gated
     because on touch :hover sticks after the finger lifts, which would strand a row looking
     half-active. */
  @media (hover:hover){
    .mod:not(.is-active):hover{ background:color-mix(in srgb, var(--green) 6%, transparent);
                                border-left-color:color-mix(in srgb, var(--green) 60%, transparent); }
  }
  /* press deepens the wash to sit between hover (6%) and open (15%), so the click registers as
     depth before the row opens. No transform here — a scale would drag the rail and progress bar
     out of true. Ungated: this feedback is wanted on touch too. */
  .mod:not(.is-active):active{ background:color-mix(in srgb, var(--green) 11%, transparent); }
  /* the ring goes on the row, because the row is the target — the button's own outline would draw
     a rectangle round the label only, which now understates the hit area. :has(:focus-visible)
     rather than :focus-within, so it appears for keyboard focus but not on click. */
  .mod-btn:focus-visible{ outline:none; }
  .mod:has(.mod-btn:focus-visible){ outline:2px solid var(--focus-colour); outline-offset:-2px; }
  /* The name leads the row. This inverts the reference, which put a 12px small-caps kicker over
     26px copy — the names are what you scan and click, so they shouldn't be less than half the
     size of their own description. Full ink in every state; only the rail and fill around them
     change.
     Uppercase is dropped rather than scaled up with the size: at 26px with --ls-label two of the
     six names wrap the 388px column, and six rows of caps at body-large size shouts. There's no
     weight to lean on either — only 300 and 400 are loaded, so anything heavier would synthesise.
     cursor:inherit so the label doesn't punch an arrow-shaped hole in the row's pointer — the
     whole row is one hit area and should feel like one. */
  .mod-btn{ display:block; width:100%; text-align:left; padding:0; border:0; background:none;
            font-family:inherit; font-weight:400; cursor:inherit; color:var(--ink);
            font-size:var(--fs-body-large); line-height:var(--lh-body-large); }
  /* 0fr -> 1fr slides the copy open; display/height:auto can't transition */
  .mod-text{ display:grid; grid-template-rows:0fr; transition:grid-template-rows .4s ease; }
  /* body, not body-large: the name owns the row now and this is the supporting note. --lh-prose
     is the sheet's leading for running copy, as opposed to --lh-body for titles.
     The old 32ch cap goes with the size change. `ch` is measured at the element's OWN size, so at
     26px it was ~416px — wider than the 388px column, i.e. never actually binding — but at 16px
     it would be ~256px and would squeeze the copy into a needlessly narrow column inside an empty
     one. The row's width gives ~48ch, which is already a good measure.
     The opacity co-animation is what stops the copy reading as a wipe: the row's height is the
     only thing the 0fr→1fr can move, so without this the text is dragged out from under a clip
     edge. Fading out is quicker than fading in, and the fade-in is held back a beat, so the row
     is already opening before the words arrive — closing feels immediate, opening feels led. */
  .mod-text > p{ overflow:hidden; min-height:0; margin:0; color:var(--ink);
                 font-size:var(--fs-body); line-height:var(--lh-prose);
                 opacity:0; transition:opacity .15s ease; }
  .mod.is-active .mod-text > p{ opacity:1; transition:opacity .3s ease .1s; }
  .mod.is-active .mod-text{ grid-template-rows:1fr; }
  /* padding, not margin: the row collapses to 0fr when closed and overflow:hidden clips this,
     so the gap only exists while the copy is open */
  .mod.is-active .mod-text > p{ padding-top:var(--sp-1); }
  /* ≤760px: one column. align-items goes back to stretch, or the column would shrink both
     children to their content width. */
  @media (max-width:760px){
    .sec{ padding:var(--sp-5) var(--sp-2); }
    .mod-row{ flex-direction:column; align-items:stretch; }
    /* order, not DOM order: the list comes first in source (it's the content; the field is
       decorative), but the stack still reads copy → field → modules the way it always has */
    .mod-cym{ order:-1; flex:none; width:100%; }
    .mod-list{ flex:none; }
  }
  @media (prefers-reduced-motion:reduce){
    .mod, .mod-text, .mod-text > p{ transition:none; }
    /* no autoplay at all: the advance is driven by the fill ending, so killing the animation
       stops the carousel dead. The bar just sits full, marking the open row's rail. */
    .mod.is-active::before{ animation:none; transform:scaleY(1); }
  }

  /* ---------- who we've worked with: an endless logo marquee ----------
     The spacing is padding INSIDE each item rather than a gap on the track. With a gap the
     track would measure two sets PLUS one extra gap, so translateX(-50%) wouldn't land on
     exactly one set and the loop would visibly drift. Half the spacing on each side also makes
     the seam between the two sets match the spacing everywhere else. */
  /* touch-action:pan-y is what keeps the page scrollable: horizontal drags are ours, vertical
     swipes still scroll the page. Without it a swipe over the marquee traps the finger. */
  /* centre the kicker above the logo banner */
  #clients > .label{ text-align:center; }
  /* the row box each mark is centred in — see .marquee-set img */
  .marquee{ --brand-box:54px;
            margin-top:var(--sp-4); overflow:hidden; cursor:grab; touch-action:pan-y;
            -webkit-mask-image:linear-gradient(to right,transparent,#000 6%,#000 94%,transparent);
                    mask-image:linear-gradient(to right,transparent,#000 6%,#000 94%,transparent); }
  .marquee.is-dragging{ cursor:grabbing; }
  /* no CSS animation: a running animation outranks inline styles, so it would override the
     transform the drag writes. JS drives the position for both the auto-slide and the drag. */
  .marquee-track{ display:flex; width:max-content; will-change:transform; }
  .marquee-set{ display:flex; align-items:center; margin:0; padding:0; list-style:none; }
  /* Each mark is now its own artwork at its own size — 1.5:1 for Frood through to 13.7:1 for
     Burning Orange — where the old set was seven identical squares that could share one width and
     a baked-in centre line. So each gets its own height and centres in a common row box.

     The heights are per mark because no single rule fits them. Matching heights makes WAER four
     times Burning Orange's visual mass — a bounding box's height is the mark's height, not its
     weight — and matching widths does the reverse. These normalise the geometric mean of each
     mark's dimensions (its area, near enough), so a long thin wordmark is given its length and a
     squat one its height, and they carry the same weight in the row. Derived as 62 * h / √(w*h)
     from the artwork's own size, then rounded; nudge one if it reads heavy, and use that formula
     as the starting point for anything added.

     Keep the spacing as padding INSIDE each item — see the note on .marquee above; a gap here
     would break the seamless loop. */
  .marquee-set li{ position:relative; flex:none; padding-inline:calc(var(--sp-5) / 2);
                   height:var(--brand-box); display:flex; align-items:center; justify-content:center; }
  /* The marks are single-colour silhouettes, so we tint them with the accent instead of drawing
     the black artwork: each SVG is used as a mask over a coloured box (::before). Driving the fill
     from --accent-3/--ink rather than a filter is what lets it track the palette toggle — and each
     mark fades to ink on hover. The <img> stays only for layout width and its alt text, hidden
     under the tint. --mark-h drives both the image height and the mask height so the two align. */
  .marquee-set img{ width:auto; height:var(--mark-h); display:block; opacity:0;
                    user-select:none; -webkit-user-drag:none; }
  .marquee-set li::before{ content:""; position:absolute; inset:0;
                           /* slot 3 — the scheme's warm note (pale-orange salmon under Harbour) */
                           background:var(--accent-3); transition:background .3s ease;
                           -webkit-mask:var(--logo) center / auto var(--mark-h) no-repeat;
                                   mask:var(--logo) center / auto var(--mark-h) no-repeat; }
  @media (hover:hover){ .marquee-set li:hover::before{ background:var(--ink); } }
  .b-arc            { --logo:url(assets/brands/arc.svg);             --mark-h:37px; }
  .b-burning-orange { --logo:url(assets/brands/burning-orange.svg);  --mark-h:17px; }
  .b-donna-franca   { --logo:url(assets/brands/donna-franca.svg);    --mark-h:41px; }
  .b-frood          { --logo:url(assets/brands/frood.svg);           --mark-h:51px; }
  .b-papaya         { --logo:url(assets/brands/papaya.svg);          --mark-h:48px; }
  .b-waer           { --logo:url(assets/brands/waer.svg);            --mark-h:31px; }
  .b-wuffes         { --logo:url(assets/brands/wuffes.svg);          --mark-h:42px; }

  /* self-drawing dashed grid inside the card, clipped to its rounded corners */
  .feature-grid{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; }
  /* content layer over the grid: lockup top-left, the mosaic beneath it */
  .feature-inner{ position:relative; z-index:1; padding:var(--sp-4);
                  display:flex; flex-direction:column; }
  .feature-inner .label{ color:var(--paper); margin:0 0 var(--sp-2); }
  .feature-head{ color:var(--paper); font-weight:300; font-size:var(--fs-heading);
                 line-height:var(--lh-heading); letter-spacing:var(--ls-heading); margin:0;
                 max-width:var(--mw-heading); text-wrap:balance; }
  /* the lead sits on ink here, so it takes the soft grey rather than the paper tone */
  .feature-lockup .lead{ margin-top:var(--sp-3); }
  /* ---------- what we build: a wall of product boards laid into an isometric plane ----------
     the stage reserves the on-screen footprint of the sheared mosaic + its cast shadow.
     Sized against its container, not the viewport: it lives inside the ink card's padding,
     so a vw-based width would overflow the card rather than centre in it. */
  /* 1040/590, not the plane's own 1040/780: shearing the box flattens it, so the tiles only
     ever cover 0.576 of the bento's width in height. The taller ratio reserved ~250px of empty
     band above and below. This hugs the tiles with a little slack. */
  .board-stage{ position:relative; width:100%; max-width:1180px; margin:0 auto;
                aspect-ratio:1040 / 590; overflow:visible; }
  /* A loose scatter of boards, freely placed then sheared onto the same ~33° isometric plane.
     Taken out of flow deliberately: transforms don't affect layout, so in flow the un-sheared
     780-tall box would size the stage and defeat the ratio above. Absolute + translate centres
     it instead.
     The tiles cover only 80.5% of the bento's width and sit off-centre inside it (measured),
     so -50% alone leaves the mosaic 5.2% left and 0.6% high. The offsets below correct that.
     Everything here is a % of the bento's own box, so it holds at any scale — which is what
     lets the mobile rule below resize the mosaic without re-deriving the centring. */
  .board-bento{ position:absolute; top:50%; left:50%; width:88.1%; aspect-ratio:1040 / 780;
     transform:translate(-44.798%,-49.393%) matrix(0.573,-0.380,0.580,0.380,0,0); transform-origin:center; }
  /* tiles are positioned as % of the plane; height follows each board's own ratio */
  .board-tile{ position:absolute; display:block; height:auto; border-radius:14px;
     box-shadow:0 60px 19.1px var(--shadow-tile); }
  .t-statement{ left:1%;  top:10%; width:22%; z-index:4; }
  .t-donut{     left:7%;  top:50%; width:23%; z-index:4; }
  /* the service board is the hero: largest, front-centre */
  .t-panel{     left:26%; top:30%; width:46%; z-index:5; }
  .t-segments{  left:60%; top:2%;  width:21%; z-index:3; }
  .t-vbars{     left:80%; top:14%; width:20%; z-index:2; }
  .t-ratio{     left:56%; top:56%; width:22%; z-index:3; }

  /* the hand-built service board, kept among the finished cards; its type scales with the tile */
  .board-panel{ container-type:inline-size; display:grid; grid-template-columns:1fr 1fr;
     aspect-ratio:748 / 500; background:var(--paper); overflow:hidden;
     border:1px solid color-mix(in srgb, var(--ink) 6%, transparent); }
  .pcol{ padding:5.4% 5.6%; }
  .pcol + .pcol{ border-left:1px solid var(--line); }
  .board-head{ display:flex; justify-content:space-between; align-items:baseline; margin-bottom:8%; }
  .board-kicker{ font-size:2.4cqw; letter-spacing:.14em; text-transform:uppercase; color:var(--dim); }
  .metric{ display:grid; grid-template-columns:1fr auto; align-items:baseline;
           column-gap:6%; padding:5.5% 0; border-top:1px solid var(--line); }
  .metric:first-of-type{ border-top:0; padding-top:0; }
  .metric-label{ grid-column:1; font-size:2.3cqw; text-transform:uppercase; letter-spacing:.06em; color:var(--dim); }
  .metric-val{ grid-column:1; font-size:6.6cqw; font-weight:400; line-height:1; margin-top:2%; }
  .metric-delta{ grid-column:2; grid-row:1 / 3; align-self:center; font-size:2.4cqw; }
  .metric-delta.up{ color:var(--green); } .metric-delta.down{ color:var(--orange); }
  .pcol.board-chart{ padding:0; position:relative; }
  .board-chart svg{ display:block; width:100%; height:100%; }
  .chart-legend{ position:absolute; left:8%; bottom:8%; display:flex; gap:8%;
                 font-size:2cqw; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-soft); }
  .chart-legend b{ font-weight:400; display:inline-flex; align-items:center; gap:.4em; }
  .chart-legend b::before{ content:''; width:.5em; height:.5em; border-radius:50%; background:currentColor; }
  .lg-preventive{ color:var(--orange); } .lg-corrective{ color:var(--green); } .lg-urgent{ color:var(--gold); }
  /* ≤600px: the mosaic is small enough that the desktop's side margin wastes the width it has.
     124.2% = 1/0.805 — i.e. blow the bento up until the TILES (not the bento box, which is 20%
     wider than them) span the stage edge to edge. The stage grows to 1040/770 to match the
     taller mosaic that follows. Centring is inherited: it's in bento-relative %, so it holds. */
  @media (max-width:600px){
    .board-stage{ aspect-ratio:1040 / 770; }
    .board-bento{ width:124.2%; }
  }

  /* small footer — wordmark, tagline, copyright over a hairline */
  .footer{ display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap;
           gap:var(--sp-2) var(--sp-4); padding:var(--sp-4) var(--sp-5);
           border-top:1px solid var(--line); }
  .footer-logo{ height:20px; width:auto; }
  .footer-tag{ font-size:var(--fs-label); line-height:var(--lh-label); color:var(--ink); margin:0; }
  .footer-meta{ font-size:var(--fs-label); line-height:var(--lh-label); color:var(--dim); margin:0; }

  /* capabilities — small icon+title+text tiles on a 3-col grid,
     with a header row (heading left, quiet link right) above them */
  .caps-head{ display:flex; align-items:flex-start; justify-content:space-between;
              gap:var(--sp-4); flex-wrap:wrap; }
  .caps-link{ font-size:var(--fs-label); text-transform:uppercase; letter-spacing:.14em;
              color:var(--dim); text-decoration:none; white-space:nowrap; margin-top:var(--sp-1);
              display:inline-flex; align-items:center; gap:.45em;
              transition:color var(--dur-hover) var(--ease-out), transform var(--dur-press) var(--ease-out); }
  @media (hover:hover){ .caps-link:hover{ color:var(--ink); } }
  .caps-link:active{ transform:scale(.97); }
  .caps-link:focus-visible{ outline:var(--focus-ring); outline-offset:3px; }
  .caps-link svg{ width:12px; height:12px; }
  .caps-grid{ margin-top:var(--sp-5); display:grid; grid-template-columns:repeat(3,1fr);
              gap:var(--sp-5) clamp(var(--sp-4), 8vw, 170px); }
  /* small panel-coloured icon box, matching the placeholder radius used elsewhere */
  .cap-icon{ width:72px; height:72px; display:grid; place-items:center; color:var(--ink);
             background:var(--panel); border-radius:5px; margin:0 0 var(--sp-3); overflow:hidden; }
  .cap-icon svg{ width:22px; height:22px; display:block; }
  .cap-cym{ width:100%; height:100%; display:block; }
  .cap .card-text{ max-width:36ch; }
  @media (max-width:900px){ .caps-grid{ grid-template-columns:repeat(2,1fr); } }
  @media (max-width:520px){ .caps-grid{ grid-template-columns:1fr; } }

  /* --- copy-fit prose helpers (additive; reuse existing tokens) --- */
  .lead{ margin-top:var(--sp-3); max-width:52ch; color:var(--ink-soft);
         font-size:var(--fs-body); line-height:var(--lh-prose); }
  .sec--center .lead{ margin-inline:auto; max-width:60ch; }
  .prose{ margin-top:var(--sp-3); }
  .split-main .prose{ margin-top:var(--sp-4); }
  .prose p{ margin:0; max-width:56ch; color:var(--ink-soft);
            font-size:var(--fs-body); line-height:var(--lh-prose); }
  .sec--center .prose p{ margin-inline:auto; max-width:60ch; }
  .prose p + p{ margin-top:var(--sp-3); }
  /* the thesis punchline — a body-large statement line under the animation */
  .thesis-line{ margin-top:var(--sp-3); font-size:var(--fs-body-large); line-height:var(--lh-body-large);
                max-width:32ch; text-wrap:balance; }
  .closing-note{ margin-top:var(--sp-3); max-width:48ch; text-align:center;
                 font-size:var(--fs-label); line-height:var(--lh-prose); color:var(--dim); }

  /* ---------- contact modal (Netlify form) ---------- */
  .modal{ border:none; padding:0; background:transparent; color:var(--ink); width:min(520px,92vw);
          position:fixed; inset:0; margin:auto; height:min(600px, calc(100svh - 2rem)); overflow:auto; }
  .modal::backdrop{ background:transparent;
                    -webkit-backdrop-filter:blur(7px); backdrop-filter:blur(7px); }
  @media (prefers-reduced-motion:no-preference){
    .modal[open]{ animation:modal-in .28s cubic-bezier(.2,.7,.2,1); }
    .modal[open]::backdrop{ animation:backdrop-in .28s ease; }
    /* the exit — the entrance run backwards, a touch quicker. The JS holds the dialog OPEN and waits
       on this before it calls .close(); without it the [open] attribute drops instantly and the card
       vanishes with no fade. `forwards` holds the end frame so it can't flash back before close().
       Higher specificity than the [open] rule above, so it wins the animation property. */
    .modal[open].is-closing{ animation:modal-out .2s cubic-bezier(.4,0,.6,1) forwards; }
    .modal[open].is-closing::backdrop{ animation:backdrop-out .2s ease forwards; }
  }
  @keyframes modal-in{ from{ opacity:0; transform:translateY(10px) scale(.98); } to{ opacity:1; transform:none; } }
  @keyframes backdrop-in{ from{ opacity:0; } to{ opacity:1; } }
  @keyframes modal-out{ from{ opacity:1; transform:none; } to{ opacity:0; transform:translateY(10px) scale(.98); } }
  @keyframes backdrop-out{ from{ opacity:1; } to{ opacity:0; } }
  .cform{ position:relative; height:100%; display:flex; flex-direction:column; gap:var(--sp-3);
          background:var(--paper); border:1px solid var(--line); border-radius:5px;
          padding:var(--sp-3); box-shadow:0 40px 90px var(--shadow); }
  .modal-close{ position:absolute; top:var(--sp-2); right:var(--sp-2);
                width:34px; height:34px; display:grid; place-items:center;
                background:none; border:none; border-radius:5px; cursor:pointer;
                font-size:22px; line-height:1; color:var(--dim);
                transition:color var(--dur-hover) var(--ease-out), background var(--dur-hover) var(--ease-out), transform var(--dur-press) var(--ease-out); }
  @media (hover:hover){ .modal-close:hover{ color:var(--ink); background:color-mix(in srgb, var(--ink) 6%, transparent); } }
  .modal-close:active{ transform:scale(.9); background:color-mix(in srgb, var(--ink) 10%, transparent); }
  .modal-close:focus-visible{ outline:var(--focus-ring); outline-offset:2px; }
  .modal-title{ font-weight:400; font-size:var(--fs-body-large); line-height:var(--lh-body-large);
                letter-spacing:var(--ls-heading); margin:0; text-wrap:balance; }
  .cform .label{ margin:0; }
  .cform-head{ display:flex; flex-direction:column; gap:var(--sp-2); padding-right:var(--sp-4); }
  .cform-row{ display:flex; gap:var(--sp-2); }
  .cform-row .cfield{ flex:1; min-width:0; }
  .cfield{ display:flex; flex-direction:column; gap:var(--sp-1); }
  .cfield > span{ font-size:var(--fs-label); text-transform:uppercase; letter-spacing:var(--ls-label); color:var(--dim); }
  .cfield input, .cfield textarea{ font-family:inherit; font-size:var(--fs-body); line-height:1.3; color:var(--ink);
        background:var(--panel); border:none; border-radius:5px; padding:var(--sp-2); width:100%;
        transition:background var(--dur-hover) var(--ease-out), box-shadow var(--dur-hover) var(--ease-out); }
  .cfield textarea{ resize:vertical; min-height:96px; }
  /* the field lightens toward paper under the pointer, so it responds before you click into it */
  @media (hover:hover){ .cfield input:hover, .cfield textarea:hover{ background:color-mix(in srgb, var(--panel) 55%, var(--paper)); } }
  /* focus keeps the paper fill but adds a green ring — the old rule dropped the outline and left
     keyboard users with only a faint background shift. A box-shadow ring composes with the fill. */
  .cfield input:focus, .cfield textarea:focus{ outline:none; background:var(--paper);
        box-shadow:0 0 0 2px color-mix(in srgb, var(--green) 55%, transparent); }
  .cform-submit{ border:none; cursor:pointer; flex:none; }
  .cform-foot{ display:flex; align-items:center; gap:var(--sp-3); }
  .cfield--grow{ flex:1; }
  .cfield--grow textarea{ flex:1; min-height:0; resize:none; }
  .cfield input::placeholder, .cfield textarea::placeholder{ font-size:var(--fs-label); font-weight:400;
        text-transform:uppercase; letter-spacing:var(--ls-label); line-height:var(--lh-label);
        color:var(--ink-soft); opacity:1; }
  .cform-status{ margin:0; min-height:1.1em; font-size:var(--fs-label); line-height:1.3; color:var(--dim); }
  @media (max-width:520px){ .cform-row{ flex-direction:column; gap:var(--sp-3); } }

  /* ---------- scheme toggle ----------
     TEMPORARY — the harness for comparing schemes. Delete this block, the scheme blocks at the
     top of the sheet, and the matching block in main.js to remove it. The markup is injected
     (main.js), so there is nothing to remove from index.html. */
  .scheme-bar{ position:fixed; z-index:60; left:50%; bottom:var(--sp-2); transform:translateX(-50%); }
  /* the closed control. list-style:none + ::-webkit-details-marker kills the disclosure triangle
     in every engine; the caret below is ours, so it can point up. */
  .scheme-current{ display:inline-flex; align-items:center; gap:.6em; list-style:none; cursor:pointer;
    font-size:var(--fs-label); line-height:1; text-transform:uppercase; letter-spacing:var(--ls-label);
    color:var(--ink); padding:var(--sp-1) var(--sp-2); border-radius:5px;
    background:color-mix(in srgb, var(--paper) 82%, transparent);
    -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
    box-shadow:0 8px 30px var(--shadow);
    transition:background .2s ease; }
  .scheme-current::-webkit-details-marker{ display:none; }
  .scheme-current:hover{ background:color-mix(in srgb, var(--paper) 94%, transparent); }
  .scheme-caret{ width:0; height:0; margin-left:.2em;
    border-left:4px solid transparent; border-right:4px solid transparent;
    border-bottom:5px solid var(--dim); transition:transform .2s ease; }
  .scheme-bar[open] .scheme-caret{ transform:rotate(180deg); }
  /* Opens UPWARD — the control is already at the bottom of the viewport. bottom:100% puts the
     menu's foot on the summary's head; a top-anchored menu would run off the page.
     width:max-content, NOT right:0 — the rows are wider than the closed control (they carry a
     note the summary doesn't), and pinning both edges to it left them overflowing the panel.
     min-width keeps the menu from ever being narrower than the control it belongs to. */
  .scheme-menu{ position:absolute; bottom:100%; left:0; width:max-content; min-width:100%;
    margin-bottom:var(--sp-1);
    display:flex; flex-direction:column; gap:1px; padding:var(--sp-1);
    border-radius:5px; border:1px solid var(--line);
    background:color-mix(in srgb, var(--paper) 92%, transparent);
    -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
    box-shadow:0 12px 40px var(--shadow);
    /* the list outgrew the viewport once it passed a dozen or so rows — cap it and let it scroll
       rather than run off the top of the screen, which is where a bottom-anchored menu goes */
    max-height:calc(100vh - 6rem); overflow-y:auto; overscroll-behavior:contain; }
  /* Grouped by paper temperature (see main.js). The rule between groups is what makes the list
     scannable — without it the headings float and the sets run together. */
  .scheme-group{ display:flex; flex-direction:column; gap:1px; }
  .scheme-group + .scheme-group{ margin-top:var(--sp-2); padding-top:var(--sp-2);
    border-top:1px solid var(--line); }
  .scheme-group-label{ margin:0; padding:var(--sp-1) var(--sp-2);
    font-size:calc(var(--fs-label) * 0.85); line-height:1;
    text-transform:uppercase; letter-spacing:var(--ls-label);
    color:color-mix(in srgb, var(--ink) 38%, transparent); }
  .scheme-opt{ display:flex; align-items:center; gap:.6em; width:100%;
    font-family:inherit; font-size:var(--fs-label); font-weight:400; line-height:1;
    text-transform:uppercase; letter-spacing:var(--ls-label); text-align:left;
    color:var(--dim); background:none; border:0; border-radius:4px; cursor:pointer;
    padding:var(--sp-1) var(--sp-2); white-space:nowrap;
    transition:color .2s ease, background .2s ease; }
  .scheme-opt:hover{ color:var(--ink); background:color-mix(in srgb, var(--ink) 6%, transparent); }
  .scheme-opt.is-active{ color:var(--ink); background:color-mix(in srgb, var(--ink) 12%, transparent); }
  /* pushed right, and the first thing to go when the menu is narrow */
  .scheme-note{ margin-left:auto; padding-left:var(--sp-3); color:var(--ink-soft); text-transform:none;
    letter-spacing:0; font-size:calc(var(--fs-label) * 0.92); }
  /* Establishes the DEFAULT palette for the swatch inside it, which then overrides with its own
     scheme. That nesting is the point: Blue Signal sets one token and inherits the rest, so a
     swatch resolving it needs the same base underneath — exactly as it works on :root. Without
     it, Blue Signal's swatch would borrow the ACTIVE scheme's green and gold. */
  .scheme-base{ display:contents; }
  .scheme-dots{ display:inline-flex; gap:2px; padding:3px; flex:none;
    border-radius:99px; background:var(--paper); border:1px solid var(--line); }
  .scheme-dots i{ width:7px; height:7px; border-radius:50%; display:block; }
  .scheme-dots i:nth-child(1){ background:var(--accent-1); }
  .scheme-dots i:nth-child(2){ background:var(--accent-2); }
  .scheme-dots i:nth-child(3){ background:var(--accent-3); }
  @media (max-width:600px){ .scheme-note{ display:none; } }
