/**
 * theme/law.css — GROUND + STRUCTURE + THE FULL RAMP, hand-authored, not
 * derived from a ColorTheme document.
 *
 * Owner decision, 2026-09-25: `stage`'s picked near-black ground (#080C12)
 * and its blue/orange accent pair are KILLED. Superseded by
 * `docs/designs/site-home-the-toy.md` (written later, survived two
 * adversarial review rounds), which mandates, verbatim:
 *
 *   "Pitch black ground. Not near-black."
 *   "White is structure, colour is content... Colour means a note. Always —
 *   never decoration... focus rings are white, like every other structural
 *   line."
 *
 * See theme/design-direction.md for the full record of the reversal —
 * `stage` is kept there as a path taken and rejected, not erased.
 *
 * 🔴 REVISION, 2026-09-25 (same day, second pass): the first version of this
 * file overrode only `--bg`/`--structure`/`--border`/`--border-bright` and
 * was WRONG BY OMISSION — verified by rendering the built site in a browser
 * and pulling `getComputedStyle` off the live page, not by reading source.
 * `derive.mjs` generates the ENTIRE token ramp (every text tier, every
 * surface, every semantic-adjacent accent) from `stage`'s accent hue, so
 * `--text-primary` (body copy, every page), `--text-secondary` (nav links),
 * `--link`, all four surface tokens and `--scrim` were still rendering
 * `stage`'s light blue — invisible in `tokens.css`'s source because the
 * override lives in a SEPARATE file layered on top, but glaring in a
 * browser. This revision covers the full ramp. If you are about to override
 * one more stray token here, that is a sign the FIRST fix was incomplete
 * too, not that this one is — check by rendering, not by reading.
 *
 * # Why a separate stylesheet, not a hand-authored ColorTheme
 *
 * `*.ColorTheme.json` are vendored byte-identical from dbo-contracts and
 * `npm run check:themes` runs orphan detection (PLAN.md §4) — a
 * hand-authored document fails that gate outright. No vendored theme is a
 * candidate either: checked all 14 (`node -e` over `theme/*.ColorTheme.json`,
 * 2026-09-25) — none carries a true `#000000` ground (closest is `matrix` at
 * `#050805`), and every one of the 14 carries a two-tone accent+secondary
 * pair, which the owner's ruling now forbids as decorative. So this file
 * loads AFTER tokens.css (`theme/build-tokens.mjs`'s `renderHead` emits its
 * `<link>` second, unconditionally, in every mode — order is load-bearing:
 * two `:root {}` blocks of equal specificity resolve to whichever is later
 * in the cascade) and overrides the tokens the site law is absolute about.
 * `src/css/base.css`'s own `:where(:root) {…}` fallback block cannot ever
 * fight either stylesheet — `:where()` has zero specificity by definition,
 * so a real `:root {}` here always wins regardless of file order.
 *
 * # The method: luminance-preserving desaturation, not eyeballed greys
 *
 * Every text/surface tier below that used to carry `stage`'s blue hue is
 * replaced with a NEUTRAL grey computed to have the *same WCAG relative
 * luminance* as the colour it replaces (sRGB → linear via the standard
 * piecewise transform, `L = 0.2126R + 0.7152G + 0.0722B`, then solved for
 * the grey `g` where `R=G=B` gives that same `L`). This is not "close
 * enough" — WCAG contrast ratio is `(L1+0.05)/(L2+0.05)` and depends ONLY on
 * relative luminance, never on hue, so a luminance-preserving grey conversion
 * preserves every contrast ratio the original colour had against any
 * background, to within ~0.5% (sRGB 0–255 integer quantization is the only
 * source of drift). Verified numerically for every token converted this way
 * (script run 2026-09-25, not committed — the values below are its output):
 * every resulting pair is equal to or looser than its pre-conversion ratio,
 * because `#000` has strictly more contrast headroom than `stage`'s
 * `#080C12`, which is what `derive.mjs` originally floored against. The one
 * exception, called out where it appears below, is `--accent-dim`, whose
 * *designed* band (not a WCAG floor, a deliberate upper clamp) got a manual
 * one-step correction rather than the raw luminance-preserving value.
 *
 * # THE WARM-NEUTRAL PASS, 2026-09-29 — read this BEFORE you read the ramp
 *   as a law violation, because that is exactly what it looks like
 *
 * Owner, twice, five days apart: *"we have too many colors that clash - less
 * is bess here"* — and then, after the band tints had been retired and the
 * four crew illustrations re-saturated to match the reference artwork —
 * *"we still have to many washed colors"*.
 *
 * The second complaint is the interesting one, because by then the art was
 * measurably fine (all four illustrations sit at .38–.45 warm-band saturation
 * against the owner's own hand-authored reference at .450). The washed reading
 * was not in the pictures. It was in THIS FILE, and it was two things:
 *
 *   1. `--surface` was COOLER than the ground it sat on. #f2f0ea, hue 45°,
 *      oklch chroma .0083, against `--bg` #FAECD7, hue 78°, chroma .0316.
 *      Every panel on the site was a cool-neutral card floating on warm
 *      cream. `--surface-high`/`--surface-lift`/`--input` were worse: chroma
 *      .0000, literal dead grey.
 *   2. The ENTIRE text ramp was pure neutral grey — `--text-primary`,
 *      `--text-secondary`, `--ink` — all chroma .0000, printed on a ground
 *      whose hue is 78°. `--text-secondary` alone is the most-used ink on the
 *      site. Only `--text-muted` carried warmth, and only by accident (it was
 *      the fix for an AA failure; see its note).
 *
 * ## Why this is NOT a breach of "colour means a note, never decoration"
 *
 * ⚠️ THE NEXT AGENT WILL READ A WARMED RAMP AS A VIOLATION OF THIS FILE'S
 * OWN LAW. It is the opposite, and the argument is short:
 *
 * A neutral is not a hue-free colour. It is the colour that DISAPPEARS
 * against its ground. On a white ground that is #888888; on a cream ground at
 * hue 78° it is a grey that carries hue 78°. When you print a chroma-.0000
 * grey on a chroma-.0316 warm ground, you have not painted "no colour" — you
 * have painted a COLD one, because the eye judges it relative to the paper,
 * and relative to this paper a neutral grey is blue. That is a second colour
 * on the page, uninvited, doing nothing, and it is the thing the owner has
 * been looking at and calling washed.
 *
 * So this pass REMOVES a colour; it does not add one. The test that settles
 * it: after the change, is there any tone on this site that a reader could
 * name? No — the ink reads as black, the panels read as paper. Before the
 * change there was one (a cold grey), and it was on every page. "White is
 * structure, colour is content" survives intact: the structural tones now
 * belong to the structure's own paper instead of quietly contradicting it.
 *
 * The line that must NOT be crossed is stated where the ramp is declared and
 * is worth repeating here: the ink is at oklch chroma .0156–.0169 against a
 * ground at .0316 — about half. It is legible only as the ABSENCE of the old
 * cold cast. Ink that visibly reads as BROWN is a worse failure than ink that
 * reads as grey, because brown is a colour a reader can name and grey is at
 * least a non-statement. If a later pass is tempted to push the warmth until
 * you can see it: you are not supposed to be able to see it.
 *
 * ## The method — same maths as the desaturation pass above, run backwards
 *
 * WCAG contrast is `(L1+.05)/(L2+.05)` over relative luminance alone and is
 * blind to hue, which is what made the 2026-09-25 desaturation safe. The same
 * property makes RE-saturation safe, in the same way and for the same reason:
 *
 *   - fix OKLCH hue at 78° — `--bg`'s own, measured, not guessed;
 *   - fix OKLCH chroma at a constant per family — ~.014–.017 for the text
 *     ramp (calibrated to `--text-muted`'s existing, already-shipping .0139),
 *     ~.0275 for the surface ladder (just under the ground's .0316, because a
 *     panel must never out-saturate the page it sits on);
 *   - solve OKLCH lightness for the EXACT WCAG relative luminance of the grey
 *     being replaced;
 *   - then search the integer sRGB neighbourhood for the triple closest to
 *     all three targets at once, rejecting anything whose luminance drifts
 *     more than 0.4% or whose hue drifts more than 8°.
 *
 * Worst luminance drift shipped: 0.11% (`--surface-lift`). Worst contrast
 * drift shipped: 0.004 (`--text-primary`, 8.369 → 8.365). Both are 8-bit sRGB
 * quantization — the floor of what this method can reach, not slack in it.
 * Not one ratio on the site moved in the first decimal place.
 *
 * ⚠️ VERIFIED IN THE BROWSER, NOT FROM THE ARITHMETIC. Every ratio above and
 * below was re-measured with `getComputedStyle` on the BUILT page across `/`,
 * `/support` and `/privacy`, compositing the full ancestor opacity chain
 * (which matters: /support's form sits inside a `fieldset[disabled]` at
 * `opacity: .55`, and `.toy-description` renders at `.78`). Source-reading
 * would have missed both. Same discipline as the 2026-09-25 revision note at
 * the top of this file: check by rendering, not by reading.
 *
 * ## What this pass deliberately left alone
 *
 *   - **The dark ramp** (`.toy-stage`, `.band.void`) stays neutral. Its
 *     ground is `#000000`, which has no hue to agree with; warming there
 *     WOULD be decoration. Full argument at that selector.
 *   - **`--accent-dim`** stays `#a0a0a0`. It is dead (zero consumers on all
 *     four rendered pages, confirmed in the browser) and it is not a rung in
 *     a ladder. The solved warm value is recorded at its declaration so
 *     nobody has to redo the work if it ever gains a consumer.
 *   - **The surface ladder's missing LIGHTNESS step.** Every light surface
 *     token is within 1.014–1.074:1 of the ground, so panels were only ever
 *     visible by their wrong hue — which means warming them correctly makes
 *     them vanish. Every live consumer has a `1px solid var(--border)`
 *     perimeter at 16.2:1 and still reads as a box, so nothing regressed; but
 *     `.report-kind-face:hover` is now a hover you can barely see. Fixing
 *     that is a CONTRAST change, out of scope for a pass whose whole promise
 *     was that no ratio moves. Flagged, not silently absorbed.
 *
 * # Accent, secondary, link, selection: "harmless if referenced," not tuned
 *
 * `--accent`/`--accent-bright`/`--secondary`/`--secondary-bright`/`--link`/
 * `--text-selection`/`--accent-on-scrim`/`--secondary-on-scrim` do not get
 * the luminance-preserving treatment — they collapse to `--structure`
 * (white) outright. These were `stage`'s decorative accent hue by
 * definition; "convert to an equivalent grey" would still be publishing a
 * bespoke tint as site chrome, just a desaturated one, which is not what
 * "colour is only ever a note, never decoration" asks for. White is the
 * only value that is unambiguously *never* decoration on this site — it is
 * what `--structure` already means. `--accent-dim` is the one exception: its
 * job ("recessive... never louder than body text") is inherently about being
 * DIMMER than the text ramp, which white cannot represent once hue is gone,
 * so it keeps a real (luminance-derived, band-clamped) grey instead of
 * aliasing to white. `--ink-on-accent`/`--ink-on-secondary` follow from
 * `--accent`/`--secondary` becoming white: ink on a white fill is `--bg`
 * (black), not the near-black `stage` literal they used to hold.
 * `--text-selection` currently has zero consumers in `src/css/` (checked
 * 2026-09-25 — no `::selection` rule exists yet), so this is a forward
 * guarantee, not a rendered fix: if a `::selection` rule is added later, it
 * inherits white rather than `stage`'s blue with no further action here.
 *
 * # Semantics — NOT touched, and why (see also design-direction.md)
 *
 * `--success(-bright)`, `--warning`, `--error(-bright)`, `--gold` are left
 * exactly as `derive.mjs` computed them from `stage` — still colour, still
 * hue-bearing, deliberately. Reasoning: these are FUNCTIONAL state signals
 * (pass/fail/in-flight), not decoration, and the one place they could
 * realistically render — the support/issue-report form's validation states
 * — is not "the toy" the app's zero-decorative-colour law describes; it is
 * an ordinary web form, where WCAG 1.4.1 already requires the state to be
 * conveyed by more than colour alone (text/icon), so the colour is
 * reinforcement, not the message. Recolouring them to grey would remove the
 * conventional red/green/amber vocabulary a visitor already reads correctly,
 * for a purity gain that the owner's own focus-ring exception (a
 * *structural*, not decorative, colour use also carved out of the same
 * absolute-sounding rule) suggests is not actually intended to be
 * exceptionless. **This is a proposal, not a closed decision** — if the
 * owner wants strict zero-colour-anywhere, these six tokens are the
 * remaining candidates, flagged here rather than silently left for a later
 * session to rediscover (same discipline as the drift note this file's
 * predecessor left, which turned out to matter). Only `--ink-on-success` and
 * `--ink-on-warning` changed, and only because their stored value was a
 * literal copy of `stage`'s `#080C12` ground — replaced with `var(--bg)` as
 * a pure cleanup (same near-black intent, now the real constant instead of
 * a stray stage hex); the success/warning FILL colours themselves are
 * unchanged. `--ink-on-error` (`#F0F0F0`) is untouched too — it was never a
 * stage-ground literal, it is error's own fill-contrast ink, unrelated to
 * this cleanup.
 *
 * Regenerate nothing — this file is hand-authored, not generated. Do not run
 * `npm run tokens` expecting it to change; that command copies this file
 * into `theme/public/` and links it from `theme/head.html` (see
 * `build-tokens.mjs`'s `LAW_CSS` constant) but does not write its contents.
 */
