/* ═══════════════════════════════════════════════════════════════════════════
   LE CODEX · CODEX-TOKENS.CSS
   The vocabulary of the design system: colour, type, space, and the few
   physical constants (radius, motion). Nothing here draws a component — this
   file only names values. Every page links this first, then the component
   sheet, then (optionally) its own page-specific styles.

   Extracted verbatim from the canonical homepage (the parchment "$499 / $799"
   reference build). Do not fork these values into a page's own :root — change
   them here and the whole product moves together.
   ═══════════════════════════════════════════════════════════════════════════ */

/* UI STANDARD v2's ONE RULE needs a sans face, and the product had none — the
   44 pages each link Cinzel and EB Garamond in their own <head>, and --sans was
   a bare `system-ui` stack. Inter is loaded HERE, once, rather than pasted into
   44 heads: every page already links this sheet, so one line reaches all of
   them and no page can be the one that forgot.

   Weights are exactly the four the standard names. @import must precede every
   rule in the file, which is why it sits above :root rather than beside the
   type tokens it serves. */
@import url('/static/vendor/fonts/fonts.css');

:root {

  /* ═══ UI STANDARD v2 — THE CANONICAL PALETTE (RUN 66) ═════════════════════
     docs/design/UI-STANDARD-v2.md is the spec. These eleven names are the
     source of truth for colour; every older name below is re-pointed at one of
     them, so one edit here re-themes every screen that consumes a token.

     WHY ALIASES AND NOT A RENAME. The repo runs on --cream / --paper /
     --ink-2..4 / --rule / --command / --on-command / --sage / --mode-*, of
     which only --ink is shared with v2. A literal rename is several hundred
     sites across 44 pages and two shared sheets — the "large rebuild" the
     adopting brief says to defer rather than half-apply. This is the same
     pattern the retired dark-surface names below already use.

     MEASURED BEFORE ADOPTION, NOT AFTER — every text tone against every
     surface it can land on, both themes. The palette clears AA everywhere
     except one token, and that exception is real:

         LIGHT   --taupe on --bg    2.92:1     below the 3.0 NON-TEXT floor
                 --taupe on --chip  2.74:1     below it as well
                 --taupe on --card  3.21:1     large text / UI only, never body
         DARK    --taupe            4.07-4.70  fine on every surface

     So in LIGHT, --taupe could not carry text on the page or on a chip AT ALL,
     not even large text. That was raised as FOUNDER-CONFIRM rather than
     silently corrected: the hex is the founder's and changing it is a brand
     decision, not a refactor.

     THE FOUNDER RULED, RUN 67. Light --taupe moves #9A8E7E -> #877B6B. The
     reason it is a ruling and not a tweak: --taupe carries the wordmark's "de"
     and the mode/meta text, so it is FUNCTIONAL, not decorative, and the
     decorative exemption --rule enjoys below cannot be stretched to cover it.
     Dark --taupe is unchanged. RE-MEASURED HERE, not copied from the ruling:

         LIGHT   --taupe on --bg    3.77:1     clears the 3.0 floor
                 --taupe on --chip  3.54:1     clears
                 --taupe on --card  4.14:1     clears
                 --taupe on --cream-d 3.18:1   clears

     And it lands in the right PLACE in the ramp, which is the part a hex swap
     could easily have broken. Darkest to lightest, both themes now read
     ink > ink-2 > ink-3 > taupe > ink-4:

         LIGHT   16.50 > 6.45 > 4.80 > 3.77 > 3.19   (on --bg)
         DARK    15.74 > 7.98 > 5.83 > 4.70 > 3.31   (on --bg)

     taupe slots into the gap between the two tertiary tokens instead of
     colliding with either, which is why the four-tone scale below survives it.

     AND IT DECIDES THE ONE MAPPING THAT IS NOT OBVIOUS. v2 gives THREE text
     tones (--ink, --ink2, --taupe); this repo carries FOUR, and the extra one
     is --ink-3, the AA-small step that labels, meta and captions are set in.
     --ink-3 measures 4.80:1 on the v2 page and --taupe 2.92:1, so aliasing
     --ink-3 onto --taupe would take every small label in the product from
     passing to failing. BOTH tertiary tokens therefore keep their own values —
     --ink-4 measures 3.19:1 on the v2 page and needed no help either. The
     first version of this block aliased --ink-4 onto --taupe and the palette
     gates failed it on three pages; the note beside those tokens records what
     that argument got wrong. v2's three-tone scale is a step short at both
     ends of the tertiary range. */
  --bg:        #F7F4EF;
  --navbg:     #FFFFFF;
  --card:      #FFFFFF;
  --ink2:      #5F5750;
  --taupe:     #877B6B;   /* founder ruling RUN 67 — was #9A8E7E (2.92:1) */
  --green:     #22593B;
  --hair:      #E7E1D8;
  --chip:      #EFEDE8;
  --btn:       #1A1512;
  --btntext:   #F7F4EF;
  --shadow:    0 12px 34px -20px rgba(26,21,18,.28);
  --shadow2:   0 6px 16px -12px rgba(26,21,18,.18);

  /* ── Parchment surfaces — NOW ALIASES ONTO v2 ────────────────────────────
     The page sat on --cream #F2EDE4 and cards on --white #FDFAF5, with a
     comment insisting cards are "NOT #FFFFFF, which reads cold and breaks the
     paper illusion". v2 rules otherwise: --card IS #FFFFFF, lifted off a
     warmer --bg. That is the standard's call and this is the record of it.
     --cream-d / --cream-dd keep literal values — v2 defines no wash tier, and
     inventing one would be adding to the standard rather than adopting it. */
  --cream:     var(--bg);
  --cream-d:   #E8E1D5;   /* deeper section / selected-row wash */
  --cream-dd:  #DDD5C8;   /* deepest wash, used sparingly */
  --white:     var(--card);

  /* ── Ink ramp (text + dark surfaces) ─────────────────────────────────────
     --ink is primary text AND the colour of "command" surfaces (the top nav,
     dark sections, primary buttons). The lighter steps are a de-emphasis
     ladder. */
  --ink:       #1A1512;   /* primary text · dark surfaces — v2 (was #1A1410) */
  --ink-1:     #2C2318;   /* secondary dark surface — no v2 equivalent */
  --ink-2:     var(--ink2);   /* secondary text, body copy on light */

  /* ── THE TWO TERTIARY TOKENS ARE NOT INTERCHANGEABLE. READ THIS BEFORE
     REACHING FOR EITHER. (Founder ruling, RUN 46 C1.)

     A previous pass darkened both by the MINIMUM that reaches 3:1 on cream and
     white, and certified them "LARGE/UI only" in this comment. The product then
     used them for 8-13px text on every screen — every Cinzel eyebrow, "Sign
     out", the IBO disclaimer, .profile-nav-btn, .paper-label. At those sizes AA
     wants 4.5:1, not 3:1, so tests/test_rendered_contrast.py measured 292
     strings below AA across 22 routes and ONE root cause behind them. A comment
     saying "LARGE/UI only" is not a constraint; it is a hope.

     So the roles are now separated by their contrast, not by a note:

       --ink-3  is SMALL-TEXT SAFE. 4.52:1 on --cream, 5.06:1 on --white.
                Tuned to land just over AA-small and no further: darkening it to
                --ink-2's 6.89:1 would have collapsed the two into one step and
                flattened the type hierarchy the six type roles depend on. Hue
                and saturation held (HSL H 0.094 -> 0.090, S 0.123 -> 0.124), so
                the parchment palette is unmoved — only lightness fell.
                THIS is the token for labels, meta, eyebrows, captions: anything
                a student reads at body size.

       --ink-4  is LARGE TEXT (>=24px, or >=18.66px bold) AND NON-ESSENTIAL
                DECORATION ONLY. 3.00:1 on --cream — which is the AA bar for
                large text and non-text UI, and is NOT the bar for a 9px label.
                Placeholders, disabled states, watermark numerals. If the string
                carries meaning at body size, it is --ink-3, and the rendered
                contrast gate will say so.

     The ladder on --cream: ink 15.64 · ink-1 13.24 · ink-2 6.89 · ink-3 4.52 ·
     ink-4 3.00. Five perceptible steps, each with a job.

     ONE STATED LIMIT, not hidden: --ink-3 measures 4.05:1 on --cream-d and
     3.62:1 on --cream-dd, the deeper selected-row washes. Small text on --ink-3
     is AA on the page and on a card, and is NOT AA on a deep wash. There is one
     such site today (the Contrôle eyebrow on /) and it is fixed at that site
     rather than by darkening the token for all 378 uses of it. */
  /* BOTH KEEP THEIR OWN VALUES — v2's three-tone text scale is a step short at
     BOTH ends of the tertiary range, and the gates proved the second half of
     that after I got it wrong.

       --ink-3  4.80:1 on v2's --bg. --taupe would be 2.92:1, so aliasing it
                would take every small label in the product from passing to
                failing. Reasoned before the edit; never attempted.

       --ink-4  3.19:1 on v2's --bg — it CLEARS the 3.0 floor on the new page
                unchanged. I aliased it onto --taupe anyway, arguing the two
                were the same decoration role and that taupe's 2.92 was "no
                worse than today". Both halves were wrong: 2.92 is below the
                floor, not level with it, and the old value was never in
                trouble. test_page_palette and test_design_system_rollout both
                failed on `('ink-4', 'var(--taupe)', 2.92, 3.0)` across three
                pages, which is the alias and not the palette.

     THE FOUNDER'S RULING SETTLED IT PERMANENTLY, RUN 67, AND IN THE DIRECTION
     THIS ARGUMENT DID NOT ANTICIPATE. Light --taupe is now #877B6B (3.77:1 on
     --bg), so the contrast objection above is spent — but the ruling ALSO says
     the scale keeps FOUR tones and --ink-3/--ink-4 are NEVER aliased onto
     --taupe. The two tokens therefore keep their own values for a reason that
     no longer depends on a measurement: they are a different role, and a role
     is not a hex. Had the ruling arrived as a hex swap alone, the natural next
     edit would have been to re-attempt exactly the alias the gates caught. */
  --ink-3:     #766A5C;   /* tertiary — labels, meta, small text (AA-small) */
  --ink-4:     #988768;   /* quaternary — LARGE TEXT + DECORATION ONLY */

  /* DECORATIVE — deliberately exempt from WCAG 1.4.11, ruled 2026-07-25.
     Session 4 darkened this to #9F8561 to clear 3:1 and flagged the trade for a
     decision. The ruling is to revert: --rule is a purely decorative hairline,
     1.4.11 applies to components whose BOUNDARY must be perceivable to
     understand or operate them, and none of these carry meaning on their own.
     Darkening every border on the site to satisfy a rule that does not apply
     would have cost the parchment aesthetic for no accessibility gain.

     THE CONDITION ON THAT EXEMPTION: nothing may depend on --rule alone to be
     understood or operated. A control whose only affordance is its border, or a
     state shown only by a border colour, needs a real boundary token or a
     label/icon — it cannot be styled with --rule and called accessible.
     scripts/contrast_audit.py records this pair as 'decorative' rather than
     skipping it, so the exemption stays a visible decision rather than a hole
     in the report. */
  --rule:      var(--hair);  /* hairline borders, dividers, grid gaps (decorative) */

  /* ── Command surfaces ────────────────────────────────────────────────────
     THE INK RAMP HAS TWO JOBS AND THEY COME APART IN DARK MODE. The comment
     above says it outright: "--ink is primary text AND the colour of 'command'
     surfaces (the top nav, dark sections, primary buttons)." In light that is
     harmless — one near-black serves as text on parchment and as a dark bar.
     Under [data-theme="dark"] the two demands contradict each other: text must
     become candle-white, while the nav must stay dark. Flipping the single
     token would have turned the 137 declarations setting `background` to the
     --ink token — every
     topbar, primary button and dark CTA panel in the product — into bright
     glare strips on an espresso page, which is the exact opposite of the dark
     brief's "candle-white, never glare".

     So the surface role gets its own vocabulary. --ink keeps the text role and
     flips; these keep the surface role and stay dark in both themes.

     IN LIGHT MODE THESE ARE THE SAME VALUES AS BEFORE, deliberately: --command
     is --ink's #1A1410 and --command-2 is --ink-1's #2C2318, so repointing the
     surfaces changed no light-mode pixel. The one exception is stated in the
     commit — 18 declarations that set `color` to the --cream token on a command
     surface now use --on-command (#FDFAF5 rather than #F2EDE4), a shift of
     about one percent of luminance that raises their contrast from 16.1:1 to
     17.4:1.

     WHEN TO USE WHICH: --command for a bar, panel or primary button that is
     dark on purpose; --command-2 for the secondary panel and hover step;
     --on-command for any text or mark sitting on either. Never --ink for a
     background, and never --cream or --white for text, or the next theme
     inverts it again. */
  --command:     var(--btn);     /* nav, primary button, dark section */
  --command-2:   var(--ink-1);   /* secondary panel, hover step */
  --on-command:  var(--btntext); /* text and marks on a command surface */
  /* ── THE ON-COMMAND RAMP (RUN 67 Phase C) ────────────────────────────────
     WHY THESE EXIST: --command is `var(--btn)`, and --btn INVERTS between the
     themes (#1A1512 light, #F1EBE0 dark) because v2 makes ink the only solid
     button, cream on dark. Anything painted onto a command surface must invert
     with it — and ~200 declarations across the product painted `rgba(255,255,
     255,alpha)` instead, which is white in BOTH themes.

     MEASURED, and it is not a tidiness point. On --command:
         rgba(255,255,255,0.92)   LIGHT 15.37:1     DARK  1.17:1
         rgba(255,255,255,0.50)   LIGHT  5.27:1     DARK  1.09:1
         var(--on-command)        LIGHT 16.50:1     DARK 15.58:1
     1.17:1 is invisible. That shipped on ten public /content guides and the
     admin topbar: in dark mode the wordmark and nav labels were white text on a
     cream bar. The literal-colour gate calls this "the light/dark flip trap" and
     this is the trap firing, not an example of it.

     -2 is the secondary text step (body-legible on the bar), -rule is the
     decorative hairline that WCAG 1.4.11 does not reach — the same exemption
     and the same condition --rule carries: nothing may depend on it alone. */
  --on-command-2:    #948E85;   /* secondary text on a command surface */
  --on-command-rule: #3F3A38;   /* hairline on a command surface (decorative) */

  /* CORRECT/WRONG TINTED TEXT ON A COMMAND SURFACE — AND THIS PAIR WAS A LIVE
     FLIP BUG. ui/session_summary.html painted the session delta (the big number
     a student sees straight after finishing) #7AE0A0 / #E07A7A on
     .session-hero, which is var(--command). MEASURED on that surface:

         #7AE0A0   LIGHT 11.21:1     DARK  1.36:1     invisible
         #E07A7A   LIGHT  6.24:1     DARK  2.45:1     invisible

     --command inverts, so a light green on a hero that turns CREAM is a
     near-invisible number. These tokens invert with it instead. */
  --on-command-up:   #7AE0A0;   /* improvement, on a command surface */
  --on-command-down: #E07A7A;   /* decline, on a command surface */

  /* ── THE MASTERY BAND SCALE (skill map) ──────────────────────────────────
     A DATA SCALE, NOT A UI ACCENT — and as of RUN 70 the values say so.

     WHAT THIS BLOCK USED TO BE, AND WHY IT CHANGED. It used to read
     #6B1818 / #7A5A10 / #3A7A50 / #1A4A28 under a comment asserting that the
     bands "deliberately do NOT reuse --wrong/--correct" because "sharing a hue
     is not sharing a meaning". They were not sharing a hue. --band-1 was
     BYTE-IDENTICAL to --wrong and --band-4 to --correct, in BOTH themes,
     including the dark theme's lift — which is copying, not convergence. So
     the skill map painted "Developing (0-25)" — a student's weakest subtopics,
     their score, their bar and their legend key — in the exact pixels that mean
     "you got this wrong", and a paragraph in this file said it did not.

     It was invisible to every gate because the uses are an inline style
     attribute and a string returned from bandColor(), and because the accent
     law matched token NAMES. RUN 70 widened it to read both and to compute its
     family BY VALUE, and this is the first thing it found. The half of the
     lesson that had already been learned is two blocks down: the dark --band-3
     carries a comment explaining it was moved off dark --green for exactly this
     reason. The same test was never applied to --wrong or --correct.

     THE SCALE IS NOW SINGLE-HUE AND ORDINAL — founder ruling, RUN 70. Mastery
     is a position on a scale, so it is drawn as one: a monotone ink ramp,
     palest to deepest, with no verdict hue anywhere in it. Every band is named
     in words in the legend, the bar's LENGTH carries the precise value, and
     "not started" is a dashed outline rather than a fill, so three signals
     carry the meaning the colour used to carry alone.

     MEASURED at adoption, not copied from the ruling. Bands are fills and dots,
     not text, so the floor is the 3:1 non-text threshold; the score number
     itself is now --ink-2 and no longer takes a band colour, which raises the
     weakest reading on the map from 3.57:1 to 6.45:1.

         on --bg   band-1 3.19  band-2 4.80  band-3 6.45  band-4 16.50  (light)
         on --bg   band-1 3.31  band-2 5.83  band-3 7.98  band-4 15.74  (dark)

     THE COST, STATED. Adjacent steps sit 1.34-2.56:1 apart in light and
     1.37-1.97:1 in dark. Two neighbouring 10px dots are therefore harder to
     tell apart than a red/amber/green ramp made them, and that is the price of
     taking the verdict palette off a screen that is not a verdict. The legend
     words and the bar length are what make it affordable. */
  --band-0: #8A7F72;         /* not started — dashed OUTLINE only, never a fill */
  --band-1: var(--ink-4);    /* developing 0-25 */
  --band-2: var(--ink-3);    /* building 25-50 */
  --band-3: var(--ink-2);    /* confident 50-75 */
  --band-4: var(--ink);      /* mastered 75+ */

  /* The scrim behind a modal. One value, one name, so a second overlay cannot
     invent a slightly different black. */
  --scrim: rgba(26,20,16,0.65);

  /* ── ON A "DEEP" SURFACE — THEME-STABLE, AND DELIBERATELY SO (RUN 69) ─────
     The GDC blocks paint their heads with --gdc-ti-deep / --gdc-casio-deep /
     --gdc-hp-deep. MEASURED: those three are IDENTICAL in both themes — they
     are calculator BRAND colours (TI navy, Casio maroon, HP green), not palette
     tokens, so they do not invert the way --command does.

     THAT IS WHY --on-command IS THE WRONG TOKEN HERE and why the white-alpha on
     these surfaces was never the light/dark flip trap. --on-command inverts
     with --btn; dropped onto a surface that stays dark in both themes it would
     turn dark text on dark in one of them — the same defect, mirrored. The
     surface was checked before a token was chosen, per the P2 ruling.

     THE TRANSLUCENCY IS KEPT ON PURPOSE. A solid grey would read correctly on
     exactly one of the three deeps and clash on the other two; white at alpha
     lifts identically on navy, maroon and green. So these tokenise the ALPHA
     rather than replacing it — which satisfies law 5 (a colour that comes from
     a token and is right in both themes) without inventing three per-brand
     ramps for a control that is brand-agnostic.

     Measured worst-case across the three deep surfaces:
         --on-deep     10.25:1     --on-deep-2   ~7.2:1
         --on-deep-3   ~4.3:1      --on-deep-edge / -lift  decorative
     They are defined ONCE, in :root, and are NOT repeated in the dark block —
     the absence is the statement that this surface does not flip. */
  --on-deep:      #FFFFFF;
  --on-deep-2:    rgba(255,255,255,0.80);
  --on-deep-3:    rgba(255,255,255,0.55);
  --on-deep-edge: rgba(255,255,255,0.22);   /* boundary on a deep surface */
  --on-deep-lift: rgba(255,255,255,0.18);   /* a chip lifted off a deep surface */

  /* ── Semantic states ─────────────────────────────────────────────────────
     Desaturated on purpose. Correct is a deep forest green, wrong a deep
     oxblood — never bright. Each pairs a text colour with a low-opacity
     background wash so "correct" and "wrong" are legible without relying on
     hue alone (a colour-blind student reads them from position + label too). */
  --correct:    #1A4A28;  --correct-bg: rgba(26,74,40,0.07);   /* right answer */
  /* The deeper step of the same wash. Exists so the four-band mastery scale
     has a fourth surface WITHOUT using --sage-wash — the accent green, which
     law 1 forbids as a fill. --correct is a semantic (an answer was right),
     not the brand accent, so its wash is not an accent fill. */
  --correct-bg-2: rgba(26,74,40,0.14);
  --wrong:      #6B1818;  --wrong-bg:   rgba(107,24,24,0.07);  /* wrong answer */
  --amber:      #7A5A10;  --amber-bg:   rgba(122,90,16,0.08);  /* caution / advisory */
  /* On dark surfaces, "correct" text needs a lighter tint to stay legible: */
  --correct-on-dark: rgba(140,210,160,0.9);

  /* TEXT AND MARKS ON A SATURATED SEMANTIC FILL — and it is NOT --on-command.
     The two roles look alike and come apart in dark mode. --on-command sits on
     a surface that is dark in BOTH themes (the nav, a primary button), so it is
     light in both. This one sits on --correct / --wrong / --amber, and those
     INVERT: a deep forest green in light becomes #6FB98A in dark. The product
     wrote `color:#fff` on both kinds of surface, which is right for the first
     and, in dark, gives 1.42:1 on a --correct fill and 1.98:1 on an --amber
     one — the save toast and the concept-UID badge, both unreadable.

     So this token flips WITH the fill instead of against it. Measured on the
     three fills: light 9.86 / 11.53 / 6.07, dark 7.76 / 6.44 / 7.53.

     Its LIGHT value is --white's #FDFAF5 and its DARK value is --cream's
     #17130E, and it is deliberately a literal rather than `var(--white)`:
     dark_mode_audit's ROLES check forbids `color: var(--white)` outright,
     because a surface token used as text goes dark-on-dark the moment a theme
     flips. Naming the role is the point. */
  --on-fill:    #FDFAF5;

  /* ── Mode accents ────────────────────────────────────────────────────────
     Each working mode has ONE muted accent so a student subconsciously knows
     where they are. Never the bright primary palette — these are the darkened,
     dignified versions. Applied via body.cx-mode-* (see components sheet),
     which sets --mode-accent; components read --mode-accent, never these
     directly, so a page only ever shows one mode colour at a time. */
  /* SAGE IS THE STRUCTURAL ACCENT, and it is never a fill (Elite Design
     Standard, founder-approved 2026-08-11). It is allowed exactly five jobs:
     a left border of at most 4px, a label, a 1.5px check, a 2px nav underline,
     and the correct-answer outline. Whisper tints of 3–5% are the only
     "background" it may ever have. Anything wider or more saturated than that
     is the cheap tell the standard exists to stop.
     --mode-study now points HERE rather than carrying its own copy of the
     hex, so the study mode and the product's accent cannot drift apart. */
  --sage:            var(--green);  /* structural accent — v2 --green */
  --mode-study:      var(--sage);  /* Study — sage green */
  /* ── FOUNDER RULING, RUN 68 — ONE ACCENT MEANS ONE COLOUR ────────────────
     The four modes wore four different hues: sage, slate blue, maroon, burnt
     sienna. v2 says "one accent on screen at a time", and the ruling settles
     what that means — one accent is ONE COLOUR USED CONSISTENTLY, not one
     element. Four mode cards each wearing the same structural green is one
     accent applied four times; four cards in four colours is four accents.
     So every mode accent is now the green, and per-mode colour-coding is gone.

     --sienna SURVIVES AND IS UNTOUCHED. It still has one permitted use — the
     outline on an answer the student chose and got wrong — and that is a
     semantic, not a mode. Only --mode-exam's ALIAS onto it is retired.

     THE WASH/RULE TIERS FOLLOW, or the modes would keep their colour-coding
     through the back door: a slate-blue border on a green-accented card is the
     same defect one layer down. (The washes are separately forbidden as fills
     by law 1; these values remain only for any non-fill use.)

     CHECKED BEFORE COLLAPSING, NOT AFTER: ui/report.html was borrowing
     --mode-drill and --mode-diagnostic as a SEVERITY palette for finding types
     — nothing to do with modes. Collapsing first would have left three
     identical green badges and destroyed a real distinction, so report.html was
     conformed to v2 in the same commit and no longer references a mode token. */
  --mode-diagnostic: var(--sage);   /* was #2A4A6B slate blue */
  --mode-drill:      var(--sage);   /* was #6B1A3A maroon */
  /* SIENNA IS QUARANTINED, and the standard names both of its permitted uses:
     examination mode, and the outline on an answer the student chose and got
     wrong. Same treatment as sage — one hex, one name, and the mode is an
     alias onto it, so "the exam colour" and "the chosen-wrong colour" cannot
     drift into two different siennas. */
  --sienna:          #7A2E1A;
  --mode-exam:       var(--sage);    /* was var(--sienna) — ruling RUN 68 */
  --mode-accent:     var(--ink);/* default when no mode class is set */

  /* Each mode needs three tiers, not one: the ACCENT for text and marks, a
     RULE for its borders, and a WASH for the surface tint that tells a student
     at a glance which mode they are in. ui/student_session.html had already
     grown all three by hand; promoting them here makes the mode system a
     first-class part of the design system (which the 5-modes UI needs) instead
     of one page's private convention.

     DERIVED, NOT INVENTED. The three sets that already existed use a consistent
     relationship — rule = ~22% accent over --white, wash = ~5% accent over
     --white — and these reproduce them. The same two fractions then give the
     EXAM tier, which had no correct values before: student_session used
     #1A1410 (ink) as its exam accent, contradicting both this file and the
     design brief, which agree the examination mode is burnt sienna. Corrected
     to sienna here.

     Verified: every accent on its own wash clears WCAG 2.2 AA for body text —
     study 5.78:1, diagnostic 8.14:1, drill 9.89:1, exam 8.05:1. So mode colour
     may carry TEXT, not merely decoration. (Colour still never carries meaning
     alone — see the components sheet; a mode is always labelled too.) */
  /* The sage and sienna tiers get their own names for the same reason the
     accents did: they are used OUTSIDE the mode system. The correct-answer
     outline is sage in every mode, and the chosen-wrong outline is sienna in
     every mode — a student in Drill must not see the drill accent used to
     mark them wrong. The mode tiers are aliases onto them. */
  --sage-rule:            #C8E0D0;   --sage-wash:            #EDF5F0;
  --sienna-rule:          #E0CDC5;   --sienna-wash:          #F4ECE6;
  --mode-study-rule: var(--sage-rule);   --mode-study-wash: var(--sage-wash);
  --mode-diagnostic-rule: var(--sage-rule);   --mode-diagnostic-wash: var(--sage-wash);
  --mode-drill-rule:      var(--sage-rule);   --mode-drill-wash:      var(--sage-wash);
  --mode-exam-rule:       var(--sage-rule);   --mode-exam-wash:       var(--sage-wash);

  /* Defaults when no mode class is set — neutral, so an unclassed page is calm
     rather than accidentally wearing a mode. */
  --mode-rule:       var(--rule);
  --mode-wash:       var(--white);

  /* ── The three calculator brands ─────────────────────────────────────────
     THESE ARE NOT MODE ACCENTS AND MUST NOT BE CONFUSED WITH THEM. A mode says
     what a student is DOING (Diagnostic, Study, Drill, Le Contrôle); these say
     which MACHINE the keystrokes are for, and a student in Drill on a Casio
     needs both readings at once. They are a separate vocabulary for that
     reason, and no component reads them through --mode-accent.

     WHERE THEY CAME FROM (RUN 64). All 42 were written straight into
     ui/student_session.html as hexadecimal — 60 of that page's 73 literals. A
     hex in a page rule was never a token, so `[data-theme="dark"]` could not
     reach it: under the dark preference the calculator block painted its LIGHT
     near-white slab, carrying near-black key text, in the middle of an espresso
     page. It is the defect dark_mode_audit exists to prevent, arriving through
     a door it does not watch (its scope is TOKENS, and a literal is not a
     missing one). Promoting them here is what puts them behind that gate.

     THE LIGHT VALUES ARE THE PAGE'S OWN BYTES, unchanged, so light mode is a
     no-op by construction and was verified so mechanically rather than by eye.

     `deep` AND `accent` HOLD THE SAME LIGHT VALUE ON PURPOSE, and that is the
     find worth keeping. Each brand's deep ink was doing two jobs: the block
     head's BACKGROUND and the step label's TEXT. In light both read, because
     everything around them is pale. In dark they demand opposite directions —
     the head must stay a deep panel, the label must lift to be read. That is
     exactly the --ink / --command split this file already records, and the
     standing ruling is that a token with two roles cannot be themed. So:

       deep      a panel that is dark in BOTH themes (head, step numeral, the
                 key cap's bottom edge). Pair it with on-deep.
       accent    the brand as TEXT on the block (step label, arrow). Lifts.
       rule      hairlines and key borders — a boundary, judged at 3:1.
       wash      the block's own surface. key / key-fn / syntax / note are the
                 faces that sit on it.
       ink       the text on those faces. note-ink is its quieter sibling.

     THE DARK VALUES ARE DERIVED, NOT PICKED. Each pale tint is the brand ink at
     some fraction over --white; the dark value re-mixes the SAME fraction of
     the same ink over the dark card #241D15 — the relationship the mode tiers
     above already record. Each text value keeps its hue and is lifted until it
     clears AA on EVERY face it can land on (lifting against one face is how a
     first pass shipped five pairs at 3.9–4.4:1), with SATURATION CAPPED AT 0.50:
     #2A0000 and #001A10 are near-black with S = 1.00, and lifting those
     untouched gives a fire-engine red and a neon green, against a design north
     star that reads "never bright". The cap is the band the house's own dark
     accents already occupy (--sage 0.33, --mode-diagnostic 0.51).

     MEASURED, all three brands, both themes: every text-on-face pair clears
     4.5:1 (worst 4.50, TI arrow on its wash) and every border clears 3:1
     (worst 3.01, Casio fn-key edge). Gated by dark_mode_audit's contrast sweep
     and named in tests/test_dark_mode.py, so a brand that stops being measured
     fails there rather than in a student's eyes. */
  --gdc-ti-rule:       #4A7AAF;   --gdc-ti-wash:       #F0F4FA;
  --gdc-ti-deep:       #1B3A6A;   --gdc-ti-accent:     #1B3A6A;
  --gdc-ti-on-deep:    #C8DEFF;   --gdc-ti-key:        #E8EFF8;
  --gdc-ti-ink:        #0A1E3A;   --gdc-ti-key-fn:     #D0DDF0;
  --gdc-ti-fn-rule:    #2A5A9A;   --gdc-ti-fn-edge:    #0A2A6A;
  --gdc-ti-syntax:     #E0EBF8;   --gdc-ti-note:       #EAF1FB;
  --gdc-ti-note-ink:   #1A3A6A;   --gdc-ti-step-rule:  rgba(74,122,175,0.18);

  --gdc-casio-rule:      #9A3030; --gdc-casio-wash:      #FAF0F0;
  --gdc-casio-deep:      #6A1010; --gdc-casio-accent:    #6A1010;
  --gdc-casio-on-deep:   #FFCCCC; --gdc-casio-key:       #F8ECEC;
  --gdc-casio-ink:       #2A0000; --gdc-casio-key-fn:    #F0D8D8;
  --gdc-casio-fn-rule:   #7A2020; --gdc-casio-fn-edge:   #4A0808;
  --gdc-casio-syntax:    #F8E0E0; --gdc-casio-note:      #FBF0F0;
  --gdc-casio-note-ink:  #5A1010; --gdc-casio-step-rule: rgba(154,48,48,0.15);

  --gdc-hp-rule:       #2A7A64;   --gdc-hp-wash:       #F0FAF6;
  --gdc-hp-deep:       #0A4A38;   --gdc-hp-accent:     #0A4A38;
  --gdc-hp-on-deep:    #BBFFEE;   --gdc-hp-key:        #E4F5EE;
  --gdc-hp-ink:        #001A10;   --gdc-hp-key-fn:     #C8EAD8;
  --gdc-hp-fn-rule:    #1A6A4A;   --gdc-hp-fn-edge:    #083A28;
  --gdc-hp-syntax:     #D8F0E8;   --gdc-hp-note:       #E8F8F2;
  --gdc-hp-note-ink:   #0A3A28;   --gdc-hp-step-rule:  rgba(42,122,100,0.15);

  /* ── Type families ───────────────────────────────────────────────────────
     serif  = everything the student reads and reasons about (headings, body,
              questions, answers). EB Garamond, roman + italic.
     sans   = structural chrome ONLY (uppercase letter-spaced eyebrows, nav
              labels, badges). Never headings or content.
     mono   = raw numeric / statistical values (scores, seeds, timers, UIDs). */
  /* UI STANDARD v2's ONE RULE REWRITES THE TWO ROLES ABOVE, and the note there
     is now the OLD division — kept because it records what this product used to
     believe, not what it does.

       WAS   serif = everything the student reads and reasons about (headings,
             body, questions, answers); sans = structural chrome ONLY.
       v2    serif = HERO WORDS ONLY (greeting, page/card titles);
             sans  = EVERYTHING FUNCTIONAL (nav, eyebrows, body, buttons,
                     chips, labels).

     So the boundary moved in both directions at once: body copy leaves the
     serif, and the nav leaves the system stack for a named face. RUN 44 had
     moved the nav the other way — off 9px letter-spaced sans labels and onto
     EB Garamond 14.5px sentence case, calling them "the loudest remaining piece
     of web-app chrome in the app". v2 reverses that deliberately; the record of
     the reversal is in docs/design/UI-STANDARD-v2.md. */
  --serif: 'EB Garamond', 'Garamond', Georgia, serif;
  --sans:  'Inter', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
  --mono:  'Courier New', Courier, monospace;
  /* ── The display face — THE WORDMARK, AND NOTHING ELSE (v2, RUN 67) ──────
     Cinzel: inscribed caps matching the printed logo.

     IT HAS BEEN ROUND A FULL CIRCLE, AND THAT IS THE POINT OF THIS NOTE. It
     started as the wordmark face and nothing else. The Elite Design Standard
     gave it two more jobs — the eyebrow (10–11px, uppercase) and the page title
     (26–28px) — to get system-ui eyebrows off screens that were meant to read
     as printed paper. UI STANDARD v2's ONE RULE takes BOTH of those back: serif
     for hero words, sans for everything functional. Eyebrows, mode labels,
     button text, nav labels and the quiet strip are functional, so they are
     Inter; the page title is a hero word, so it is EB Garamond. Neither is
     Cinzel, and Cinzel is left with the one job it began with.

     THE ONE SURVIVING CONSUMER IS .cx-wordmark__2, VIA --font-wordmark. That is
     not an oversight to be tidied away later: v2 does not mention Cinzel at all,
     and the brand canon holds that the mark and wordmark come from the
     brand-assets folder and are never re-drawn or re-set. So the lockup is out
     of v2's SCOPE rather than retired by its silence. If a future edit finds
     --font-display on anything that is not the wordmark, that is the bug.

     WEIGHTS: 500 and 600 only. Every page requests Cinzel at 400;500;600, so
     700 would be synthesised — a smeared faux-bold, which is exactly the
     cheap tell this is meant to remove. Never set font-weight above 600 on
     --font-display. */
  --font-display:  'Cinzel', 'EB Garamond', Georgia, serif;
  --font-wordmark: var(--font-display);

  /* ── Type scale (pick from these — do not invent sizes) ───────────────────
     Page H1s are weight 400, not bold: the serif at size carries the weight,
     and an italic <em> word supplies emphasis. */
  --fs-eyebrow:   9px;    /* uppercase structural label */
  --fs-micro:     11px;   /* mono stats, tiny meta */
  --fs-small:     14px;   /* sub-labels, hints (often italic) */
  --fs-body:      17px;   /* primary reading text, question stems */
  --fs-body-lg:   18px;   /* base body on marketing surfaces */
  --fs-title:     21px;   /* card / step titles (weight 500) */
  --fs-h3:        24px;
  --fs-h2:        clamp(32px, 3.5vw, 46px);  /* section heading */
  --fs-h1:        clamp(42px, 5.5vw, 70px);  /* hero heading */
  --fs-stat:      52px;   /* big single-number displays */

  --lh-tight:  1.1;   /* headings */
  --lh-snug:   1.4;
  --lh-body:   1.7;   /* reading copy */
  --lh-loose:  1.9;   /* question text, generous math lines */

  --ls-eyebrow: 0.22em;  /* structural-label letter-spacing (0.18–0.28em range) */
  --ls-tight:  -0.02em;  /* large headings tuck in slightly */

  /* ── Spacing scale (hand-tuned, cluster on these) ─────────────────────── */
  --sp-1:  4px;    --sp-2:  8px;    --sp-3:  12px;   --sp-4:  16px;
  --sp-5:  24px;   --sp-6:  32px;   --sp-7:  40px;   --sp-8:  48px;
  --sp-9:  64px;   --sp-10: 80px;   --sp-11: 120px;  /* section top/bottom */

  /* ── Physical constants ──────────────────────────────────────────────────
     Motion is opacity/position only, quick and quiet.

     RADIUS: THE STANDARD MOVED, AND THIS IS THE RECORD OF IT. The line that
     stood here said "radius never exceeds 2px — an examination-paper
     aesthetic, not a consumer app", and every component in this sheet was
     built to it. The founder-approved Elite Design Standard (2026-08-11)
     supersedes that with a THREE-LEVEL ELEVATION LADDER, and the reasoning is
     not "rounder is nicer": a hairline card at 2px reads as a table cell, and
     the standard's card is a piece of paper laid on the page. The radius is
     what separates the two, and it is the only thing that does — there is
     still no shadow on a card and no border above 1px.

       page   nothing at all
       card   1px hairline · --radius-card · NO shadow
       frame  --radius-frame · --shadow-frame · ONCE per screen

     --radius STAYS at 2px and keeps every component that has not yet been
     rebuilt to the standard. It is not a second system: it is the un-migrated
     half of this one, and it shrinks with each phase. When the last component
     moves, --radius goes with it. */
  --radius:      2px;
  --radius-card:  11px;   /* card / panel / option — the standard's 10–12 */
  --radius-frame: 16px;   /* the app frame and any overlay */
  --radius-btn:    7px;   /* the ink fill-button */

  /* ── Border weight ───────────────────────────────────────────────────────
     UNMANAGED UNTIL RUN 65, and the audit is what made that visible: 383
     border declarations across SIX distinct widths — 0.5, 1, 1.5, 2, 3, 4 —
     and not one token among them.

     Read the distribution before reading the founder's word "thick": 313 of
     the 383 are ALREADY a correct 1px hairline. The product is not uniformly
     heavy, which is why the fix is not a global thinning. It is heavy in
     places, and there was no single line to move — so "hairline everywhere"
     meant 383 hand-edits with nothing able to keep it done.

     Three names. The third is the only one permitted to be wide, and only on
     a LEFT edge: that is the Elite Design Standard's single exception to
     "sage is never a fill", and tests/test_no_accent_band.py exists precisely
     to hold the line between a 4px left border and a 4px band.

       --bw-hair       1px   every resting edge: card, panel, option, divider
       --bw-mark       2px   a STATE mark on an edge: active underline, card bar
       --bw-edge       3px   the accent LEFT border, and only a left border
       --bw-edge-focal 4px   the ONE focal card's left edge

     THE LAST TWO ARE TWO NUMBERS FOR ONE IDEA, AND THAT IS RECORDED RATHER
     THAN QUIETLY RESOLVED. 3px is what `.cx-mode-card` and `.cx-explain__body`
     have carried since before the standard; 4px is what the approved mockup
     gives the Now card (docs/design/onepage-app.html: `border-left: 4px solid
     var(--sage)`). Collapsing them to one width is a visible change to one of
     the two, which is the founder's call and not a token refactor's. Both are
     named so the choice is a one-line edit when it is made — the point of the
     scale is that the drift is now VISIBLE, not that it is already settled.

     0.5px IS NOT A THINNER HAIRLINE. It is a border that renders at DPR 2 and
     disappears at DPR 1 — an edge that exists on the designer's screen and
     not on half the students'. The twelve declarations carrying it resolve UP
     to --bw-hair as their pages migrate. That is a repair, not a thickening,
     and it is the one place where "less thick" and "correct" point opposite
     ways. */
  --bw-hair: 1px;
  --bw-mark: 2px;
  --bw-edge: 3px;
  --bw-edge-focal: 4px;

  /* ── The canvas ──────────────────────────────────────────────────────────
     THE SHELL HANDED A PAGE A BAR AND NO CANVAS (RUN 65, RC6). `.cx-has-nav`
     sets `padding-top: var(--nav-h)` — exactly enough to clear the fixed bar
     and not one pixel beyond it — and `.cx-container` carried no vertical
     padding at all. So every screen invented its own: 820px on four of them,
     940px on /session, 48px of top air on four, 56px on one, against the
     approved mockup's 1040px and 60px.

     The air was never missing. It was UNOWNED, which is why it drifted, and
     why a token change alone could not have reached it — there was nothing
     for a token to be spent by.

     These are the mockup's own numbers (docs/design/onepage-app.html: the
     frame is 1040px, the canvas is `padding: 60px 64px 66px`). */
  --canvas-w:      1040px;  /* the reading width of a destination */
  --canvas-pad-y:    60px;  /* air between the spine and the first thing said */
  --canvas-pad-x:    64px;

  /* ONE shadow, once per screen, and only on a frame or an overlay. A shadow
     on every card is on the standard's forbidden list — it is the fastest way
     to make a page look like a template. */
  --shadow-frame: 0 18px 40px -24px rgba(26,20,16,0.45);
  --container:   1100px;   /* standard centred max-width */
  --container-narrow: 680px;
  /* 60px, per the Elite Design Standard's persistent frame. It was 72. Every
     consumer reads the token — .cx-has-nav's top padding, exam_mode's chrome
     strip, student_session's sticky header — so the whole product moves with
     this one line, which is exactly why the focus strip's collapse to 52 is
     also written as a token override rather than a second height. */
  --nav-h:       60px;
  --t-fast:  0.15s;
  --t-base:  0.25s ease;
  --t-slow:  0.55s ease;   /* section fade-in reveal */

  /* ── Legacy dark-surface names, now ALIASES ──────────────────────────────
     These five were literal cold greys (#1C1C1E, #2C2C2E, #3A3A3C, #E8E0D0,
     #48484A) — a separate, unmanaged second palette that only the .lc-* layer
     and ui/profile.html's page-local rules ever used. They are now aliases onto
     the real system, so the legacy surfaces and the token-driven ones cannot
     drift apart, and the warm dark palette reaches them for free.

     They resolve to LIGHT values in light mode, which is correct and harmless:
     every rule that reads them is already inside a [data-theme="dark"] selector,
     so they are only ever rendered under the dark block below. Do not consume
     them in new work — use the real tokens. */
  --bg-dark:        var(--cream);      /* page under the dark preference */
  --surface-dark:   var(--white);      /* cards, nav, inputs on dark */
  --surface-dark-2: var(--cream-dd);   /* raised strip: topbar, section head */
  --ink-dark:       var(--ink);        /* body text on dark */
  --rule-dark:      var(--rule);       /* hairline on dark */

  /* ── Legacy .lc-* layer constants ────────────────────────────────────────
     Two values the ported .lc-* components use that have no canonical
     equivalent. Kept at their shipped values so the port changes no render;
     they retire with the last .lc-* class. Do not consume these in new work. */
  --lc-cream-dark: #EDE8DF;   /* .lc-answer-option.selected wash */
  --lc-sepia:      #C4A882;   /* .lc-dot.done */

  /* ═══ UI STANDARD v3 — "ATELIER HERITAGE" (RUN 76) ══════════════════════
     docs/design/UI-STANDARD-v3.md is the spec. These are NAMESPACED --v3-*
     and NOTHING existing consumes them.

     WHY NAMESPACED RATHER THAN A REDEFINITION OF --bg/--card/--ink. v3's
     surfaces differ from v2's (--bg #FAF7F1 against v2's #F7F4EF), so
     redefining the canonical names would restyle all 44 shipped pages in the
     same commit that introduces the first v3 screen — a global re-theme
     smuggled in under a new-screen change, and it would move every number the
     contrast and dark-mode gates have recorded. The founder's instruction is
     to see the shell BEFORE anything else is rebuilt. When the rollout is
     approved, --bg is re-pointed at --v3-bg and the namespace retires.

     Every one of these has a dark value below; scripts/dark_mode_audit.py
     fails if a :root colour is not overridden, so v3's both-themes law is
     enforced by a gate that already existed.

     MEASURED BEFORE ADOPTION, both themes, every text tone on every surface it
     can land on — the full table is §3 of the spec. One value is NOT as the
     founder issued it, and that is flagged rather than applied silently:

         LIGHT --v3-taupe   spec #9A8E7E   3.00/3.21/3.21/2.73 on
                                           --bg/--card/--nav/--chip
                            SHIPPED #877B6B  3.87/4.14/4.14/3.51

     #9A8E7E is the exact hex the founder ruled AGAINST in UI-STANDARD-v2
     AMENDMENT 1 (RUN 67) on identical grounds; the v3 block appears to restate
     v2's pre-amendment values. #877B6B is that standing ruling, not a value an
     agent invented, and v3 does not repeal it. It is one line to revert.
     Spec §7 carries the open question, including the fact that taupe meta is
     "large / UI only" in BOTH themes even at #877B6B. */
  /* ── THE V3 TYPEFACES (RUN 100) ────────────────────────────────────────
     design-system/APP-SPEC.html sets the app in Fraunces (display serif) and
     Libre Franklin (UI sans). Both are vendored under static/vendor/fonts by
     scripts/vendor_fonts.py and served from our own origin; no page asks a
     third party for a face.

     THEY ARE --v3-* AND NOT --serif/--sans ON PURPOSE. Those two tokens set
     the type of the WHOLE product, marketing pages included, and this run's
     scope is the logged-in app. Re-pointing them would have re-set the public
     face as a side effect of an app rebuild — a change nobody asked for,
     landing on the surface with the widest audience.

     THE FALLBACK STACKS ARE NOT DECORATION. Fraunces falls back through
     Georgia to the generic serif, so scripts/v2_type_law.py's face pattern
     still reads this token as A SERIF if it ever reaches a scanned file, and
     the ONE RULE keeps applying to it. Libre Franklin's stack ends in
     sans-serif, which that pattern's lookbehind correctly excludes. */
  /* ── STANDARD v1 §1 · THREE FACES, EACH WITH ITS OWN ROLE (RUN 114) ──
     docs/design/codex-logged-in-client-standard-v1.md: "EB Garamond = hero
     words, titles, and maths notation (italic). Inter = all functional/UI
     text. Cinzel = wordmark + small-cap eyebrow labels only."

     THIS REPLACES FRAUNCES + LIBRE FRANKLIN, which design-system/APP-SPEC.html
     set and RUN 100 adopted. Standard v1 is founder-locked and later, and it
     is the single source the logged-in client is now built to. All three faces
     are already vendored under static/vendor/fonts and served from our own
     origin; nothing here asks a third party for a face.

     --v3-smallcap IS NEW, AND IT CLOSES A LIVE DEFECT. `.v3-wordmark` asked
     for `var(--v3-display, Georgia, serif)` and --v3-display IS DEFINED
     NOWHERE IN static/ — so the brand lockup has been rendering in GEORGIA on
     every v3 surface. tests/test_type_law_rendered.py could not see it: its
     SERIF pattern matches `georgia` as well as `cinzel`, and the lockup is on
     its permitted list, so the wrong face was permitted for being a face at
     all. Named, not silently patched, because the gate agreeing with the bug
     is the interesting half.

     THE FALLBACK STACKS ARE NOT DECORATION, and the reason is unchanged from
     the Fraunces note this replaces: EB Garamond falls back through Georgia to
     the generic serif, so scripts/v2_type_law.py's face pattern still reads
     --v3-serif as A SERIF if it ever reaches a scanned file. Inter's stack ends
     in sans-serif, which that pattern's lookbehind correctly excludes. */
  --v3-serif: 'EB Garamond', Georgia, 'Times New Roman', serif;
  --v3-sans:  'Inter', ui-sans-serif, system-ui, -apple-system,
              'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --v3-smallcap: 'Cinzel', Georgia, serif;

  /* ── STANDARD v1 §1 · THE LOCKED LIGHT PALETTE (RUN 114 UNIT 1) ───────
     Every value below is the Standard's own hex, with ONE exception, which is
     --v3-taupe and is stated on its own line. Measured through
     scripts/contrast_audit.contrast_ratio against the Standard's own surfaces
     (--v3-bg, --v3-card, --v3-nav, --v3-chip) BEFORE adoption — worst ratio
     per token: ink 15.38, ink2 6.01, signature 5.43, signature-ink 7.62,
     study 4.78, controle 6.26, button text 18.10. All clear AA-small. */
  --v3-bg:        #FAF7F1;
  --v3-card:      #FFFFFF;
  --v3-nav:       #FFFFFF;
  --v3-ink:       #1A1512;
  --v3-ink2:      #5F5750;
  /* THE ONE VALUE THAT IS NOT THE STANDARD'S, AND IT IS THE SAME ONE AS
     BEFORE. Standard v1 issues --taupe:#9A8E7E and the locked mock uses it as
     the COLOUR OF TEXT — every Cinzel eyebrow, every small-cap key, every stat
     label. On the Standard's own new surfaces it measures 3.00/3.21/3.21/2.73
     on --bg/--card/--nav/--chip: under the AA-small floor that §1 of the same
     document requires, and under the 3.0 non-text floor on --chip.

     #9A8E7E is the exact hex the founder ruled AGAINST in UI-STANDARD-v2
     Amendment 1 (RUN 67) on identical grounds, and Law K5 then went further
     and made taupe a non-text MARK, with test_v3_standard gate 2 failing the
     build on taupe reaching `color`. Standard v1 restates the pre-amendment
     value; it does not repeal the amendment, and this run does not guess that
     it meant to. The standing ruling stands, and the mock's taupe-coloured
     labels are set in --v3-ink3 (which measures 4.90 worst on the new ground).

     THE DIRECTOR RULED ON THIS, 4 SEP, AND #877B6B STANDS. Standard v1 §1's
     token line is amended to #877B6B and §5 records the reason in one
     sentence: "Where the mock and accessibility disagree, accessibility wins."
     So this value is no longer a standing ruling being held against a newer
     document — it is the newer document. */
  --v3-taupe:     #877B6B;  /* Standard v1 says #9A8E7E — see the note above */
  --v3-hair:      #EAE3D8;
  --v3-chip:      #F1ECE2;
  --v3-signature: #2E6A4E;
  /* The ONLY permitted tinted fill, and only behind the one focus card
     (Law C3). The spec gives this value for LIGHT only. */
  --v3-signature-wash: rgba(46,106,78,.10);
  /* ── THE OTHER CORRECTNESS WASH (RUN 114 UNIT 4) ──────────────────────
     Standard v1 §1: "earned/correct = --signature-wash fill + --signature-ink
     text; missed/incorrect = claret wash rgba(140,59,78,.10) + #8C3B4E text.
     Quiet, never loud." The earned half already existed; this is the other, at
     the Standard's exact value, and it is a TOKEN because gate 1 of
     tests/test_v3_standard.py forbids a literal colour on a v3 surface.

     MEASURED BEFORE ADOPTION, like everything else in this block: the claret
     ink #8C3B4E on this wash, composited, is 5.92:1 over --v3-bg and 6.31:1
     over --v3-card. Both clear AA-small. */
  --v3-missed-wash: rgba(140,59,78,.10);
  /* THE OVERLAY SCRIM. Not a colour the spec issues, and it could not be
     omitted: the Method and GDC overlays need a ground between the panel and
     the page or the question reads THROUGH the panel. Derived mechanically
     from --v3-ink, the same way --v3-signature-wash's dark value is derived
     from the dark signature, and stated here rather than written inline as a
     literal, which gate 1 forbids on a v3 surface. */
  --v3-scrim: rgba(26,21,18,.42);
  /* ── THE RADIUS SCALE (RUN 116 Track C) ────────────────────────────────
     Standard v1 §1: "Cards: --card, 1px --hair, radius 12-14". These are the
     steps the founder-locked mock actually draws, named rather than invented:
     13px on cards and panels, 10px on a selectable tile inside one, 9px on a
     control, 4px on a focus ring, and a lozenge for a chip.

     THEY EXIST BECAUSE A PAGE MAY NOT WRITE ITS OWN. tests/
     test_design_system_rollout.py's radius check says so in as many words --
     "larger corners come from the token file" -- and it goes red on any
     literal above 2px in a tracked page. ui/v3's screens already comply by
     having no page-local CSS at all; the two full-screen flows outside ui/v3
     (onboarding and the setup walkthrough) do have page-local CSS, and these
     are what it points at.

     NOT ALIASED ONTO --radius-card (11px). That token is v2's standard of
     10-12 and is consumed by the v2 component sheet; pointing the client's
     cards at it would move every v2 panel the day Standard v1's range moves,
     which is the coupling the --v3-* namespace exists to avoid. */
  --v3-radius-card: 13px;
  --v3-radius-tile: 10px;
  --v3-radius-ctl:   9px;
  --v3-radius-sm:    4px;
  --v3-radius-pill: 100px;

  /* ── THE APP-SPEC ROLES THE v3 PALETTE HAD NO TOKEN FOR (RUN 100) ───────
     design-system/APP-SPEC.html, transcribed. Gate 1 forbids a literal colour
     anywhere on a v3 surface, so every one of these has to exist here before
     the component sheet can use it.

     --v3-ink3 IS THE ONE VALUE THAT IS NOT THE SPEC'S, AND IT IS A LIGHTNESS
     STEP ON THE SPEC'S OWN HUE. The spec puts --muted (#8A8069) on .eyebrow,
     .micro, .reason, .stat .k, .panel .sub, .metaitem .k and .todo .td —
     captions a student READS — and it measures 3.06:1 on --bg, under the AA
     floor. --v3-taupe cannot take that role either: Law K5 makes taupe a
     non-text mark and gate 2 refuses it on `color`. So the meta zone gets its
     own TEXT token, as dark as the floor requires and no darker: 4.52:1 on
     --v3-bg, 5.04 on --v3-card, 4.64 on --v3-chip. The DARK value is the
     spec's #8E8570 unchanged — it already measures 4.62:1.

     --v3-brass IS A MARK, NOT TEXT, and that is measured rather than assumed:
     #9A7B3F is 3.12:1 on --v3-bg. It draws the crest stroke, the avatar ring,
     the current-question dot and the focus card's edge gradient. Where the
     spec puts brass on a NUMERAL (.todo .n) the component sheet uses --v3-ink2
     for the glyph and keeps brass for the ring around it. */
  /* RE-DERIVED ONTO STANDARD v1's GROUND (RUN 114 UNIT 1). These four are
     roles APP-SPEC.html had and the Standard's token list does not name, so
     they cannot simply be copied across: leaving them at values derived from
     a #EBE3D2 page on a #FAF7F1 page is how a rebuilt screen ends up with an
     inset that reads darker than the card it sits in. Each is derived rather
     than chosen, and the derivation is written down:

       --v3-surface2   IS the Standard's --panel #FCFBF8. Same role (the
                       recessed surface inside a white card: the mock's
                       .stem-head, .m-cell and .contract all use --panel), so
                       the Standard's own value takes it and UNIT 4's facing
                       columns need no further token.
       --v3-hair2      the old pair's own lightness step, applied to the new
                       hairline: #D6CBB2 -> #C4B896 is x0.916/0.906/0.843 per
                       channel, and #EAE3D8 through that is #D6CEB6.
       --v3-brass      unchanged, and re-measured rather than assumed: 3.38:1
                       worst (on --chip) against the 3.0 non-text floor. It is
                       a mark — the crest stroke, the avatar ring, the
                       current-question dot — and never text.
       --v3-accent-hover  the mock's own `.btn:hover{background:#000}`. The
                       primary is ink now, not forest, so the old forest hover
                       #173628 would have been a green flash under a black
                       button. The `.go` action's hover is --v3-signature-ink,
                       which is the mock's rule too and needs no token here.
       --v3-shadow*    the Standard's own shadow, cast from --ink's channels
                       instead of the old palette's. */
  --v3-ink3:       #6D6553;
  --v3-surface2:   #FCFBF8;
  --v3-hair2:      #D6CEB6;
  --v3-brass:      #9A7B3F;
  --v3-brass-soft: #B79A5E;
  --v3-accent-hover: #000000;
  --v3-shadow:      0 1px 2px rgba(26,21,18,.04), 0 12px 32px -18px rgba(26,21,18,.22);
  --v3-shadow-sm:   0 1px 1px rgba(26,21,18,.05);
  --v3-shadow-lift: 0 1px 2px rgba(26,21,18,.05), 0 18px 40px -22px rgba(26,21,18,.26);


  /* ── THE "-INK" FAMILY (RUN 78 Phase 3) ────────────────────────────────
     DARK-TONE VARIANTS OF THE MODE COLOURS, FOR LABEL TEXT ON A TINT.
     The approved Work mockup labels every outcome row with the mode's name on
     a 12% tint of that mode's colour. The mode colour cannot BE that text:
     #BE842A measures 3.22:1 on white, and Law C1 forbids it on a text property
     outright. These are the darkened variants the mockups use, and
     codex-visual-system-heritage.md names the first of them --signature-ink.

     MEASURED on every surface a label can land on -- the 12% tint, --card,
     --bg and --chip. Worst case per token:

         diagnostic-ink  7.55    study-ink   6.96
         controle-ink    7.33    drill-ink   4.92

     All four clear AA-small. Amber is the tight one, at 4.92 on --chip, and it
     is the whole reason the family exists. */
  /* RE-MEASURED ON THE NEW HUES (RUN 114 UNIT 1). Study is petrol now and Le
     Controle is claret, so a green study-ink and a sienna controle-ink would
     have been the wrong HUE for their own tint, not merely the wrong step.
     Worst ratio, over the 12% tint of the mode's own colour and over
     --v3-bg / --v3-card / --v3-chip / --v3-surface2:

       signature-ink / diagnostic-ink  #245239   7.09   (the Standard's own)
       study-ink                       #265E7A   5.65   derived: #2C6E8F is
                                                        4.49 on its own tint,
                                                        so one step darker
       drill-ink                       #8A5C17   4.82   unchanged
       controle-ink                    #8C3B4E   5.72   (the Standard's own
                                                        claret; it clears the
                                                        floor unaided) */
  --v3-signature-ink:      #245239;
  --v3-mode-diagnostic-ink: #245239;
  --v3-mode-study-ink:      #265E7A;
  --v3-mode-drill-ink:      #8A5C17;
  --v3-mode-controle-ink:   #8C3B4E;

  /* THE MODE CHIP'S FILL, OPAQUE AND PRE-COMPOSITED. The mockups write these
     as rgba tints at .12 (.14 for drill) over --card. Kept at exactly those
     alphas and those bases, but resolved to an opaque hex, because a
     TRANSLUCENT fill has no fixed contrast -- it has whatever the thing behind
     it leaves. Measured: the label cleared AA over --card and --bg and FAILED
     over --chip (light drill 4.33:1, dark controle 3.89:1). Opaque makes the
     pair one number a gate can hold. */

  /* ── GDC DEVICE SKINS (RUN 78 Phase 3) ─────────────────────────────────
     THE ONE TOKEN FAMILY THAT DOES NOT FLIP BETWEEN THEMES, and that is the
     point rather than an oversight: these depict physical calculators. A
     TI-Nspire is charcoal with a cool-blue menu key on any desk in any light,
     and a skin that turned cream in light mode would stop being a picture of
     the device in the student's bag.

     Declared identically in the dark block below so the dark-mode audit sees
     an explicit value rather than an omission. Values read from the approved
     docs/design/mockups/gdc.html. */
  --v3-dev-ti-body:      #3A3F45;
  --v3-dev-ti-name:      #C9CDD2;
  --v3-dev-ti-pad:       #2C3036;
  --v3-dev-ti-bezel:     #23272C;
  --v3-dev-ti-screen:    #0E1B2A;
  --v3-dev-ti-key:       #4B5158;
  --v3-dev-ti-keyink:    #CFD3D8;
  --v3-dev-ti-accent:    #4A78B8;
  --v3-dev-ti-accentink: #FFFFFF;
  --v3-dev-ti-enter:     #8FCE9F;
  --v3-dev-ti-enterink:  #112233;

  --v3-dev-casio-body:      #E9ECEF;
  --v3-dev-casio-name:      #5B636D;
  --v3-dev-casio-bezel:     #C8CCD0;
  --v3-dev-casio-screen:    #E9F1E6;
  --v3-dev-casio-key:       #D3D7DB;
  --v3-dev-casio-keyink:    #333333;
  --v3-dev-casio-accent:    #F3A13A;
  --v3-dev-casio-accentink: #2A1B06;
  --v3-dev-casio-enter:     #2E5FA3;
  --v3-dev-casio-enterink:  #FFFFFF;
  --v3-dev-casio-fkey:      #2E5FA3;
  --v3-dev-casio-fkeyink:   #FFFFFF;

  --v3-dev-hp-body:      #17191C;
  --v3-dev-hp-name:      #B9BDC2;
  --v3-dev-hp-bezel:     #000000;
  --v3-dev-hp-screen:    #0C0F14;
  --v3-dev-hp-key:       #2A2E33;
  --v3-dev-hp-keyink:    #CFD3D8;
  --v3-dev-hp-accent:    #E08A2C;
  --v3-dev-hp-accentink: #1A1A1A;
  --v3-dev-hp-enter:     #3A6F4E;
  --v3-dev-hp-enterink:  #FFFFFF;

  --v3-mode-diagnostic-chip:#DFE7E1;
  --v3-mode-study-chip:     #DFE4D2;
  --v3-mode-drill-chip:     #F0E6C4;
  --v3-mode-controle-chip:  #F1DBD2;
  /* STANDARD v1: "Buttons: ink solid primary (--btn/--btntext)". The primary
     was the forest and is now the ink, which is also why --v3-accent-hover
     moved to black above. Measured: 18.10:1. */
  --v3-btn:       #1A1512;
  --v3-btntext:   #FFFFFF;

  /* The four mode colours. IDENTITY AND WAYFINDING, never a fill (Law C3).
     --v3-mode-drill is the AMBER RULE's subject: 3.01:1 on --v3-bg, so it is
     permitted only as an accent edge, a node ring or a dot, and
     tests/test_v3_amber_never_text.py fails the suite if it reaches a text
     property. */
  /* STANDARD v1 §1, the four mode colours as issued: "Diagnostic forest
     #2E6A4E · Study petrol #2C6E8F · Drill amber #BE842A · Le Controle claret
     #8C3B4E". Study moves from a sage green to petrol blue and Le Controle
     from sienna to claret; both are the Standard's values, not adjustments.
     Amber is unchanged and is still an edge/dot only — it measures 2.73:1 on
     --v3-chip, so a mode mark is never placed on a chip. */
  --v3-mode-diagnostic: #2E6A4E;
  --v3-mode-study:      #2C6E8F;
  --v3-mode-drill:      #BE842A;
  --v3-mode-controle:   #8C3B4E;
}