:root {
  /* ── Ground + structure (FIFTH pass, 2026-09-26 — owner: "the black page
       background does not feel kid-friendly"). ──────────────────────────

     🔴 `--bg` FLIPS: the PAGE ground moves from pitch black to a soft warm
     white (~#FAFAF8). The 2026-09-25 reasoning that pinned it to
     `#000000` — `approved.json`'s `measurements.reel_corner_pixel`,
     "artifact bands MUST be true black or the square shows" — governed
     RASTER SCREENSHOTS (`assets/screens/`), which no longer exist; the
     `#world` scene is SVG now, not a captured raster with corner pixels to
     protect. That measurement never bound the page ground and does not
     survive as a reason to keep it black. (`bands.css:117` still cites the
     same now-dead constraint for the SAME reason — flagged in this pass's
     handoff report; that file is outside this lease.)

     TRUE BLACK SURVIVES, as a deliberate OBJECT rather than the default:
     the hero (`.toy`, `docs/designs/site-home-the-toy.md`: "pitch black,
     not near-black") and the `#world` scene card (`.band.void`,
     white-on-black scene drawings — inverting them would make the scenes
     look unlike the app they depict, an explicit owner call, not this
     agent's aesthetic choice). `--void` is no longer `--bg`'s alias — it
     is its OWN literal now, because the two facts have come apart: the
     page is light, `--void` is still exactly `#000000`. */
  /* 🔴 `#world` RETIRED, 2026-09-29 (later the same day) — the second black
     object named just above no longer exists as its own section. The same
     white-on-black drawing now runs inside `no-wrong-way`'s wall panel
     (`layouts/scene-band-live.yml:208-209`, `.wall-surface` in `src/css/
     wall.css`), which reads `var(--void)` directly rather than through
     `.band.void` — `no-wrong-way` itself is an ordinary cream band. The
     argument above (why `--void` must stay exactly `#000000`) is unchanged;
     only which markup carries it moved. Full account in the "dark-surface
     override" doc block further down this file. */
  /* 🔴 CREAM, not near-white — OWNER CORRECTION, 2026-09-29: "have the main
     site on cream here — like a kids drawing on paper (but more refined) and
     more friendly and fun." `#FAFAF8` was a cool near-white that read as
     clinical next to the illustration set; `#F6EFE1` is the warm paper the
     crew scenes are drawn on (site/_proto/crew/), so the artwork sits ON the
     page rather than in a lighter box floating over it.
     🔴 VALUE REFINED 2026-09-29, same day: #F6EFE1 -> #FAECD7. Not a taste
     tweak — MEASURED. The crew illustrations (assets/artwork/) are drawn on
     their own cream, and sampling a clean 40x40 corner patch of each of the
     four gave #f9edd9 / #faebd1 / #faecd7 / #fbecd8. Against #F6EFE1 every
     scene showed as a visible rectangle on the page. #FAECD7 is that cluster's
     centre — every scene now sits within 2/255 per channel of the page, which
     is below the threshold anything reads as an edge. Matching the page to the
     art rather than recolouring four illustrations keeps the artwork
     replaceable by file swap, which is this repo's own rule.
     Contrast checked, not eyeballed: `--ink` #111111 on #FAECD7 = 16.3:1,
     far past AA's 4.5:1, so nothing that was passing stops passing.
     `--void` is untouched and stays exactly #000000 — see the note above.
     The play surface is STILL black (owner call, same date: cream page,
     black play surface only), so the app's own look survives the flip and
     the toy becomes the one dark thing on a warm page. */
  --bg: #FAECD7;
  --void: #000000;

  /* The one explicit "this is structure, not decoration" token — UNCHANGED,
     stays exactly `#ffffff`, unconditionally, in every scope. Two real
     consumers depend on that specifically, checked before deciding this,
     not assumed:
       1. `components.css`'s nav (`.site-header { color: var(--structure);
          mix-blend-mode: difference }`) — `difference(white, X) = 255-X`
          per channel is a full tonal invert, which is what makes the nav
          read legibly over EVERY band tone automatically. `difference(ink,
          X)` for a dark `X` (`--ink` #111 over `--void` #000) computes to
          ~(17,17,17) — a NEAR-BLACK result on a near-black band: invisible.
          Scoping `--structure` itself would silently break the one thing
          it was built to make automatic.
       2. `toy.css`'s own focus rings (`.toy-note:focus-visible` etc.) and
          `.toy-scene { color: var(--structure) }` — always rendered INSIDE
          `.toy`, always meant to be white, never meant to adapt.
     Because of (1), `--structure` cannot be the scoped "adapts to the
     surface" token this pass otherwise needs — see `--text-bright` below,
     which takes on that job instead (aliased FROM, not equal to,
     `--structure`, in the dark-surface override at the bottom of this
     block). */
  --structure: #ffffff;

  /* theme/README.md documents `--border-bright` as "the widget perimeter
     AND the focus ring." Previously aliased to `--structure` (always
     white) — WRONG now: white borders/underlines/focus rings on the new
     light page ground are invisible, and `--structure` itself must stay
     pinned to white for the nav (immediately above), so it cannot supply
     the light-ground value either. Re-aliased to `--text-bright` instead —
     the brightest/highest-emphasis rung of the text ramp below, which
     THIS pass makes scope-aware for exactly this reason: dark ink by
     default, white again inside `.toy`/`.band.void`. `--border`/
     `--border-bright` and `--text-bright` were always the same underlying
     idea (the maximum-contrast structural tone for whatever surface
     they're drawn on) — this makes that explicit instead of coincidental. */
  --border: var(--text-bright);
  --border-bright: var(--text-bright);

  /* ── Surfaces (THIRD pass, 2026-09-26): light elevation ladder, one step
       off the new `--bg`. Only `--surface` has a real consumer today
       (`.skip-link`/`.showcase-frame`/`.band-alt`, all `src/css/`,
       checked before writing these, not assumed) — the rest are the same
       forward-guarantee this file already keeps for other unconsumed
       tokens (`--text-selection` etc., see below), restored to their
       dark-theme values inside the `.toy`/`.band.void` override at the
       bottom of this block rather than deleted, so nothing here regresses
       IF a dark-surface panel ever needs one. Not WCAG-rigor-tuned beyond
       `--surface` itself (nothing else renders them) — see that token's
       own contrast note.

     🔴 WARMED ONTO THE PAPER, 2026-09-29 (SIXTH pass — owner, twice: "we
     have too many colors that clash - less is bess here", then, after the
     band tints were retired and the artwork re-saturated, "we still have to
     many washed colors"). See "THE WARM-NEUTRAL PASS" in this file's header
     for the method, the full before/after table, and why this is a colour
     being REMOVED rather than one added.

     THE BUG, stated plainly: this whole ladder was a COOL neutral sitting on
     a WARM ground. `--surface` #f2f0ea measured hue 45°, oklch chroma .0083;
     `--surface-high`/`--surface-lift`/`--input` measured chroma .0000 — dead
     grey. `--bg` is #FAECD7, hue 78°, chroma .0316. A panel one step off the
     page is supposed to read as the same paper lit differently; these read as
     a different, colder paper laid on top. The fills are all within 1.01–1.07:1
     of the ground, so the step was never carrying LIGHTNESS information at
     all — the only thing that made a panel visible was its wrong hue. That is
     the definition of an accidental colour.

     EVERY VALUE BELOW IS THE SAME WCAG RELATIVE LUMINANCE AS THE ONE IT
     REPLACES (worst drift on this ladder: 0.11%, `--surface-lift`). So every
     text-on-surface ratio is preserved: ink on `--surface` 16.570 → 16.584,
     `--text-primary` 8.547 → 8.550, `--text-secondary` 6.243 → 6.248,
     `--text-primary` on `--input` 8.244 → 8.248. Nothing that passed stops
     passing, and nothing that failed starts passing either — this pass moved
     hue, never value.

     HOW THE VALUES WERE CHOSEN: fix OKLCH hue at 78° (the ground's own) and
     OKLCH chroma at ~.0275 (a shade UNDER the ground's .0316 — a panel must
     never out-saturate the page it sits on), then solve OKLCH lightness for
     the target WCAG luminance and search the integer sRGB neighbourhood for
     the triple closest to all three at once. Not eyeballed; not "nudge the
     blue channel down until it looks warm."

     ⚠️ WHAT THIS PASS DELIBERATELY DID NOT FIX, so the next reader does not
     mistake it for an oversight: the ladder has almost no luminance
     separation from the ground (1.014–1.074:1) and `--surface-high` sits
     1.037:1 off `--surface`. `.report-kind-face:hover` is therefore a hover
     state you can barely see, and `.report-standdown`'s fill does nothing —
     issue-report.css already says so in its own words ("no fill at all").
     Giving the ladder a real step is a CONTRAST change (it would move
     text-on-surface ratios), which this pass was scoped out of. It is the
     right next question for whoever owns that decision. */
  --surface: #faefdc;        /* was #f2f0ea (hue 45°, chroma .0083 — cool).
                                 Y 0.87138 → 0.87224 (+0.10%), oklch chroma
                                 .0083 → .0278 at hue 81.5°. Against `--bg`
                                 1.021:1 → 1.022:1 — i.e. still, correctly,
                                 no lightness step; it is the same paper, a
                                 hair lighter, instead of a colder one. */
  --surface-high: #f6ebd8;   /* was #ececec, chroma .0000. Y 0.83880 → 0.83967 */
  --surface-lift: #efe5d2;   /* was #e6e6e6, chroma .0000. Y 0.79130 → 0.79042.
                                 DEAD — zero consumers on any rendered page
                                 (scanned all four in the browser, 2026-09-29).
                                 Warmed anyway, and only because it is a RUNG:
                                 a ladder with one cool step left in it is how
                                 the cool cast gets reintroduced the first time
                                 somebody reaches for the next token down. */
  --terminal: #fef3e0;       /* was #f5f4f2. Y 0.90525 → 0.90554. Also dead;
                                 same rung argument as --surface-lift. */
  --input: var(--surface-high);  /* ALIASED, 2026-09-29, not restated. The old
                                 comment here claimed it "tracks surface-high"
                                 while carrying its own literal copy of the hex —
                                 an invitation to drift that this pass would
                                 have had to update in two places. `var()` in a
                                 custom property re-resolves per scope, so the
                                 dark-surface block's own `--surface-high` still
                                 drives it there with no further wiring. */
  --highlight: #eadccb;      /* was #e0ded8. Y 0.73048 → 0.72991. Dead; rung. */
  /* `scrim`'s own derivation rule is "the theme's own darkest token" — that
     is no longer `--bg` (light now); repointed to `--void` (the fixed
     black literal, above) so a scrim — conceptually a wash over MEDIA,
     always dark, independent of whatever the page ground happens to be —
     stays dark regardless of the ground flip. Dead code either way (no
     photographic content on this site, PLAN.md), so this is a forward
     guarantee, not a rendered fix. */
  --scrim: var(--void);

  /* ── Accent/secondary/link/selection: collapsed to the ADAPTIVE
       structural tone (`--text-bright`, below — NOT `--structure`, which
       must stay pinned to white for the nav; see the header above), never
       decorative. Same "harmless if referenced" policy as before, just
       re-pointed at a token that actually adapts to the surface it renders
       on, now that there IS a light default surface to get wrong. See
       header for why no grey survives here except --accent-dim. `--accent`
       itself is overridden again further down (the band-palette section,
       `var(--theme-flute)`) — this baseline exists so the token is never
       literally undefined/decorative if that override is ever removed. ── */
  --accent: var(--text-bright);
  --accent-bright: var(--text-bright);
  /* Designed band was 1.8–3.2:1 against the surface, "never louder than
     body text" — a RECESSIVE tone, which is inherently about being dimmer
     than the primary text tier, so (like the dark-theme value it replaces)
     it keeps a real grey rather than aliasing anything. Solved for ~2.5:1
     against the new light `--bg` (mid-band): `--accent-dim: #a0a0a0`
     measures 2.5:1 exactly. The dark-theme value (`#5d5d5d`, 3.19:1
     against black) is restored inside the `.toy`/`.band.void` override
     below — same recessive JOB, different surface, different number.

     🔴 LEFT COOL ON PURPOSE by the warm-neutral pass, 2026-09-29, and this
     is the one token in the file that pass decided AGAINST touching. Three
     reasons, in order:
       1. It is DEAD. Scanned all four rendered pages in the browser (not
          grepped — `getComputedStyle` over every element, matching computed
          backgrounds and colours against the token's own value): zero
          consumers on `/`, `/support`, `/privacy`, `/accessibility`. Nothing
          on this site is washed out BY this token, because nothing paints it.
       2. It is not a rung on a ladder. `--surface-lift`/`--highlight`/
          `--terminal` are equally dead and WERE warmed, because they sit in a
          sequence whose live members moved and a half-warmed sequence is a
          trap. `--accent-dim` stands alone; there is no neighbour it can
          disagree with.
       3. Its spec is a RATIO BAND ("1.8–3.2:1 against the surface, never
          louder than body text"), not a hue — and its dark-surface twin
          (`#5d5d5d`, below) is correctly neutral, because `--void` is pure
          black and has no hue for a neutral to agree with. Keeping both
          halves neutral keeps the token internally consistent.
     ⚠️ IF IT EVER ACQUIRES A LIGHT-GROUND CONSUMER it must be warmed with
     everything else, and the value is already solved so nobody has to redo
     the arithmetic: **#a69f97** — Y 0.35153 → 0.35138 (−0.04%), oklch chroma
     .0000 → .0141 at hue 71.3°, 2.247:1 → 2.248:1 against `--bg`. Same
     band, same ratio, warm. */
  --accent-dim: #a0a0a0;
  --secondary: var(--text-bright);
  --secondary-bright: var(--text-bright);
  --link: var(--text-bright);
  /* `--text-selection` (the ::selection ink — still zero real consumers,
     checked again this pass) tracks the same adaptive tone: whatever the
     surface's own "brightest structural" colour is, not a fixed white that
     would be invisible if a ::selection rule ever lands on the light
     ground. */
  --text-selection: var(--text-bright);

  /* ── Text tiers (THIRD pass, 2026-09-26): dark ink on light, not
       luminance-preserving conversions from `stage`'s blue anymore — that
       method solved "keep the same contrast on a DARK ground," and the
       ground direction has now inverted. Re-solved instead for the SAME
       accessibility CONTRACT each tier always carried (a target contrast
       ratio against its own ground), against the new `#FAFAF8`, via the
       identical WCAG relative-luminance formula this file has used
       throughout — not eyeballed, not copied from anywhere.

       ⚠️ THE TABLE BELOW WAS SOLVED AGAINST `#FAFAF8` AND THE GROUND MOVED
       to the warm cream `#FAECD7` LATER IN THE SAME PASS. Only `--text-muted`
       was close enough to the floor for that to matter, and it did — it
       failed at 4.07:1 for three days; see its own note below. All four are
       re-stated here against the ground the site ACTUALLY uses, measured in
       the browser on the built page 2026-09-29:
                                              on #FAFAF8   on #FAECD7 (live)
         text-bright   = --ink (#111111)        18.07:1        16.22:1
         text-primary  = #444444                 9.32:1         8.37:1
         text-secondary= #585858                 6.81:1         6.11:1
         text-muted    = #6B665E (was #737373)   5.45:1         4.89:1
       `--text-bright` reuses `--ink` rather than a fifth literal — one
       fewer near-black hex to keep in sync, and it is already the
       established "text on a light ground" anchor for this site (it was the
       band-card ink until the cards were retired, 2026-09-29). This is also
       the token `--border`/`--border-bright`/`--accent`/`--link`/etc. above
       now alias — see the dark-surface override at the end of this block
       for the light-on-dark half.

       🔴 WARMED, 2026-09-29 (SIXTH pass). THE WHOLE RAMP ABOVE WAS PURE
       NEUTRAL GREY — oklch chroma .0000, every rung — printed on a ground
       whose own hue is 78°. `--text-secondary` alone renders on roughly
       fourteen call sites (footer links, colophon, legal intro, form labels,
       report intro, FAQ intro, step detail, feature-card body), so the most
       common ink on the site was a cold grey on warm paper. That reads as
       "washed," which is exactly the word the owner used, twice.

       THE ONLY TIER THAT WAS ALREADY RIGHT IS `--text-muted`, AND IT GOT
       THERE BY ACCIDENT — #6B665E was picked three days ago as the fix for
       a 4.07:1 AA failure, and the note below says so outright ("It is also
       WARM, which #737373 was not — a cold grey on a warm cream ground was
       reading slightly dirty as well as failing"). It measures oklch chroma
       .0139 at hue 79.7°: the ground's own hue, at a bit under half the
       ground's chroma. That accident is now the RULE for the whole ramp, and
       `--text-muted` is left byte-for-byte unchanged as its own reference
       specimen — the other three rungs were solved TO it, not away from it.

       THE METHOD (see the header section for why this is not decoration):
       fix OKLCH hue at 78°, fix OKLCH chroma at ~.014–.017, solve OKLCH
       lightness for the EXACT WCAG relative luminance of the grey being
       replaced, then search the integer sRGB neighbourhood for the triple
       closest to all three at once. Contrast ratio is `(L1+.05)/(L2+.05)`
       and depends only on relative luminance — never on hue — so a
       luminance-preserving hue shift cannot move a single ratio on the site.
       Verified rather than asserted: measured with `getComputedStyle` on the
       BUILT page, composited alpha included, before and after.

                                    before      after      on #FAECD7
         --ink / --text-bright    #111111  →  #161009   16.225 → 16.224
         --text-primary           #444444  →  #4a433b    8.369 →  8.365
         --text-secondary         #585858  →  #5e574f    6.113 →  6.112
         --text-muted             #6B665E  →  (unchanged) 4.894 →  4.894
       Worst luminance drift across the four: 0.093% (`--text-primary`).
       Worst ratio drift: 0.004. Both are sRGB integer quantization, which is
       the floor this method can reach, not slack in the method.

       ⚠️ WHY THE CHROMA IS THIS LOW AND MUST STAY THIS LOW. The target is
       ink that looks BLACK ON WARM PAPER, not ink that looks brown. A ramp
       that visibly reads as brown is a worse failure than one that reads as
       grey, because grey is at least a non-statement. The ground is at
       chroma .0316; the ink sits at .0156–.0169, roughly half, and at these
       levels the warmth is only legible as an ABSENCE of the cold cast — put
       the old and the new side by side and you can see it; look at the new
       one alone and you see black type. If a future pass is tempted to push
       it further "so you can actually see the warmth": you are not supposed
       to be able to see it. That is the brief.

       ⚠️ The hues land at 70.8–79.7° rather than all at 78°. That is sRGB
       integer quantization at these tiny chromas, not a choice — at
       `--ink`'s luminance the 8-bit grid offers exactly two rungs (chroma
       .0000 and chroma .0169) and nothing in between. An 8° hue spread at
       chroma <.02 is far below any perceptible difference; the alternative
       was a hotter chroma to buy a hue that nobody can see. Low chroma won.

       ⚠️ THE DARK-SURFACE RAMP IS DELIBERATELY NOT WARMED — see the
       `.toy-stage, .band.void` block at the end of this file for why. */
  --text-primary: #4a433b;
  --text-bright: var(--ink);
  --text-secondary: #5e574f;
  /* 🔴 AA FAILURE FIXED, 2026-09-29 — 4.07:1, measured in the browser on
     the built page, not eyeballed. The comment this replaces was accurate
     when it was written and became false three days later without anybody
     touching the line, which is the whole lesson: it read "#737373 clears it
     at 4.54:1 against `#FAFAF8`" — true, but `--bg` MOVED to the warm cream
     #FAECD7 later that same pass (see the ground + structure section at the
     top of this file) and this token was never re-solved against it. #737373
     on #FAECD7 is 4.074:1, below the 4.5:1 small-text floor, and it renders
     on the `<time>` stamp and `.legal-signature` of /privacy and
     /accessibility — including, with no irony spared, the accessibility page
     itself.

     WHY #6B665E AND NOT A DARKER GREY: it is already in this repo, as
     src/css/base.css's zero-specificity scaffold floor for this exact token,
     chosen there against design.md's warm off-white default. Reusing it
     means one hex for one job instead of a second near-neighbour to keep in
     sync. It is also WARM, which #737373 was not — a cold grey on a warm
     cream ground was reading slightly dirty as well as failing.

     ⚠️ WHY NOT THE MINIMUM THAT PASSES: #706B62 clears at 4.547:1 and would
     have kept the tier one shade lighter. design.md's standing rule settles
     it — "Muted ≠ illegible. When a muted tier fails contrast, darken the
     tier — don't shrink its usage or argue the aesthetics." A 0.047
     margin on a rounded hex is not a margin.

     MEASURED, on #FAECD7:  #737373 4.074:1 (FAIL)  →  #6B665E 4.894:1 (PASS)
     The same value on the old #FAFAF8 would be 5.45:1, so nothing that was
     passing stops passing if a surface ever goes back. */
  --text-muted: #6B665E;
  --text-code: var(--text-primary);      /* tracks text-primary, same relationship as before */
  --text-placeholder: var(--text-muted); /* tracks text-muted, same relationship as before */

  /* ── Over a scrim: same neutral-grey treatment for the two body-ink
       tokens; the two accent-derived on-scrim tokens collapse to white for
       the same reason the base accent tokens do. No photographic content
       exists on this site (PLAN.md), so these are dead code today — kept
       harmless for whenever/if that changes. ────────────────────────────
     */
  --text-on-scrim: #dadada;      /* was #CCDCF0, same luminance as text-bright */
  --on-scrim-quiet: #8c8c8c;     /* was #828D9C */
  --on-scrim-quietest: #626262;  /* was #5A636F */
  --accent-on-scrim: var(--structure);
  --secondary-on-scrim: var(--structure);

  /* ── Ink on a filled ground (REVISED 2026-09-26, ground flip). Two
       different situations now, not one:
       - `--accent` is a FIXED colour (`var(--theme-flute)`, band-palette
         section below) — unrelated to page/surface scope — so its ink
         needs to be a FIXED dark ink too, not the (now differently
         meaningful) `--bg`. `--ink` (#111111) on `--theme-flute`
         (#B8DBED) measures a very high ratio (well past AA; flute was
         ORIGINALLY chosen against `--ink` at 4.5:1+ — see the band-palette
         section — this is the same pairing, restated for the fill/ink
         convention here rather than a new one).
       - `--secondary` is the ADAPTIVE tone now (`var(--text-bright)`,
         above: dark ink by default, white inside `.toy`/`.band.void`) —
         so ink-on-secondary needs to flip WITH it, and `var(--bg)` still
         does that correctly with no change needed: `--bg` is `#FAFAF8`
         (light ink) by default and `#000000` inside the dark-surface
         override below, i.e. exactly the opposite of whatever
         `--secondary` resolves to in the same scope, in both scopes, by
         construction — not a coincidence, `--bg` and `--structure` were
         always the two poles of the same surface.
       - success/warning FILLS are unscoped semantic colours (Semantics
         section, header) — their ink needs to be a FIXED dark ink
         regardless of page theme, so `var(--bg)` (now sometimes light,
         sometimes dark) is wrong for them where it wasn't before; repointed
         to `var(--ink)` (fixed). Error's ink (`#F0F0F0`) is untouched — it
         was never derived from `--bg` and isn't affected by any of this.
     */
  /* 🔴 `--bg`, NOT `--ink`, since 2026-09-29. It inverted in lockstep with
     `--accent` (see the band-palette section at the end of this block):
     `--accent` was the pale `--theme-flute` and is now `--text-bright`
     (#111111), so an on-accent ink of `--ink` would be #111 on #111 —
     invisible. Cream on near-black measures 16.3:1. The two tokens are a
     PAIR and must never be re-pointed one at a time. */
  --ink-on-accent: var(--bg);
  --ink-on-secondary: var(--bg);
  --ink-on-success: var(--ink);
  --ink-on-warning: var(--ink);

  /* ── Type (fourth pass, 2026-09-25 — SAME day, a THIRD owner decision):
       Urbanist retired. Touchtone now follows the sibling Riffmix site's
       APPROVED design system (riffmix-design-system-20260925, variant R) —
       three self-hosted faces, not one. See header's Type section below for
       the full argument, the per-face licence findings, and the delivery
       fix. `--tracking-tight` (house constant, `-0.011em`, unchanged — see
       header for why General Sans doesn't need a new value) remains the
       BODY value; `--tracking-display` is retuned for Cabinet Grotesk
       (`-0.035em`, was `-0.02em` for Urbanist); `--tracking-mono` is new. */
  /* 🔴 ONE FAMILY, 2026-09-29 — OWNER CALL: "the fonts dont match the new
     artwork." `--font-display` was 'Cabinet Grotesk' 800 and is now General
     Sans, the same family as the body. Cabinet Grotesk is RETIRED from the
     site (its `@font-face` and its 20 KB file are gone — see below).

     THE DIAGNOSIS, so this is not re-litigated: the type system was imported
     wholesale from Riffmix (`assets/fonts/SOURCE.md`, approved 2026-09-25) on
     the argument "companion apps, one shared visual language." The crew
     artwork was generated three days later for THIS app, and it is a warm,
     muted, hairline-contour picture-book register aimed at a parent. Cabinet
     Grotesk exists only as an ExtraBold cut: a high-contrast, sharply-cut
     display grotesk set at up to 5.2rem with `line-height: .92` — a slab of
     near-solid ink, where the artwork's heaviest mark anywhere is a 2px
     hairline. It read as a fashion-magazine cover title pasted onto a picture
     book. The type came from the adult sibling; the art was drawn for the
     child. That seam is what the owner was seeing.

     WHY GENERAL SANS AND NOT A NEW DISPLAY FACE: it is already here, already
     self-hosted, already licensed (ITF-FFL, same file), and it is the only
     one of the four faces that matched the art — a neutral, open-aperture
     humanist grotesque whose stroke value at 400/600 is close to the
     illustration's own contour. design.md asks for ONE family per surface; at
     most two. This is one, plus one new woff2 (the 600 cut `components.css`
     was ALREADY asking for and which did not ship), and it REDUCES payload,
     because Cabinet Grotesk and one of the two mono cuts come out with it.
     Let the drawing be the loud thing and keep the type quiet — the same
     relationship the desaturated children have to the coloured notes. */
  --font-body: 'General Sans', -apple-system, system-ui, "Helvetica Neue", sans-serif;
  --font-display: 'General Sans', -apple-system, system-ui, "Helvetica Neue", sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', SFMono-Regular, monospace;
  /* Retuned WITH the face, not carried over: `-0.035em` was the approved
     Riffmix spec's value for Cabinet Grotesk, a face with much tighter
     default fit. General Sans at 600 sets looser and needs less pulling in;
     `-0.02em` keeps design.md's "tighter than the framework default, because
     loose-tracked type reads as decorative" without the letters touching at
     3.6rem. A tracking value belongs to a typeface, never to a site. */
  --tracking-display: -0.02em;
  --tracking-mono: 0.14em;

  /* Riffmix-native short names — the approved system's OWN CSS variable
     names (`variant-R.html`'s `:root`), kept as a parallel vocabulary
     alongside this repo's `--font-*`/`--tracking-*` convention rather than
     replacing it, so markup written against either sibling's approved
     reference reads correctly here. Aliased, not restated, so the two
     vocabularies cannot drift from each other. */
  --disp: var(--font-display);
  --body: var(--font-body);
  --mono: var(--font-mono);

  /* ── The band palette (riffmix-design-system-20260925, approved
       2026-09-25T14:36:52Z, variant R `:root`).

       🔴 `--void` STOPPED being `--bg`'s alias 2026-09-26 (ground + structure
       section, top of this file) — the two facts came apart the moment the
       PAGE ground went light while true black had to survive as a
       deliberate object (the hero, the `#world` scene card). `--void` is
       its own literal, `#000000`, defined once, above, alongside `--bg`.
       The reasoning that ORIGINALLY pinned it there —
       `measurements.reel_corner_pixel` in `approved.json`, "artifact bands
       MUST be true black or the square shows" — governed RASTER
       SCREENSHOTS (`assets/screens/`), which no longer exist (the `#world`
       scene is SVG); that specific citation is dead, but `--void`'s VALUE
       is still non-negotiable for an independent, current reason: the
       hero's own law (`docs/designs/site-home-the-toy.md`: "pitch black,
       not near-black") and the scene card's app-fidelity argument (white-
       on-black drawings depicting a white-on-black app). 🔴 That scene card
       is `no-wrong-way`'s wall panel since 2026-09-29 (`#world`/
       `layouts/shot-band.yml` retired that day — see the header's "Ground +
       structure" note and the dark-surface override block below for the
       full account); the argument above is unchanged, only the markup
       carrying it moved. Confirmed unchanged, live on the page, after every
       edit in this pass — not just in source. (`--steel` had no
       app-sourced counterpart at all and
       was DELETED 2026-09-29 — see the note where it used to be declared,
       just below.) `--ink` likewise has no app
       equivalent (it's a text colour, not a theme colour —
       SystemTheme.ringColor has none dark enough to read as "ink" against
       a light band) — and is now doubly load-bearing, since `--text-bright`
       (above) aliases IT for the light-ground default too. */
  /* 🔴 `--steel` (#C9C9C6) DELETED 2026-09-29 with the card system — owner
     call, full argument in src/css/bands.css's header. It had exactly one
     consumer, `.band.steel`, on three of the home page's eight sections: a
     COLD grey on a warm cream page, beside warm picture-book artwork. It was
     also the only value in this whole file with no counterpart in the app at
     all (a bare Riffmix literal), which is the tell. Grep before restoring
     it: as of this change nothing in src/ or layouts/ reads it. */
  /* 🔴 #111111 → #161009, 2026-09-29 (the warm-neutral pass — the full
     argument and the whole before/after table live in the text-tiers section
     above; this line is only where the literal happens to sit).
     Y 0.005605 → 0.005609 (+0.06%), oklch chroma .0000 → .0169 at hue 70.8°,
     16.225:1 → 16.224:1 on `--bg`. This is the site's most load-bearing hex:
     `--text-bright` aliases it, and `--border`/`--border-bright`/`--accent`/
     `--secondary`/`--link`/`--text-selection` alias THAT — so a change here
     moves every heading, every hairline, every focus ring and the selected-
     control fill on /support at once. All of them were re-measured in the
     browser afterwards rather than reasoned about: the focus ring holds at
     16.22:1, the checked report-kind thumb holds at 16.22:1 against
     `--ink-on-accent`.
     ⚠️ THIS IS STILL BLACK. Set beside the old value it is warm the way
     printer's ink is warm; seen alone it reads as black, and it is meant to.
     Do not "finish the job" by pushing it toward brown — see the chroma
     warning in the text-tiers section.
     ⚠️ `--ink` is GLOBAL, not scoped, and it has two consumers that are NOT
     text tiers: `--ink-on-success`/`--ink-on-warning` (dark ink on the
     semantic fills). Measured, not assumed — on `--success` #3D9E6A
     5.6625 → 5.6621, on `--warning` #E8B84D 10.2459 → 10.2453. Four
     ten-thousandths, in the fourth decimal place, on two fills that render
     nowhere today. Nothing to re-solve. */
  --ink: #161009;

  /* 🔴 REVISED 2026-09-25 (owner feedback, same day as the band palette
     above): "the pentatonic red bg doesn't jive with the rest of the app —
     base colors should also come from the inner circle colors." The
     reasoning immediately below — `--accent: var(--note-0)` — was technically
     sound (the two hexes DID match `approved.json`'s `#FF4F45`) but wrong in
     kind: a NOTE colour means one struck pitch in this app's own law: "the
     pentatonic guarantee holds only for indices 0-7" (`SoundPalette.swift`) —
     stretching one note across an entire page-width band makes a much bigger
     claim than "this note was struck," and reads as an imported Riffmix
     accent wearing an app-native justification, not as something the app
     itself would do. `--note-0` ITSELF IS UNCHANGED AND STILL CORRECT for
     what it actually names — a struck C4. Only its use as a BAND GROUND is
     retired; nobody should re-derive the `var(--note-0)` reasoning below for
     a future accent-band need — read the paragraph you're in instead.

     The fix: `theme/theme-colors.css` (generated by the new
     `scripts/gen-theme-colors.mjs`, parsing `SystemTheme.ringColor` —
     `Touchtone/SystemNode.swift:43-53` — the hub bowl's instrument-picker
     disc, which "rotates its disc colour to the current voice's theme
     colour," BrushHub.setInstrument). An INSTRUMENT theme colour names a
     whole voice's identity, not one pitch — "this band is voiced like
     drums" is a claim the app's own hub bowl already makes about a whole
     disc, so a full-band use is the SAME kind of claim, not a bigger one.
     `--paper` moves too, same logic, same pass: `keys` (#F2EDD9, the app's
     own piano-theme cream) replaces Riffmix's literal `#EFEFED` — visually
     near-identical (both a warm off-white; the swap is not visible on its
     own) but now traceable to shipped Swift instead of the sibling site.

     Eleven candidates checked (`--theme-tones` through `--theme-rhodes`);
     full contrast table in theme/design-direction.md. Chose
     `--theme-flute` (#B8DBED, pale sky blue) for `--accent`, NOT the
     "warmer" options (`--theme-drums`/`--theme-guitar`/`--theme-rhodes`)
     the owner's "warm/desaturated" hint favoured — because none of those
     three clears WCAG 4.5:1 for `--ink` body text at the `.62` opacity
     `.sub` actually renders at (drums 4.00:1, guitar 4.19:1, rhodes
     4.46:1 — all measured, all short), and task 3's own ordering is
     explicit that contrast is the HARD gate, ahead of the warmth
     preference, precisely so this ground would not hand the next agent
     the same "the sub-line had to be deleted" problem `--note-0` did.
     `--theme-flute` measures 4.60:1 at `.62` — the only non-reserved
     candidate that actually clears it. Flagged plainly, per instruction,
     rather than discovered later: NONE of the eleven — including
     `--theme-flute` — clears 4.5:1 at the `.plate` opacity (`.55`); the
     best is 4.60:1×(.55/.62 scaling, measured directly) ≈ 3.7:1. This is
     not unique to this token: `--steel` ALREADY fell short at both `.62`
     (4.36:1) and `.55` (3.57:1) with `--ink` text — a pre-existing
     condition, not introduced here, surfaced because computing this table
     required checking it too. ✅ ALL OF IT IS MOOT AS OF 2026-09-29: the
     eleven theme colours are no longer used as band grounds at all, and
     `.plate` no longer uses an opacity — it reads `--text-muted`, which is
     scoped per surface and measures 4.89:1 on cream / 5.61:1 on `--void`.
     The carve-out this paragraph went on to claim is no longer needed and
     is no longer claimed; see `src/css/bands.css`'s `.plate` rule. The
     stale advice it gave — treat `.plate`-class 11px/.55-opacity labels as
     incidental text under WCAG 1.4.3's own carve-out, on every band, until
     an opacity or size change closes the gap — is superseded by that change
     having been made. */
  /* 🔴 `--paper` AND `--accent` RETIRED 2026-09-29 — OWNER CALL, and the
     reason is this file's OWN law two hundred lines above: "White is
     structure, colour is content... Colour means a note. Always — never
     decoration."

     Both lines broke it in the same way. `--paper: var(--theme-keys)`
     (#F2EDD9) put the app's KEYS voice-identity colour behind two bands, the
     header and the footer — 8/15/2 per channel from `--bg`, so it never read
     as a second ground, only as a smudge the page opened and closed on.
     `--accent: var(--theme-flute)` (#B8DBED) conscripted the FLUTE voice as
     sitewide chrome, and was picked on a contrast technicality (the one
     `SystemTheme.ringColor` candidate clearing 4.5:1 for `--ink` at `.sub`'s
     `.62`) rather than because a band ever meant a flute. A band does not
     mean a note.

     WHAT HAPPENS NOW THAT THESE LINES ARE GONE:
       `--paper`  — no consumers left at all (`.band.paper` retired with the
                    cards; `.site-header`/`.site-footer` moved to `--bg`, the
                    literal "another cream lift" the owner asked for).
                    Deleted outright rather than re-aliased to `--bg`, so the
                    next reader cannot resurrect a second ground by using a
                    token that still exists.
       `--accent` — falls through to the baseline ~200 lines above,
                    `--accent: var(--text-bright)`, which was written for
                    exactly this eventuality ("this baseline exists so the
                    token is never literally undefined/decorative if that
                    override is ever removed"). On the light ground that
                    resolves to `--ink` #111111.

     ⚠️ ONE CONSUMER MOVES WITH IT, and it is not on the home page:
     `src/pages/support/issue-report.css:221` fills the CHECKED report-kind
     thumb with `var(--accent)` and labels it `var(--ink-on-accent)`. With
     `--accent` now near-black, `--ink-on-accent` is re-pointed from `--ink`
     to `--bg` (see that token, above) so the pair inverts as a unit: a
     selected control becomes a dark thumb with a cream label, measured
     16.3:1, instead of a pale-blue one. That is also the more lawful answer
     — a selected state is STRUCTURE, and structure on this site is carried
     by value, never by hue. */
}

/**
 * # The dark-surface override, 2026-09-26 — the other half of the ground flip
 *
 * `:root` above is now DARK INK ON LIGHT — the site's default reading
 * surface. One place stays dark ON PURPOSE, as a deliberate object rather
 * than the page default (see the "Ground + structure" section, top of this
 * file, for the full argument): the hero (`.toy` below — renamed
 * `.toy-stage` 2026-09-29, see the note immediately after this block).
 *
 * 🔴 THE SECOND PLACE RETIRED, 2026-09-29 — OWNER CORRECTION, VERBATIM: "the
 * stage behind 'Push past a limit' and the three kids is the zoom-in-out
 * animation - you are inventing new things." `layouts/shot-band.yml` and
 * the `<section id="world" class="band void shot-band">` it rendered
 * (previously cited here as `layouts/shot-band.yml:298`) are deleted; the
 * same 18-second SVG journey now runs inside `no-wrong-way`'s wall panel —
 * `layouts/scene-band-live.yml:208-209`, `<div class="wall-surface">`
 * wrapping `<svg class="wall-svg">`. That panel is NOT a section-level
 * `.band.void` — `no-wrong-way` is an ordinary cream band (`src/css/
 * wall.css`'s own "WHY THIS BAND IS NOT `.void`" note explains why) — only
 * the panel inside it is black, via `background: var(--void)` set directly
 * (`src/css/wall.css:112`), which needs none of this block's token flips:
 * the panel has no text on the dark-ramp ladder, and its one caption sets
 * `color` straight off `--structure` (see `wall.css`'s caption note), not
 * through any of the tokens below. So `.band.void` in the selector below is
 * now UNBOUND — exactly like `src/css/bands.css`'s own copy of the same
 * selector — kept as the declared home for the next band that genuinely
 * needs the app's ground full-bleed, not because anything on the page
 * currently reads it.
 *
 * This block re-declares every token whose value must flip inside that
 * surface (`.toy-stage`; `.band.void` is unbound today — see above) —
 * restoring the ORIGINAL 2026-09-25 dark-theme ramp this file computed
 * BEFORE the ground flip (same greys, same reasoning, unchanged numbers),
 * rather than re-deriving them a second time. Because
 * CSS custom properties inherit down the DOM tree and this selector sits
 * ABOVE every real consumer in both subtrees, every existing rule in
 * `base.css`/`components.css`/`bands.css`/`toy.css` that reads one of
 * these tokens picks up the correct value with ZERO edits to those files —
 * the whole reason this is possible from a `theme/law.css`-only lease.
 *
 * `--structure` is deliberately NOT re-declared here — it is already
 * `#ffffff` unconditionally (see header), so there is nothing to restore.
 * `--void`/`--bg` likewise need no entry here for THEIR OWN sake (`--void`
 * is already the fixed black literal everywhere); `--bg` IS re-declared
 * below because real consumers (`toy.css`'s `.toy { background: var(--bg)
 * }`, `body`'s own default) read it directly and need it to mean black
 * specifically inside `.toy` — `.band.void`'s own background comes from
 * `--void` directly (`bands.css`), not `--bg`, but `--bg` is included here
 * too for the several tokens below (`--ink-on-secondary`, the surface
 * ladder) that are DEFINED in terms of it.
 */