/* ═══════════════════════════════════════════════════════════════════════════
   WARM DARK MODE — the [data-theme="dark"] override
   Built to docs/UI_DARK_MODE.html. Reading by lamplight: espresso, never black;
   candle-white, never glare. Cards lift by WARMTH, not brightness.

   THIS BLOCK IS THE WHOLE FEATURE. Not one component is restyled — every screen
   already reads these tokens (`body { background: var(--cream); color:
   var(--ink) }`), so overriding the vocabulary re-themes all 43 pages at once.
   That is the dividend of Phase A putting everything on one system: before it,
   this would have been a rewrite of two component libraries and 35 page-local
   :root blocks.

   MUST STAY AFTER :root. `[data-theme="dark"]` and `:root` have equal
   specificity (0,1,0), so source order is what makes this win. Moving it above
   :root silently disables dark mode.

   THE NAMES DIFFER FROM THE BRIEF, which said to map them. The brief's --paper
   (card) is this system's --white; its --ink2/--ink3/--ink4 are --ink-2/-3/-4;
   its --correct-wash/--wrong-wash are --correct-bg/--wrong-bg. Its --rule2 has
   no counterpart here and was dropped rather than introduced as a token with
   one consumer.

   EVERY LITERAL COLOUR IN :root HAS A VALUE HERE. That is enforced, not
   promised: scripts/dark_mode_audit.py fails if a colour token is defined in
   :root and not overridden, so the next token added cannot quietly ship a
   light-mode value onto a dark page. Tokens defined as var() references
   (--mode-accent, --mode-rule, --mode-wash, and the five legacy aliases above)
   are deliberately NOT repeated — they follow whatever they point at.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ══ THE SAME DARK PALETTE, FOR A MACHINE THAT ASKED FOR IT (RUN 106 UNIT 2) ══
   This file had ONE dark block - `[data-theme="dark"]` - and no media state.
   So a student whose operating system is set to dark, and who has never opened
   the theme control, got the LIGHT app. RUN 105 measured 11 sections answering
   the toggle and ignoring the system setting; every one was v3 app surface.

   GENERATED FROM THE BLOCK BELOW, NOT TYPED. It is ~360 declarations, and a
   hand-copied second copy is a drift waiting to happen - which is not
   hypothetical: UNIT 0 of this same run had to repair four tokens that had
   quietly diverged between APP-SPEC's two dark paths, so that a dark-OS viewer
   and a toggled-dark viewer saw a different red and gold.
   tests/test_app_dark_paths_agree.py asserts these two stay identical.

   THE :not([data-theme="light"]) GUARD IS THE LOAD-BEARING PART. Without it, a
   student who has explicitly chosen LIGHT on a dark-set machine is overruled by
   their operating system - the one outcome an explicit control must never
   produce. cx_prefs.js has written data-theme="light" for precisely this case
   since RUN 105. ══════════════════════════════════════════════════════════ */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {

    /* ── Surfaces. The ramp INVERTS: in light, --cream-d/-dd are progressively
       DEEPER washes below the page; in dark, a recessed or raised area reads by
       getting lighter, so they step UP from the page. --white keeps its meaning
       — "the card surface" — and stops being white. */
    /* ═══ UI STANDARD v2 — THE DARK PALETTE (RUN 66) ═══════════════════════
       The eleven canonical names, dark values. Every alias below follows them,
       so the two themes stay one system rather than two.

       MEASURED: --taupe clears 4.07-4.70:1 on all four dark surfaces, so the
       light-mode exception recorded in :root does NOT apply here. --ink 15.74 on
       --bg, --ink2 7.98, --green 7.55. Nothing in the dark set is short. */
    --bg:        #15120E;
    --navbg:     #1A1610;
    --card:      #201B15;
    --ink2:      #B4A895;
    --taupe:     #8A7E6C;
    --green:     #7FB08C;
    --hair:      #352E25;
    --chip:      #252017;
    --btn:       #F1EBE0;
    --btntext:   #17130E;
    --shadow:    0 20px 44px -26px rgba(0,0,0,.6);
    --shadow2:   0 10px 22px -16px rgba(0,0,0,.5);

    --cream:     var(--bg);      /* page — warm espresso, never black */
    --cream-d:   #201A13;   /* recessed section / selected-row wash */
    --white:     var(--card);    /* card / panel / option surface */
    --cream-dd:  #2A2219;   /* highest surface, used sparingly */

    /* ── Ink ramp. --ink-1 is documented above as a "secondary dark surface" but
       is used in practice as body TEXT (the /content guides, the admin table), so
       it takes a light value one step below --ink rather than a surface value.
       Measured on the page: ink 15.05:1, ink-1 12.71:1, ink-2 9.58:1, ink-3
       5.77:1. */
    --ink:       #F1EBE0;   /* primary text — v2 (was #EFE7D8) */
    --ink-1:     #DFD5C2;
    --ink-2:     var(--ink2);   /* body copy */
    /* SMALL-TEXT SAFE IN DARK ALREADY — no retune was needed here. RUN 46 C1
       moved the LIGHT --ink-3 to 4.52:1; measured on the four dark surfaces this
       value is 5.77 page / 5.20 card / 5.38 recessed / 4.89 highest, so it
       clears AA-small everywhere it can land. The light and dark tokens now
       carry the same role, which they did not before: the note here used to say
       "LARGE/UI only" while the value was already good for body. */
    --ink-3:     #9C8E78;   /* labels, meta, small text (AA-small) */
    /* THE ALIAS THE FOUNDER'S RULING FORBIDS — FOUND HERE IN DARK, RUN 67.
       This token read `var(--taupe)`. The comment above it described a computed
       value and the code underneath had been replaced by an alias, so the file
       documented one thing and shipped another; the light block one screen up
       recorded the alias being caught and reverted, and nobody looked at the dark
       block, where the identical edit had survived.

       It was not a CONTRAST bug — dark --taupe is 4.70:1 and would have passed
       every gate in the repo, which is exactly why it lasted. It collapsed the
       four-tone scale into three in one theme only, so the dark ramp had a step
       the light ramp had and the two themes stopped being one system.

       RESTORED BY THE RECORDED METHOD, not by copying the recorded number: the
       MINIMUM lift of the brief's #6E6455 that reaches 3:1 on BOTH --bg and
       --card, hue and saturation held. That is #706657 — 3.31:1 page / 3.03:1
       card / 3.06:1 recessed. The old comment claimed 3.38 / 3.04 for a hex it no
       longer named; these four numbers are measured from the value now beside
       them. Still LARGE/UI only. */
    --ink-4:     #706657;   /* quaternary — LARGE TEXT + DECORATION ONLY */

    /* Decorative hairline, same exemption as light (1.42:1 on the page). Nothing
       may depend on it alone to be understood — that condition is unchanged. */
    --rule:      var(--hair);

    /* ── Command surfaces STAY DARK. This is the pair of tokens that makes the
       theme flip safe: the nav, primary buttons and dark panels lift slightly off
       the page instead of inverting into glare. --command sits one step above the
       page (#17130E → #241D15) so a fixed topbar still reads as a distinct band,
       and --command-2 one further for its hover and secondary panels.
       --on-command becomes candle-white: 13.55:1 on --command. */
    --command:     var(--btn);
    --command-2:   #2A2219;
    --on-command:  var(--btntext);
    /* The ramp inverts with the surface: --command is CREAM here, so these are
       dark. Same roles, same names, opposite end of the scale — which is the
       whole point of them being tokens. */
    --on-command-2:    #605C56;
    --on-command-rule: #CAC5BC;

    /* The delta pair INVERTS with the surface it sits on — that is the whole
       point of it existing. On the dark theme the hero is cream, so the numbers
       go dark: 8.62:1 and 10.01:1, against 1.36 and 2.45 before. */
    --on-command-up:   #1A4A28;
    --on-command-down: #6B1818;

    /* The band scale lightens for the dark page; the ORDER is preserved, which is
       what a scale has to keep.

       RUN 70: the four started bands are now the dark ink ramp, for the reason
       written out in the light block. The note that used to stand here — that
       --band-3 was kept off #7FB08C because "that is dark --green exactly, and
       the colour-law scanner computes its green family BY VALUE, so the legend
       dot registered as an accent fill. A data scale must not collide with the
       accent." — is kept, because it is the whole argument, and because it was
       applied to the green collision and never to the two the accent law cares
       about: --band-1 WAS dark --wrong exactly, and --band-4 WAS dark --correct
       exactly. Aliasing onto the ink ramp settles all three at once. */
    --band-0: #6E655A;         /* not started — dashed OUTLINE only, never a fill */
    --band-1: var(--ink-4);
    --band-2: var(--ink-3);
    --band-3: var(--ink-2);
    --band-4: var(--ink);

    --scrim: rgba(0,0,0,0.72);

    /* THE on-deep RAMP IS RESTATED HERE WITH IDENTICAL VALUES, ON PURPOSE.
       The GDC deep surfaces do not invert — they are calculator brand colours —
       so the text on them must not invert either. Repeating the values rather
       than omitting them is the convention --gdc-ti-deep and --gdc-casio-deep
       already follow a few lines below: scripts/dark_mode_audit.py's
       check_coverage asks "what did the dark block override", exempting only
       ALIASES, so a theme-stable literal answers that question by being restated.
       Omitting them would read as an oversight and fail the audit; giving them
       different dark values would break the surface they sit on. */
    --on-deep:      #FFFFFF;
    --on-deep-2:    rgba(255,255,255,0.80);
    --on-deep-3:    rgba(255,255,255,0.55);
    --on-deep-edge: rgba(255,255,255,0.22);
    --on-deep-lift: rgba(255,255,255,0.18);

    /* ── Semantic states. The washes become solid warm tints rather than the
       light mode's 7% alpha: an alpha wash over an espresso page turns muddy and
       loses the state. Measured — correct 7.91:1 on page and 6.24:1 on its own
       wash; wrong 6.61:1 and 5.70:1; amber 7.60:1 and 6.20:1. */
    --correct:    #6FB98A;  --correct-bg: #1E2C22;
    --correct-bg-2: #24382A;
    --wrong:      #D98476;  --wrong-bg:   #2E1E1B;
    --amber:      #CE9E52;  --amber-bg:   #2E2517;
    /* The page IS the dark surface now, so this collapses onto --correct rather
       than staying a second, lighter green nobody tuned. */
    --correct-on-dark: var(--correct);

    /* Text on a semantic fill flips WITH the fill. The three states above are
       deep in light and light in dark, so what sits on them goes the other way:
       #FDFAF5 becomes the page's own espresso. Measured on the dark fills —
       correct 7.76:1, wrong 6.44:1, amber 7.53:1. */
    --on-fill:    #17130E;

    /* ── Mode accents. Each lifts to stay legible and KEEPS ITS HUE — sage is
       still sage. On the page: study 7.28:1, diagnostic 7.46:1, drill 6.70:1,
       exam 7.26:1 — all clear 4.5:1 for text and 3:1 for UI.
       --sage is overridden here rather than --mode-study, which is now an alias
       onto it: overriding the alias would have broken the single-accent link
       the light block establishes, and the dark study accent WAS this value. */
    --sage:            var(--green);  /* structural accent — v2 --green */
    /* Same ruling as the light block: one accent, one colour. */
    --mode-diagnostic: var(--sage);   /* was #7FA8D6 */
    --mode-drill:      var(--sage);   /* was #CE87A2 */
    /* --sienna, like --sage, is overridden here rather than --mode-exam, which
       is now an alias onto it. The value is the dark exam accent unchanged. */
    --sienna:          #D8926B;   /* burnt sienna — 7.26:1 on the dark page */

    /* Rule and wash tiers DERIVED, not invented — the same relationship the light
       tiers record (rule = 22% accent over the card surface, wash = 5% over it),
       recomputed against the dark card #241D15. Every accent still clears AA on
       its own wash: 6.11 / 6.20 / 5.63 / 6.03. */
    --sage-rule:            #333E2E;   --sage-wash:            #27241B;
    --sienna-rule:          #4C3728;   --sienna-wash:          #2D2319;
    --mode-diagnostic-rule: var(--sage-rule);   --mode-diagnostic-wash: var(--sage-wash);
    --mode-drill-rule:      var(--sage-rule);   --mode-drill-wash:      var(--sage-wash);

    /* ── The three calculator brands. Derived by the method stated beside the
       light values: tints RE-MIXED at their own fraction over the dark card,
       text LIFTED with hue held and saturation capped at 0.50, and the three
       deep surfaces (deep, on-deep, fn-edge) LEFT WHERE THEY ARE — a panel that
       is dark in light mode is dark in dark mode, exactly as --command is, and
       the pale text already on it keeps working for free. They are restated here
       rather than omitted because check_coverage requires every literal colour in
       :root to appear in this block; "unchanged" is a decision it should be able
       to see.

       The block wash separates from the page by WARMTH, not brightness — 1.12:1
       against #17130E, which is above the 1.08:1 an ordinary --white card gets,
       so the calculator block reads as a distinct surface by the same means every
       other card on the product does. */
    --gdc-ti-rule:       #4A7AAF;   --gdc-ti-wash:       #251E17;
    --gdc-ti-deep:       #1B3A6A;   --gdc-ti-accent:     #5D87C9;
    --gdc-ti-on-deep:    #C8DEFF;   --gdc-ti-key:        #26221D;
    --gdc-ti-ink:        #6E96CF;   --gdc-ti-key-fn:     #2B2E31;
    --gdc-ti-fn-rule:    #4178C0;   --gdc-ti-fn-edge:    #0A2A6A;
    --gdc-ti-syntax:     #272421;   --gdc-ti-note:       #25201A;
    --gdc-ti-note-ink:   #5E89C9;   --gdc-ti-step-rule:  rgba(74,122,175,0.30);

    --gdc-casio-rule:      #BF4040; --gdc-casio-wash:      #281E16;
    --gdc-casio-deep:      #6A1010; --gdc-casio-accent:    #CD6969;
    --gdc-casio-on-deep:   #FFCCCC; --gdc-casio-key:       #2B1E16;
    --gdc-casio-ink:       #D07171; --gdc-casio-key-fn:    #362019;
    --gdc-casio-fn-rule:   #C14444; --gdc-casio-fn-edge:   #4A0808;
    --gdc-casio-syntax:    #2F1F18; --gdc-casio-note:      #281E16;
    --gdc-casio-note-ink:  #CD6969; --gdc-casio-step-rule: rgba(154,48,48,0.30);

    --gdc-hp-rule:       #2B7D66;   --gdc-hp-wash:       #241F16;
    --gdc-hp-deep:       #0A4A38;   --gdc-hp-accent:     #32967A;
    --gdc-hp-on-deep:    #BBFFEE;   --gdc-hp-key:        #24231A;
    --gdc-hp-ink:        #37A57B;   --gdc-hp-key-fn:     #252F24;
    --gdc-hp-fn-rule:    #2C8460;   --gdc-hp-fn-edge:    #083A28;
    --gdc-hp-syntax:     #25281E;   --gdc-hp-note:       #242119;
    --gdc-hp-note-ink:   #339973;   --gdc-hp-step-rule:  rgba(42,122,100,0.30);

    /* The frame shadow is not a colour token by check_coverage's reading (it
       starts with a length, so is_colour() is false and it is not REQUIRED to
       appear here). It is given a dark value anyway, because a drop shadow tuned
       for parchment is invisible on espresso — depth on a dark page comes from
       the surface being warmer than the page, which --cream/--white already do,
       so this only has to deepen the seat rather than paint a halo. */
    --shadow-frame: 0 18px 40px -24px rgba(0,0,0,0.65);

    /* ── Legacy .lc-* constants. Retire with the last .lc-* class. */
    --lc-cream-dark: #2A2219;   /* selected-option wash — raised, not paler */
    --lc-sepia:      #C4A882;   /* already warm and light; 8.16:1 on the page */

    /* ═══ UI STANDARD v3 — THE DARK PALETTE (RUN 76) ════════════════════════
       Every --v3-* literal above has a value here. MEASURED on the dark
       surfaces: --v3-ink 15.48 on --v3-bg, --v3-ink2 7.85, --v3-signature 5.65,
       and all four mode colours clear AA as text (4.70 worst, Le Controle on
       --v3-chip). --v3-taupe is 4.62/4.30/4.49/4.07 — above the 3.0 non-text
       floor everywhere, below AA-small on --card and --chip, which is the
       open question in spec §7 and is recorded rather than rounded away.

       NOTE --v3-btn INVERTS ITS ROLE BETWEEN THEMES. In light it is ink; in dark
       the founder's spec makes it the signature green with near-black text
       (5.50:1). That is deliberate in the spec and is not the v2 pattern, where
       --btn simply flipped to cream. */
    --v3-bg:        #13160F;
    --v3-card:      #1B1E15;
    --v3-nav:       #13160F;
    --v3-ink:       #EBE3D0;
    --v3-ink2:      #BCB29B;
    --v3-taupe:     #8A7E6C;
    --v3-hair:      #33372A;
    --v3-chip:      #191C13;
    /* APP-SPEC's corrected dark accent (RUN 106 UNIT 0). #48926E could not
       carry a light ink: --v3-btntext on it measured 3.17. */
    --v3-signature: #4C9B75;
    /* THE SPEC DOES NOT GIVE A DARK VALUE FOR THIS ONE. It is derived
       MECHANICALLY rather than chosen: the same 10% alpha, over the dark
       signature the spec does give (#4E9E72). Stated because a token with no
       spec value is exactly the kind of thing that later reads as founder-issued
       when it is not. If the founder wants a different dark wash, this is the
       line. dark_mode_audit.py requires every :root colour to be overridden, so
       leaving it out was not an option either. */
    --v3-signature-wash: rgba(72,146,110,.10);
    /* Derived from the DARK controle #CB8072 exactly as the line above is
       derived from the dark signature. Dark is FROZEN for this rebuild and no
       dark screen is being built; a token still needs a dark value or
       scripts/dark_mode_audit.py fails on the omission, which is the gate
       working rather than an invitation to design dark. */
    --v3-missed-wash: rgba(203,128,114,.12);
    /* Heavier in dark: the panel and the page are closer in luminance, so a
       .42 scrim over #17140F does not separate them. */
    --v3-scrim: rgba(0,0,0,.62);

    /* The dark half of the RUN 100 spec roles. --v3-ink3 is the spec's own
       #8E8570: it measures 4.62:1 on --v3-card and needed no step. */
    --v3-ink3:       #8E8570;
    --v3-surface2:   #242719;
    --v3-hair2:      #434735;
    --v3-brass:      #C7A85F;
    --v3-brass-soft: #A98F4F;
    --v3-accent-hover: #478B6A;
    --v3-shadow:      0 1px 2px rgba(0,0,0,.4), 0 12px 30px -16px rgba(0,0,0,.65);
    --v3-shadow-sm:   0 1px 2px rgba(0,0,0,.35);
    --v3-shadow-lift: 0 1px 2px rgba(0,0,0,.45), 0 22px 46px -22px rgba(0,0,0,.75);


    /* THE -INK FAMILY INVERTS ITS ROLE. In light these are DARKENED variants for
       text on a pale tint; in dark the label needs a LIFTED one, and the dark
       mode colours the spec already issues are exactly that. So each -ink token
       resolves to its own mode colour here.

       THAT IS WHY AMBER IS TEXT IN DARK AND NEVER IN LIGHT. #D6A24E measures
       7.04:1 at worst on the dark surfaces; #BE842A measures 2.73:1 at worst on
       the light ones. Law C1 is a statement about one hex on one set of
       surfaces, not about the colour amber. */
    /* A LITTLE LIGHTER THAN THE SIGNATURE ITSELF, deliberately. This is the
       ink used when the accent appears as TEXT on a tinted chip, and the
       spec's accent taken verbatim gives 4.45 on --v3-mode-diagnostic-chip
       - close enough to look finished, not close enough to be right.
       4.82 on the chip, 5.46 on a card, 5.90 on the page. */
    --v3-signature-ink:       #4FA27A;
    --v3-mode-diagnostic-ink: #6FC49A;
    --v3-mode-study-ink:      #9DB897;
    --v3-mode-drill-ink:      #D6A24E;
    --v3-mode-controle-ink:   #DB9789;

    /* SOLVED, NOT CHOSEN. A dark tint washes contrast out fast, because the
       label and its chip are the SAME hue -- raising the alpha walks the
       background toward the text instead of away from it. At the mockups' .14
       the claret label measured 4.12:1 on its own chip. .06 is the largest
       common alpha at which all four clear AA with headroom; one value for all
       four so the chips stay visually consistent. The LIGHT chips keep the
       mockups' own alphas untouched, because a light tint moves toward white and
       has all the room it needs. */

    /* ── GDC DEVICE SKINS (RUN 78 Phase 3) ─────────────────────────────────
       THE ONE TOKEN FAMILY THAT DOES NOT FLIP BETWEEN THEMES, and that is the
       point rather than an oversight: these depict physical calculators. A
       TI-Nspire is charcoal with a cool-blue menu key on any desk in any light,
       and a skin that turned cream in light mode would stop being a picture of
       the device in the student's bag.

       IDENTICAL TO THE LIGHT BLOCK, ON PURPOSE. Every other token here flips;
       these do not, because a calculator does not. The values are repeated
       rather than omitted so the dark-mode audit reads a decision instead of a
       gap. */
    --v3-dev-ti-body:      #3A3F45;
    --v3-dev-ti-name:      #C9CDD2;
    --v3-dev-ti-pad:       #2C3036;
    --v3-dev-ti-bezel:     #23272C;
    --v3-dev-ti-screen:    #0E1B2A;
    --v3-dev-ti-key:       #4B5158;
    --v3-dev-ti-keyink:    #CFD3D8;
    --v3-dev-ti-accent:    #4A78B8;
    --v3-dev-ti-accentink: #FFFFFF;
    --v3-dev-ti-enter:     #8FCE9F;
    --v3-dev-ti-enterink:  #112233;

    --v3-dev-casio-body:      #E9ECEF;
    --v3-dev-casio-name:      #5B636D;
    --v3-dev-casio-bezel:     #C8CCD0;
    --v3-dev-casio-screen:    #E9F1E6;
    --v3-dev-casio-key:       #D3D7DB;
    --v3-dev-casio-keyink:    #333333;
    --v3-dev-casio-accent:    #F3A13A;
    --v3-dev-casio-accentink: #2A1B06;
    --v3-dev-casio-enter:     #2E5FA3;
    --v3-dev-casio-enterink:  #FFFFFF;
    --v3-dev-casio-fkey:      #2E5FA3;
    --v3-dev-casio-fkeyink:   #FFFFFF;

    --v3-dev-hp-body:      #17191C;
    --v3-dev-hp-name:      #B9BDC2;
    --v3-dev-hp-bezel:     #000000;
    --v3-dev-hp-screen:    #0C0F14;
    --v3-dev-hp-key:       #2A2E33;
    --v3-dev-hp-keyink:    #CFD3D8;
    --v3-dev-hp-accent:    #E08A2C;
    --v3-dev-hp-accentink: #1A1A1A;
    --v3-dev-hp-enter:     #3A6F4E;
    --v3-dev-hp-enterink:  #FFFFFF;

    --v3-mode-diagnostic-chip:#1D2A22;
    --v3-mode-study-chip:     #233021;
    --v3-mode-drill-chip:     #302911;
    --v3-mode-controle-chip:  #331E19;
    /* THE PRIMARY FILL MOVES WITH ITS INK (RUN 106). This was #397558, a
       darker green than --v3-signature, and it is the surface
       --v3-btntext sits on. Changing the ink to the page ground without
       moving the fill took "Begin" from 4.70 to 3.36 - a correct rule
       applied to one half of a pair. On APP-SPEC's corrected dark accent
       the same ink measures 5.44. */
    --v3-btn:       #4C9B75;
    /* THE PAGE GROUND, not a cream. In dark the accent is light and its ink
       is dark - the same one rule APP-SPEC now states. On the signature
       this measures 5.44; the cream it replaces measured 3.17. */
    --v3-btntext:   #13160F;

    --v3-mode-diagnostic: #48926E;
    --v3-mode-study:      #84A17F;
    --v3-mode-drill:      #D6A24E;
    --v3-mode-controle:   #CB8072;
  }
}
[data-theme="dark"] {

  /* ── Surfaces. The ramp INVERTS: in light, --cream-d/-dd are progressively
     DEEPER washes below the page; in dark, a recessed or raised area reads by
     getting lighter, so they step UP from the page. --white keeps its meaning
     — "the card surface" — and stops being white. */
  /* ═══ UI STANDARD v2 — THE DARK PALETTE (RUN 66) ═══════════════════════
     The eleven canonical names, dark values. Every alias below follows them,
     so the two themes stay one system rather than two.

     MEASURED: --taupe clears 4.07-4.70:1 on all four dark surfaces, so the
     light-mode exception recorded in :root does NOT apply here. --ink 15.74 on
     --bg, --ink2 7.98, --green 7.55. Nothing in the dark set is short. */
  --bg:        #15120E;
  --navbg:     #1A1610;
  --card:      #201B15;
  --ink2:      #B4A895;
  --taupe:     #8A7E6C;
  --green:     #7FB08C;
  --hair:      #352E25;
  --chip:      #252017;
  --btn:       #F1EBE0;
  --btntext:   #17130E;
  --shadow:    0 20px 44px -26px rgba(0,0,0,.6);
  --shadow2:   0 10px 22px -16px rgba(0,0,0,.5);

  --cream:     var(--bg);      /* page — warm espresso, never black */
  --cream-d:   #201A13;   /* recessed section / selected-row wash */
  --white:     var(--card);    /* card / panel / option surface */
  --cream-dd:  #2A2219;   /* highest surface, used sparingly */

  /* ── Ink ramp. --ink-1 is documented above as a "secondary dark surface" but
     is used in practice as body TEXT (the /content guides, the admin table), so
     it takes a light value one step below --ink rather than a surface value.
     Measured on the page: ink 15.05:1, ink-1 12.71:1, ink-2 9.58:1, ink-3
     5.77:1. */
  --ink:       #F1EBE0;   /* primary text — v2 (was #EFE7D8) */
  --ink-1:     #DFD5C2;
  --ink-2:     var(--ink2);   /* body copy */
  /* SMALL-TEXT SAFE IN DARK ALREADY — no retune was needed here. RUN 46 C1
     moved the LIGHT --ink-3 to 4.52:1; measured on the four dark surfaces this
     value is 5.77 page / 5.20 card / 5.38 recessed / 4.89 highest, so it
     clears AA-small everywhere it can land. The light and dark tokens now
     carry the same role, which they did not before: the note here used to say
     "LARGE/UI only" while the value was already good for body. */
  --ink-3:     #9C8E78;   /* labels, meta, small text (AA-small) */
  /* THE ALIAS THE FOUNDER'S RULING FORBIDS — FOUND HERE IN DARK, RUN 67.
     This token read `var(--taupe)`. The comment above it described a computed
     value and the code underneath had been replaced by an alias, so the file
     documented one thing and shipped another; the light block one screen up
     recorded the alias being caught and reverted, and nobody looked at the dark
     block, where the identical edit had survived.

     It was not a CONTRAST bug — dark --taupe is 4.70:1 and would have passed
     every gate in the repo, which is exactly why it lasted. It collapsed the
     four-tone scale into three in one theme only, so the dark ramp had a step
     the light ramp had and the two themes stopped being one system.

     RESTORED BY THE RECORDED METHOD, not by copying the recorded number: the
     MINIMUM lift of the brief's #6E6455 that reaches 3:1 on BOTH --bg and
     --card, hue and saturation held. That is #706657 — 3.31:1 page / 3.03:1
     card / 3.06:1 recessed. The old comment claimed 3.38 / 3.04 for a hex it no
     longer named; these four numbers are measured from the value now beside
     them. Still LARGE/UI only. */
  --ink-4:     #706657;   /* quaternary — LARGE TEXT + DECORATION ONLY */

  /* Decorative hairline, same exemption as light (1.42:1 on the page). Nothing
     may depend on it alone to be understood — that condition is unchanged. */
  --rule:      var(--hair);

  /* ── Command surfaces STAY DARK. This is the pair of tokens that makes the
     theme flip safe: the nav, primary buttons and dark panels lift slightly off
     the page instead of inverting into glare. --command sits one step above the
     page (#17130E → #241D15) so a fixed topbar still reads as a distinct band,
     and --command-2 one further for its hover and secondary panels.
     --on-command becomes candle-white: 13.55:1 on --command. */
  --command:     var(--btn);
  --command-2:   #2A2219;
  --on-command:  var(--btntext);
  /* The ramp inverts with the surface: --command is CREAM here, so these are
     dark. Same roles, same names, opposite end of the scale — which is the
     whole point of them being tokens. */
  --on-command-2:    #605C56;
  --on-command-rule: #CAC5BC;

  /* The delta pair INVERTS with the surface it sits on — that is the whole
     point of it existing. On the dark theme the hero is cream, so the numbers
     go dark: 8.62:1 and 10.01:1, against 1.36 and 2.45 before. */
  --on-command-up:   #1A4A28;
  --on-command-down: #6B1818;

  /* The band scale lightens for the dark page; the ORDER is preserved, which is
     what a scale has to keep.

     RUN 70: the four started bands are now the dark ink ramp, for the reason
     written out in the light block. The note that used to stand here — that
     --band-3 was kept off #7FB08C because "that is dark --green exactly, and
     the colour-law scanner computes its green family BY VALUE, so the legend
     dot registered as an accent fill. A data scale must not collide with the
     accent." — is kept, because it is the whole argument, and because it was
     applied to the green collision and never to the two the accent law cares
     about: --band-1 WAS dark --wrong exactly, and --band-4 WAS dark --correct
     exactly. Aliasing onto the ink ramp settles all three at once. */
  --band-0: #6E655A;         /* not started — dashed OUTLINE only, never a fill */
  --band-1: var(--ink-4);
  --band-2: var(--ink-3);
  --band-3: var(--ink-2);
  --band-4: var(--ink);

  --scrim: rgba(0,0,0,0.72);

  /* THE on-deep RAMP IS RESTATED HERE WITH IDENTICAL VALUES, ON PURPOSE.
     The GDC deep surfaces do not invert — they are calculator brand colours —
     so the text on them must not invert either. Repeating the values rather
     than omitting them is the convention --gdc-ti-deep and --gdc-casio-deep
     already follow a few lines below: scripts/dark_mode_audit.py's
     check_coverage asks "what did the dark block override", exempting only
     ALIASES, so a theme-stable literal answers that question by being restated.
     Omitting them would read as an oversight and fail the audit; giving them
     different dark values would break the surface they sit on. */
  --on-deep:      #FFFFFF;
  --on-deep-2:    rgba(255,255,255,0.80);
  --on-deep-3:    rgba(255,255,255,0.55);
  --on-deep-edge: rgba(255,255,255,0.22);
  --on-deep-lift: rgba(255,255,255,0.18);

  /* ── Semantic states. The washes become solid warm tints rather than the
     light mode's 7% alpha: an alpha wash over an espresso page turns muddy and
     loses the state. Measured — correct 7.91:1 on page and 6.24:1 on its own
     wash; wrong 6.61:1 and 5.70:1; amber 7.60:1 and 6.20:1. */
  --correct:    #6FB98A;  --correct-bg: #1E2C22;
  --correct-bg-2: #24382A;
  --wrong:      #D98476;  --wrong-bg:   #2E1E1B;
  --amber:      #CE9E52;  --amber-bg:   #2E2517;
  /* The page IS the dark surface now, so this collapses onto --correct rather
     than staying a second, lighter green nobody tuned. */
  --correct-on-dark: var(--correct);

  /* Text on a semantic fill flips WITH the fill. The three states above are
     deep in light and light in dark, so what sits on them goes the other way:
     #FDFAF5 becomes the page's own espresso. Measured on the dark fills —
     correct 7.76:1, wrong 6.44:1, amber 7.53:1. */
  --on-fill:    #17130E;

  /* ── Mode accents. Each lifts to stay legible and KEEPS ITS HUE — sage is
     still sage. On the page: study 7.28:1, diagnostic 7.46:1, drill 6.70:1,
     exam 7.26:1 — all clear 4.5:1 for text and 3:1 for UI.
     --sage is overridden here rather than --mode-study, which is now an alias
     onto it: overriding the alias would have broken the single-accent link
     the light block establishes, and the dark study accent WAS this value. */
  --sage:            var(--green);  /* structural accent — v2 --green */
  /* Same ruling as the light block: one accent, one colour. */
  --mode-diagnostic: var(--sage);   /* was #7FA8D6 */
  --mode-drill:      var(--sage);   /* was #CE87A2 */
  /* --sienna, like --sage, is overridden here rather than --mode-exam, which
     is now an alias onto it. The value is the dark exam accent unchanged. */
  --sienna:          #D8926B;   /* burnt sienna — 7.26:1 on the dark page */

  /* Rule and wash tiers DERIVED, not invented — the same relationship the light
     tiers record (rule = 22% accent over the card surface, wash = 5% over it),
     recomputed against the dark card #241D15. Every accent still clears AA on
     its own wash: 6.11 / 6.20 / 5.63 / 6.03. */
  --sage-rule:            #333E2E;   --sage-wash:            #27241B;
  --sienna-rule:          #4C3728;   --sienna-wash:          #2D2319;
  --mode-diagnostic-rule: var(--sage-rule);   --mode-diagnostic-wash: var(--sage-wash);
  --mode-drill-rule:      var(--sage-rule);   --mode-drill-wash:      var(--sage-wash);

  /* ── The three calculator brands. Derived by the method stated beside the
     light values: tints RE-MIXED at their own fraction over the dark card,
     text LIFTED with hue held and saturation capped at 0.50, and the three
     deep surfaces (deep, on-deep, fn-edge) LEFT WHERE THEY ARE — a panel that
     is dark in light mode is dark in dark mode, exactly as --command is, and
     the pale text already on it keeps working for free. They are restated here
     rather than omitted because check_coverage requires every literal colour in
     :root to appear in this block; "unchanged" is a decision it should be able
     to see.

     The block wash separates from the page by WARMTH, not brightness — 1.12:1
     against #17130E, which is above the 1.08:1 an ordinary --white card gets,
     so the calculator block reads as a distinct surface by the same means every
     other card on the product does. */
  --gdc-ti-rule:       #4A7AAF;   --gdc-ti-wash:       #251E17;
  --gdc-ti-deep:       #1B3A6A;   --gdc-ti-accent:     #5D87C9;
  --gdc-ti-on-deep:    #C8DEFF;   --gdc-ti-key:        #26221D;
  --gdc-ti-ink:        #6E96CF;   --gdc-ti-key-fn:     #2B2E31;
  --gdc-ti-fn-rule:    #4178C0;   --gdc-ti-fn-edge:    #0A2A6A;
  --gdc-ti-syntax:     #272421;   --gdc-ti-note:       #25201A;
  --gdc-ti-note-ink:   #5E89C9;   --gdc-ti-step-rule:  rgba(74,122,175,0.30);

  --gdc-casio-rule:      #BF4040; --gdc-casio-wash:      #281E16;
  --gdc-casio-deep:      #6A1010; --gdc-casio-accent:    #CD6969;
  --gdc-casio-on-deep:   #FFCCCC; --gdc-casio-key:       #2B1E16;
  --gdc-casio-ink:       #D07171; --gdc-casio-key-fn:    #362019;
  --gdc-casio-fn-rule:   #C14444; --gdc-casio-fn-edge:   #4A0808;
  --gdc-casio-syntax:    #2F1F18; --gdc-casio-note:      #281E16;
  --gdc-casio-note-ink:  #CD6969; --gdc-casio-step-rule: rgba(154,48,48,0.30);

  --gdc-hp-rule:       #2B7D66;   --gdc-hp-wash:       #241F16;
  --gdc-hp-deep:       #0A4A38;   --gdc-hp-accent:     #32967A;
  --gdc-hp-on-deep:    #BBFFEE;   --gdc-hp-key:        #24231A;
  --gdc-hp-ink:        #37A57B;   --gdc-hp-key-fn:     #252F24;
  --gdc-hp-fn-rule:    #2C8460;   --gdc-hp-fn-edge:    #083A28;
  --gdc-hp-syntax:     #25281E;   --gdc-hp-note:       #242119;
  --gdc-hp-note-ink:   #339973;   --gdc-hp-step-rule:  rgba(42,122,100,0.30);

  /* The frame shadow is not a colour token by check_coverage's reading (it
     starts with a length, so is_colour() is false and it is not REQUIRED to
     appear here). It is given a dark value anyway, because a drop shadow tuned
     for parchment is invisible on espresso — depth on a dark page comes from
     the surface being warmer than the page, which --cream/--white already do,
     so this only has to deepen the seat rather than paint a halo. */
  --shadow-frame: 0 18px 40px -24px rgba(0,0,0,0.65);

  /* ── Legacy .lc-* constants. Retire with the last .lc-* class. */
  --lc-cream-dark: #2A2219;   /* selected-option wash — raised, not paler */
  --lc-sepia:      #C4A882;   /* already warm and light; 8.16:1 on the page */

  /* ═══ UI STANDARD v3 — THE DARK PALETTE (RUN 76) ════════════════════════
     Every --v3-* literal above has a value here. MEASURED on the dark
     surfaces: --v3-ink 15.48 on --v3-bg, --v3-ink2 7.85, --v3-signature 5.65,
     and all four mode colours clear AA as text (4.70 worst, Le Controle on
     --v3-chip). --v3-taupe is 4.62/4.30/4.49/4.07 — above the 3.0 non-text
     floor everywhere, below AA-small on --card and --chip, which is the
     open question in spec §7 and is recorded rather than rounded away.

     NOTE --v3-btn INVERTS ITS ROLE BETWEEN THEMES. In light it is ink; in dark
     the founder's spec makes it the signature green with near-black text
     (5.50:1). That is deliberate in the spec and is not the v2 pattern, where
     --btn simply flipped to cream. */
  --v3-bg:        #13160F;
  --v3-card:      #1B1E15;
  --v3-nav:       #13160F;
  --v3-ink:       #EBE3D0;
  --v3-ink2:      #BCB29B;
  --v3-taupe:     #8A7E6C;
  --v3-hair:      #33372A;
  --v3-chip:      #191C13;
  /* APP-SPEC's corrected dark accent (RUN 106 UNIT 0). #48926E could not
     carry a light ink: --v3-btntext on it measured 3.17. */
  --v3-signature: #4C9B75;
  /* THE SPEC DOES NOT GIVE A DARK VALUE FOR THIS ONE. It is derived
     MECHANICALLY rather than chosen: the same 10% alpha, over the dark
     signature the spec does give (#4E9E72). Stated because a token with no
     spec value is exactly the kind of thing that later reads as founder-issued
     when it is not. If the founder wants a different dark wash, this is the
     line. dark_mode_audit.py requires every :root colour to be overridden, so
     leaving it out was not an option either. */
  --v3-signature-wash: rgba(72,146,110,.10);
  /* Derived from the DARK controle #CB8072 exactly as the line above is
     derived from the dark signature. Dark is FROZEN for this rebuild and no
     dark screen is being built; a token still needs a dark value or
     scripts/dark_mode_audit.py fails on the omission, which is the gate
     working rather than an invitation to design dark. */
  --v3-missed-wash: rgba(203,128,114,.12);
  /* Heavier in dark: the panel and the page are closer in luminance, so a
     .42 scrim over #17140F does not separate them. */
  --v3-scrim: rgba(0,0,0,.62);

  /* The dark half of the RUN 100 spec roles. --v3-ink3 is the spec's own
     #8E8570: it measures 4.62:1 on --v3-card and needed no step. */
  --v3-ink3:       #8E8570;
  --v3-surface2:   #242719;
  --v3-hair2:      #434735;
  --v3-brass:      #C7A85F;
  --v3-brass-soft: #A98F4F;
  --v3-accent-hover: #478B6A;
  --v3-shadow:      0 1px 2px rgba(0,0,0,.4), 0 12px 30px -16px rgba(0,0,0,.65);
  --v3-shadow-sm:   0 1px 2px rgba(0,0,0,.35);
  --v3-shadow-lift: 0 1px 2px rgba(0,0,0,.45), 0 22px 46px -22px rgba(0,0,0,.75);


  /* THE -INK FAMILY INVERTS ITS ROLE. In light these are DARKENED variants for
     text on a pale tint; in dark the label needs a LIFTED one, and the dark
     mode colours the spec already issues are exactly that. So each -ink token
     resolves to its own mode colour here.

     THAT IS WHY AMBER IS TEXT IN DARK AND NEVER IN LIGHT. #D6A24E measures
     7.04:1 at worst on the dark surfaces; #BE842A measures 2.73:1 at worst on
     the light ones. Law C1 is a statement about one hex on one set of
     surfaces, not about the colour amber. */
  /* A LITTLE LIGHTER THAN THE SIGNATURE ITSELF, deliberately. This is the
     ink used when the accent appears as TEXT on a tinted chip, and the
     spec's accent taken verbatim gives 4.45 on --v3-mode-diagnostic-chip
     - close enough to look finished, not close enough to be right.
     4.82 on the chip, 5.46 on a card, 5.90 on the page. */
  --v3-signature-ink:       #4FA27A;
  --v3-mode-diagnostic-ink: #6FC49A;
  --v3-mode-study-ink:      #9DB897;
  --v3-mode-drill-ink:      #D6A24E;
  --v3-mode-controle-ink:   #DB9789;

  /* SOLVED, NOT CHOSEN. A dark tint washes contrast out fast, because the
     label and its chip are the SAME hue -- raising the alpha walks the
     background toward the text instead of away from it. At the mockups' .14
     the claret label measured 4.12:1 on its own chip. .06 is the largest
     common alpha at which all four clear AA with headroom; one value for all
     four so the chips stay visually consistent. The LIGHT chips keep the
     mockups' own alphas untouched, because a light tint moves toward white and
     has all the room it needs. */

  /* ── GDC DEVICE SKINS (RUN 78 Phase 3) ─────────────────────────────────
     THE ONE TOKEN FAMILY THAT DOES NOT FLIP BETWEEN THEMES, and that is the
     point rather than an oversight: these depict physical calculators. A
     TI-Nspire is charcoal with a cool-blue menu key on any desk in any light,
     and a skin that turned cream in light mode would stop being a picture of
     the device in the student's bag.

     IDENTICAL TO THE LIGHT BLOCK, ON PURPOSE. Every other token here flips;
     these do not, because a calculator does not. The values are repeated
     rather than omitted so the dark-mode audit reads a decision instead of a
     gap. */
  --v3-dev-ti-body:      #3A3F45;
  --v3-dev-ti-name:      #C9CDD2;
  --v3-dev-ti-pad:       #2C3036;
  --v3-dev-ti-bezel:     #23272C;
  --v3-dev-ti-screen:    #0E1B2A;
  --v3-dev-ti-key:       #4B5158;
  --v3-dev-ti-keyink:    #CFD3D8;
  --v3-dev-ti-accent:    #4A78B8;
  --v3-dev-ti-accentink: #FFFFFF;
  --v3-dev-ti-enter:     #8FCE9F;
  --v3-dev-ti-enterink:  #112233;

  --v3-dev-casio-body:      #E9ECEF;
  --v3-dev-casio-name:      #5B636D;
  --v3-dev-casio-bezel:     #C8CCD0;
  --v3-dev-casio-screen:    #E9F1E6;
  --v3-dev-casio-key:       #D3D7DB;
  --v3-dev-casio-keyink:    #333333;
  --v3-dev-casio-accent:    #F3A13A;
  --v3-dev-casio-accentink: #2A1B06;
  --v3-dev-casio-enter:     #2E5FA3;
  --v3-dev-casio-enterink:  #FFFFFF;
  --v3-dev-casio-fkey:      #2E5FA3;
  --v3-dev-casio-fkeyink:   #FFFFFF;

  --v3-dev-hp-body:      #17191C;
  --v3-dev-hp-name:      #B9BDC2;
  --v3-dev-hp-bezel:     #000000;
  --v3-dev-hp-screen:    #0C0F14;
  --v3-dev-hp-key:       #2A2E33;
  --v3-dev-hp-keyink:    #CFD3D8;
  --v3-dev-hp-accent:    #E08A2C;
  --v3-dev-hp-accentink: #1A1A1A;
  --v3-dev-hp-enter:     #3A6F4E;
  --v3-dev-hp-enterink:  #FFFFFF;

  --v3-mode-diagnostic-chip:#1D2A22;
  --v3-mode-study-chip:     #233021;
  --v3-mode-drill-chip:     #302911;
  --v3-mode-controle-chip:  #331E19;
  /* THE PRIMARY FILL MOVES WITH ITS INK (RUN 106). This was #397558, a
     darker green than --v3-signature, and it is the surface
     --v3-btntext sits on. Changing the ink to the page ground without
     moving the fill took "Begin" from 4.70 to 3.36 - a correct rule
     applied to one half of a pair. On APP-SPEC's corrected dark accent
     the same ink measures 5.44. */
  --v3-btn:       #4C9B75;
  /* THE PAGE GROUND, not a cream. In dark the accent is light and its ink
     is dark - the same one rule APP-SPEC now states. On the signature
     this measures 5.44; the cream it replaces measured 3.17. */
  --v3-btntext:   #13160F;

  --v3-mode-diagnostic: #48926E;
  --v3-mode-study:      #84A17F;
  --v3-mode-drill:      #D6A24E;
  --v3-mode-controle:   #CB8072;
}

/* ── Dark + higher-contrast, together ────────────────────────────────────────
   The accessibility block in codex-components.css darkens --ink-2/-3/-4 and
   --rule for `html[data-cx-contrast="high"]`. That selector is (0,1,1) and sits
   in a later file, so WITHOUT this it beats the dark block above and a student
   using both settings gets light-mode dark text on an espresso page — the two
   accessibility features cancelling each other out.

   So the combination is given its own values, and they move the way high
   contrast should: every tier gets LIGHTER on dark, where the light theme makes
   them darker. Measured on the page — ink-2 13.46:1 (from 9.58), ink-3 10.63:1
   (from 5.77), ink-4 7.52:1 (from 3.38), rule 2.92:1 (from 1.42). */
html[data-cx-contrast="high"][data-theme="dark"] {
  --ink-2:  #E4DBCA;
  --ink-3:  #CFC3AE;
  --ink-4:  #B0A48E;
  --rule:   #6A5E4C;
}