/* 🔴 `.toy` → `.toy-stage`, 2026-09-29. The scope used to be the WHOLE hero
   section, back when the whole hero was black. It is not any more: the owner's
   ground flip the same day made the page cream and kept only the play surface
   black ("cream page, black play surface only"), so `.toy` now sits on the
   light page and only `.toy-stage` — the box wrapping the play layers, see
   layouts/toy.yml — is dark. Leaving `.toy` here would have kept the newly
   VISIBLE hero copy (`.toy-copy`) on the dark ramp: light-grey text on cream,
   which fails contrast outright. `.band.void` is unchanged. */
.toy-stage,
.band.void {
  --bg: #000000;

  /* Text tiers — restored to the 2026-09-25 dark-theme ramp (luminance-
     preserving conversions of `stage`'s original blue, computed once, not
     re-derived): identical numbers to what shipped before this pass, only
     now scoped rather than global.

     🔴 DELIBERATELY NOT WARMED, 2026-09-29 — and this is a decision, not an
     omission. The warm-neutral pass rewrote the ENTIRE light ramp at `:root`
     (see the text-tiers and surfaces sections up there) and stopped at this
     selector on purpose. The reason is the same one that justifies the pass
     in the first place: a neutral is warmed to AGREE WITH ITS GROUND, and
     this block's ground is `--void`, pure `#000000`. Black has no hue. There
     is nothing here for a warm grey to agree with, so warming these rungs
     would not be removing an accidental second colour — it would be adding a
     first one, on the surface where this file's law is least willing to
     bend ("white is structure"; the hero is the app's own white-on-black
     look, reproduced — `#world` argued the same case for this selector
     until it was retired hours later that same day, 2026-09-29; see the
     dark-surface override block above). A warm grey on black is decoration.
     A warm grey on cream is the paper showing through the ink.
     So: light ramp warm, dark ramp neutral, and the asymmetry is the point.
     `--accent-dim` below keeps its neutral `#5d5d5d` for the same reason —
     and, for the same reason again, its LIGHT twin was left neutral too, but
     on a different argument (it is dead); see its note at `:root`. */
  --text-primary: #b3b3b3;
  --text-bright: #dadada;
  --text-secondary: #989898;
  --text-muted: #848484;
  --text-code: var(--text-primary);
  --text-placeholder: var(--text-muted);
  --text-selection: var(--structure);

  /* Border/link/accent-adjacent — `--text-bright` above already resolves
     to white in this scope, so every alias (`--border`, `--border-bright`,
     `--link`, `--accent-bright`, `--secondary`, `--secondary-bright`) turns
     white automatically; nothing to re-declare for those specifically.
     `--accent-dim` is the one exception (a real grey, not an alias — see
     its own root-scope comment) and gets its ORIGINAL dark-theme value
     back, band-clamped the same way it was before the ground flip. */
  --accent-dim: #5d5d5d;

  /* Surfaces — restored to the 2026-09-25 dark elevation ladder. Unconsumed
     inside these two subtrees today (checked — nothing in `toy.css`/
     `bands.css` reads `--surface`/`--surface-high`/etc.), kept for the same
     forward-guarantee reason the light-mode versions are, at the root. */
  --surface: #0f0f0f;
  --surface-high: #121212;
  --surface-lift: #151515;
  --terminal: #0b0b0b;
  --input: #121212;
  --highlight: #212121;
}

/**
 * # Type, 2026-09-25 (SECOND revision, same day) — the Riffmix trio
 *
 * Urbanist (chosen earlier this same day as "one geometric sans," full
 * reasoning kept in `theme/design-direction.md` as a path taken and
 * rejected, not erased) is RETIRED. Owner decision: Touchtone's marketing
 * site now follows the sibling Riffmix site's APPROVED design system
 * (`riffmix-design-system-20260925`, variant R, approved 2026-09-25T14:36:52Z
 * — `.../a/variant-R.html`'s `:root` block and type rules are the spec).
 * Companion apps, one shared engine (`soundcore`), reading as kin —
 * including sharing the sibling's own three-face type system and its exact
 * CSS variable vocabulary (`--disp`/`--body`/`--mono`, aliased above to this
 * repo's own `--font-*` names rather than replacing them).
 *
 * ## The three faces
 *
 * ⚠️ AS IMPORTED 2026-09-25 — two of these three rows are SUPERSEDED, see the
 * one-family note at the top of this file and `assets/fonts/SOURCE.md`:
 *
 *   --disp  Cabinet Grotesk, weight 800 only   → RETIRED; General Sans 600
 *   --body  General Sans, weights 400 + 500    → unchanged (+ the 600 cut)
 *   --mono  IBM Plex Mono, weights 400 + 500 (nav, corner plates, spec
 *           column, CTA — a much bigger role than Urbanist-era `--font-mono`
 *           DM Mono ever had, which was colophon-only)
 *                                              → SCOPED to machine output
 *                                                only, 400 alone
 *
 * All three were chosen by the approved spec, not by the importing agent —
 * the job there was licence verification and delivery, not face selection.
 * The re-cut three days later WAS a face selection, made on the owner's call
 * against the artwork that shipped in between.
 *
 * ## Licence findings — gated the whole task, checked per face, not assumed
 *
 * **IBM Plex Mono — OFL 1.1** (`assets/fonts/OFL-IBMPlexMono.txt`, fetched
 * verbatim from `google/fonts`, same pattern as Urbanist). Self-hosting and
 * subsetting both explicitly permitted. No change in handling.
 *
 * 🔴 **Cabinet Grotesk + General Sans — ITF Free Font License v2.0**
 * (`assets/fonts/ITF-FFL.txt`), NOT OFL — confirmed via
 * `api.fontshare.com/v2/fonts/slug/<slug>`, both report
 * `"license_type": "itf_ffl"`. Full text fetched 2026-09-25 by RENDERING
 * `fontshare.com/licenses/itf-ffl` (a client-rendered React app — `curl`
 * alone returns an empty SPA shell, no license text; this one needed a
 * browser, not a request, to actually verify rather than assume). Findings:
 * self-hosting is explicitly PERMITTED and "recommended" (§01) — this
 * gated the whole task, and had it come back forbidding self-hosting the
 * instruction was to stop and report, not ship or silently fall back to
 * Fontshare's own CDN links. But §02 explicitly forbids subsetting and
 * format conversion without ITF's written consent (a real divergence from
 * every OFL font this site self-hosts) — so every ITF file this repo ships
 * (`general-sans-400/500/600.woff2`; `cabinet-grotesk-800.woff2` while it was
 * here) is used EXACTLY as `api.fontshare.com`'s CSS endpoint serves it: one
 * file per weight, no
 * unicode-range split, no re-encoding, no `pyftsubset` pass by this repo's
 * tooling. See `assets/fonts/SOURCE.md` for the full findings, fetch
 * commands and file table.
 *
 * ## Delivery — same mechanism as Urbanist, still self-hosted
 *
 * Not linked to `api.fontshare.com`/`fonts.googleapis.com` (Riffmix's own
 * approved HTML does) — Touchtone's privacy page leads with an import audit
 * proving nothing leaves the device (PLAN.md §5); a third-party font request
 * on every load would leak the visitor's IP for no functional gain over
 * hosting the identical bytes here, directly contradicting that claim.
 * `site/assets/fonts/` (confirmed, again, that `pipeline/emit.mjs`'s
 * `copyAssets()` needs no change — generic recursive `assets/` copy).
 * `font-display: swap` on all four `@font-face` blocks below. No italic cut:
 * `components.css`'s `.maintenance` was the one italic consumer under
 * Urbanist and still sets `font-style: italic` on a face with no italic, so
 * it synthesises; re-add a real cut the same way if it ever matters, same
 * licence caveats apply.
 *
 * ── WHAT MONO IS ALLOWED TO MEAN HERE, decided 2026-09-29 ─────────────
 * IBM Plex Mono used to be the most-used face on the home page — `.sub`,
 * `.facts-list`, `.cta`, `.plate`, `.spec`, `.colophon`, `.build-number`,
 * all at 11–13px and +0.14em tracking. That is a laboratory instrument
 * panel, and Plex Mono is specifically IBM's corporate ENGINEERING mono,
 * drawn to say "made by engineers." The site's own design doc had already
 * diagnosed the symptom ("the only visible copy was 11px engineering
 * telemetry") without naming the typeface as the cause. Worst of it: every
 * button on `/` was set in it, so the one moment the page asked a parent to
 * act, it did so in 12px machine-tracked mono.
 *
 * 🔴 THE RULE NOW: mono means LITERAL MACHINE OUTPUT, and nothing else.
 * Four consumers survive and they are all the same kind of thing — a build
 * stamp or an `aria-hidden` telemetry label that quotes the app's own
 * readout: `.build-number`, `.colophon`, `.plate`, `.spec`. Human prose is
 * General Sans everywhere, on every page. A lone deliberate mono use is
 * defensible; four scattered ones were not, and prose in mono never was.
 *
 * ONE file preloaded from `theme/head.html`. With the display face and the
 * body face now the same family, `general-sans-400.woff2` is the single
 * primary cut the Urbanist setup used to have and this one lost — it is the
 * hero sentence, the body of every page, and (via the 600 cut, which shares
 * its `font-family` so the fallback is the right shape) the LCP headline.
 * The mono 400 is NOT preloaded any more: nothing it sets is above the fold
 * now that the nav is gone, and preloading the telemetry voice first was
 * always the wrong priority order. Combined payload is ~79 KB across four
 * files, down from ~86 KB across five.
 *
 * ## Tracking
 *
 * `--tracking-display: -0.02em` — retuned 2026-09-29 with the face. It was
 * `-0.035em`, the approved Riffmix spec's `.say` value for Cabinet Grotesk
 * (`variant-R.html`, verbatim). A tracking value belongs to a TYPEFACE, not
 * to a site, so it moved when the typeface did; General Sans sets looser and
 * needs less pulling in. (`-0.02em` is also, by coincidence, the Urbanist
 * value this file carried two passes ago — different face, same ballpark.)
 * Applied to `h1`–`h3` via the SAME specificity-bump rule already in place
 * below (`body h1, body h2, body h3` beats `base.css`'s plain `h1, h2, h3`
 * regardless of load order, no `!important`) — the token value changed, the
 * mechanism didn't, so no rule-shape edit was needed for this half of the
 * task. `--tracking-mono: 0.14em` is new — the approved spec's `.nav`/
 * `.plate`/`.spec`/`.sub`/`.cta` all read `letter-spacing:.14em`; not wired
 * to a selector here (no existing shared-variable collision forces a
 * theme-layer answer the way `h1`–`h3` did — `--font-mono` consumers can
 * read `var(--tracking-mono)` directly wherever they're authored, in
 * whichever file owns that markup).
 *
 * `--tracking-tight` (`-0.011em`, body copy) is UNCHANGED but re-confirmed,
 * not passively carried over: General Sans is a comparably neutral,
 * moderate-x-height grotesk (Fontshare's own listing: a general-purpose
 * sans, not a display face with unusual metrics the way Cabinet Grotesk or
 * Urbanist are), and `-0.011em` is this house's general "just tighter than
 * default" body constant (design.md), not a value tuned to Urbanist's
 * specific proportions — so it travels to General Sans on its own merits,
 * confirmed by rendering (see `theme/design-direction.md`), not by default.
 *
 * `.wordmark` still does NOT get `--tracking-display`, unchanged reasoning
 * from the Urbanist pass: it renders at `1.125rem` (18px, bold,
 * `components.css:34-41`) — body-copy scale, not display scale, regardless
 * of which face `--font-display`/`--disp` currently names. Worth flagging:
 * the approved spec's own `.nav` (which contains Riffmix's wordmark) is
 * styled entirely in `--mono`, not `--disp` — a genuine difference from how
 * `components.css:34-41` currently wires `.wordmark` here (`var(--font-display)`).
 * Not changed in this pass — `components.css` is outside this lease; flagged
 * in the handoff report for whoever owns that file to decide.
 */
body h1, body h2, body h3 {
  letter-spacing: var(--tracking-display);
}

/* 🔴 `cabinet-grotesk-800.woff2` (20,204 B) DELETED 2026-09-29 with its
 * `@font-face` — see the Type note at the top of this file. Nothing reads
 * `'Cabinet Grotesk'` any more; `--font-display` is General Sans.
 *
 * General Sans 600 is the cut that replaces it, and it is NOT a new licence
 * question: `api.fontshare.com/v2/fonts/slug/general-sans` reports
 * `"license_type": "itf_ffl"` for the FAMILY, and ITF-FFL v2.0's own
 * "Font Software" definition covers every file ITF distributes under the
 * licence — so `assets/fonts/ITF-FFL.txt`, already vendored, governs this cut
 * exactly as it governs 400 and 500. Confirmed before the file was
 * downloaded, not after. Fetched from the Fontshare CDN unmodified: no
 * subsetting, no re-encoding, no unicode-range split — Section 02 forbids
 * all three without written consent, which is why the two ITF faces alone
 * ship full-unicode while the OFL mono below carries a `unicode-range`. */
@font-face {
  font-family: 'General Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/general-sans-400.woff2') format('woff2');
}

@font-face {
  font-family: 'General Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/general-sans-500.woff2') format('woff2');
}

/* The display cut, added 2026-09-29. `components.css`'s `.btn`/`.badge` had
 * been ASKING for weight 600 since the Riffmix import without it ever
 * shipping — those buttons were rendering synthetic-bold or silently falling
 * back to 500. So this file closes an existing bug as well as supplying the
 * new display voice. */
@font-face {
  font-family: 'General Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/general-sans-600.woff2') format('woff2');
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* 🔴 `ibm-plex-mono-500-latin.woff2` (10,060 B) DELETED 2026-09-29 with its
 * `@font-face`. It arrived with the Riffmix import and never acquired a
 * consumer: grepped src/css, src/pages and layouts before deleting — not one
 * rule pairs `var(--font-mono)` with `font-weight: 500`. The 400 cut stays
 * and is still load-bearing; see the note on it above for what mono is now
 * allowed to mean on this site. */
