/*
 * Novo Nordisk Step Count — design tokens.
 *
 * The single implementation source for colour, typography, spacing and the other visual
 * primitives. Components consume these variables and never hard-code a brand value, so that
 * a brand change is a change to this file rather than a hunt through every component.
 *
 * Governed by:
 *   docs/design/novo-nordisk-design-contract.md   — the brand rules and their source
 *   docs/design/tael-skridt-design-principles.md  — the product's own constraints
 *
 * The 2024 colour values are transcribed from the official Novo Nordisk 2024 colour assets at
 * corporatedesignmanual.novonordisk.com/content/dam/nncorp/brand-portal/design-manuals/
 * corporate/colour/2024/<Colour name>.svg — one asset per colour, read individually. The
 * design contract lists the source asset for each value. The three 2026 values come from
 * Novo's "Novo CVI Guidelines Summary" (2026) — see the 2026 CVI block below.
 *
 * No build step exists in this project and none is introduced by this file: it is plain CSS
 * custom properties served directly from public/. Adding a build pipeline would be a
 * dependency decision under CLAUDE.md §5, not an implementation detail.
 */

/* === The Novo web font (2026-10-02) ====================================================
 * "Novo web fonts – Sept 2026" from Novo's brand team, self-hosted in public/fonts/novo under
 * the SIL Open Font License 1.1 (OFL.txt beside the files). Never a font CDN: a third-party
 * font request would tell another company who opened the page (CLAUDE.md section 10).
 *
 * Only the five weights the --font-weight-* tokens below name are served; no italic, because
 * nothing sets one. `swap` shows the Arial fallback at once and the face when it arrives, so
 * text is never invisible while a file loads. Regular and Bold are preloaded by
 * resources/views/partials/font-preload.blade.php. The face has no → (U+2192); the browser
 * draws that one glyph from the next font in the stack.
 */
@font-face { font-family: "Novo"; font-style: normal; font-weight: 400; font-display: swap; src: url("../fonts/novo/Novo-Regular.woff2") format("woff2"); }
@font-face { font-family: "Novo"; font-style: normal; font-weight: 500; font-display: swap; src: url("../fonts/novo/Novo-Medium.woff2") format("woff2"); }
@font-face { font-family: "Novo"; font-style: normal; font-weight: 600; font-display: swap; src: url("../fonts/novo/Novo-SemiBold.woff2") format("woff2"); }
@font-face { font-family: "Novo"; font-style: normal; font-weight: 700; font-display: swap; src: url("../fonts/novo/Novo-Bold.woff2") format("woff2"); }
@font-face { font-family: "Novo"; font-style: normal; font-weight: 800; font-display: swap; src: url("../fonts/novo/Novo-ExtraBold.woff2") format("woff2"); }

:root {
    /* === Brand palette — official 2024 values, then the 2026 CVI =========================
     * Nothing outside this block should reference a raw colour; everything else points at
     * the semantic layer below.
     */

    /* Primary */
    --nn-true-blue: #001965;            /* rgb(0, 25, 101) */
    --nn-snow-white: #ffffff;           /* rgb(255, 255, 255) */

    /* Secondary */
    --nn-light-blue: #3b97de;           /* rgb(59, 151, 222) */
    --nn-sea-blue: #005ad2;             /* rgb(0, 90, 210) */
    --nn-ocean-green: #2a918b;          /* rgb(42, 145, 139) */
    --nn-rose-pink: #eea7bf;            /* rgb(238, 167, 191) */
    --nn-granite-grey: #939aa7;         /* rgb(147, 154, 167) */
    --nn-sand-grey: #ccc5bd;            /* rgb(204, 197, 189) */
    --nn-lava-red: #e6553f;             /* rgb(230, 85, 63) */
    --nn-golden-yellow: #eaab00;        /* rgb(234, 171, 0) */
    --nn-forest-green: #3f9c35;         /* rgb(63, 156, 53) */

    /* === Novo's 2026 CVI ================================================================
     * [NOVO] "Novo CVI Guidelines Summary" (2026, supplied by Novo on 2026-09-22, held by
     * Kenneth): Lasting Blue #00008F (RGB 0/0/143, Pantone 2738 C), Now Turquoise #00FFFF
     * (Pantone 3115 C), Ice Blue #B6F0FF (Pantone 290 C, secondary palette). The locked boards
     * (canvas version 17) draw exactly these. The palette changes, the layout does not.
     * Neutrals and status colours are unchanged (the warm ground went white with design v4, 2026-10-01; see --board-ground), and the 2024 values above
     * stay defined because their tints still are.
     *
     * THE MAP, from the 2024 value each role had to the 2026 one:
     *   ink, text, focus, links, strong border   #001965 True Blue  -> #00008F Lasting Blue
     *   the eyebrow and link on white            #005AD2 Sea Blue   -> #00008F Lasting Blue
     *   primary hover / pressed                  Sea Blue / #1A3074 -> #3333A5 / #1A1A9A,
     *                                            Lasting Blue's 80% and 90% steps
     *   the accent on ink                        #3B97DE Light Blue -> #00FFFF Now Turquoise
     *   the raised step and the rule on ink      #24376F, #6675A3   -> #2A2AA8
     *   the small label on ink                   #9FB6E4            -> #B6F0FF Ice Blue
     * Status colours keep their 2024 values: --colour-info stays Sea Blue, so an information
     * message never reads as body text or a link.
     *
     * NOW TURQUOISE IS ONLY EVER USED ON LASTING BLUE. On white it is 1.25:1 - no text, no
     * mark. bin/check-design-tokens.php refuses a semantic token that points at it unless the
     * token's name says it lives on ink.
     */
    --nn-lasting-blue: #00008f;         /* rgb(0, 0, 143) */
    /* Repeated as a hex in public/img/chevron-down.svg (a select's chevron is a background image;
     * the CSP allows no data: image and a select takes no pseudo-element). Change both together. */
    --nn-now-turquoise: #00ffff;        /* rgb(0, 255, 255) */
    --nn-ice-blue: #b6f0ff;             /* rgb(182, 240, 255) */

    /* === Tints ==========================================================================
     * [NOVO] "You are allowed to use tints from 0-100% of True Blue and the secondary
     * palette", used alongside True Blue and Snow White, lighter tints on larger surfaces.
     *
     * The official swatch assets render their tint scale as WHITE OVERLAID on the base at
     * 0.1-0.9 alpha. Compositing white at alpha a over a base is, per channel,
     * a*255 + (1-a)*base — which is exactly what color-mix(in srgb, base p%, white) computes
     * for p = 1-a. The two models are arithmetically identical, so color-mix reproduces the
     * official scale rather than approximating it. It is used here so a tint cannot drift
     * from the colour it is a tint of.
     *
     * Named by the proportion of the BASE colour, matching how the manual states the rule
     * ("tints from 0-100% of True Blue"): --nn-true-blue-70 is 70% True Blue, 30% white.
     * The decile steps below mirror the nine steps the official asset illustrates; the
     * manual permits the full 0-100% range, so other percentages are legitimate — use
     * color-mix in the same form rather than pasting a computed hex.
     *
     * Contrast on Snow White, computed from the official value:
     *   90% 12.22:1   80% 8.84:1   70% 6.29:1   60% 4.53:1   <- AA for normal text
     *   50%  3.33:1                                          <- UI/large text only
     *   40% and below                                        <- decorative surfaces only
     */
    --nn-true-blue-90: color-mix(in srgb, var(--nn-true-blue) 90%, var(--nn-snow-white));
    --nn-true-blue-80: color-mix(in srgb, var(--nn-true-blue) 80%, var(--nn-snow-white));
    --nn-true-blue-70: color-mix(in srgb, var(--nn-true-blue) 70%, var(--nn-snow-white));
    --nn-true-blue-60: color-mix(in srgb, var(--nn-true-blue) 60%, var(--nn-snow-white));
    --nn-true-blue-50: color-mix(in srgb, var(--nn-true-blue) 50%, var(--nn-snow-white));
    --nn-true-blue-30: color-mix(in srgb, var(--nn-true-blue) 30%, var(--nn-snow-white));
    --nn-true-blue-20: color-mix(in srgb, var(--nn-true-blue) 20%, var(--nn-snow-white));
    --nn-true-blue-10: color-mix(in srgb, var(--nn-true-blue) 10%, var(--nn-snow-white));

    /* Two steps of Lasting Blue, in the same model, for the primary action's hover and
     * pressed states only. The rest of True Blue's scale above stays in use: its 60% step IS
     * the neutral edge #6675A3 the boards draw, and the 2026 CVI leaves neutrals unchanged.
     * Lasting Blue's own light tints lean violet (30% is #B2B2DD) and no board draws them. */
    --nn-lasting-blue-90: color-mix(in srgb, var(--nn-lasting-blue) 90%, var(--nn-snow-white));
    --nn-lasting-blue-80: color-mix(in srgb, var(--nn-lasting-blue) 80%, var(--nn-snow-white));

    /* [NOVO] Iconostrations sit on white or on a 15% secondary tint when strokes are True
     * Blue. Provided as the documented form for that specific use. */
    --nn-tint-15-sea-blue: color-mix(in srgb, var(--nn-sea-blue) 15%, var(--nn-snow-white));
    --nn-tint-15-ocean-green: color-mix(in srgb, var(--nn-ocean-green) 15%, var(--nn-snow-white));

    /* === The locked design's own values =================================================
     * [PROJECT, APPROVED] The boards in the locked Design Overview draw specific hex values for
     * the grounds, hairlines, tints and quiet text the product is built from. Those values are
     * the visual specification, and where they differ from a tint computed off the official
     * palette, THE BOARD WINS. That is the decision, taken deliberately: a derived tint that is
     * four or six steps away in one channel is invisible on its own and visible when a hairline
     * drawn one way meets a hairline drawn the other on the same screen.
     *
     * WHY THEY LIVE HERE AND NOT IN THE SEMANTIC LAYER. bin/check-design-tokens.php requires
     * every semantic token to point at a token rather than carry a value of its own, so that
     * "change the brand in one file" stays true. These are that file. The semantic layer below
     * still points at names, and a board value is changed in exactly one place.
     *
     * RELATION TO THE BRAND PALETTE. Every one of these is a neutral, a tint or a quiet text
     * colour — none replaces an official colour, and the 2024 and 2026 palettes above remain
     * the only brand values. [NOVO] permits tints of the palette; these
     * are what the approved design drew when it took them.
     *
     * CONTRAST, measured, against the surface each one is actually used on:
     *   #4D5E93 on #FFFFFF        6.29:1     #4D5E93 on #F5F2ED        5.63:1 (the 2026 sand ground, gone in v4)
     *   #00008F on #F5F2ED       13.47:1     #00008F on #EDF1FA       13.29:1
     *   #C9D6F0 on #00008F       10.28:1     #C9D6F0 on #2A2AA8        7.20:1
     *   #FFFFFF on #2A2AA8       10.53:1     #00FFFF on #00008F       11.99:1
     *   #2A6B22 on #E9F4E8        5.76:1     #8A6400 on #FFF6E5         5.01:1
     *   #4D5E93 on #F1EEE9        5.44:1     #00008F on #EAAB00         7.39:1
     * Every one of those clears AA for normal text. The field hairlines are #6675A3 since the
     * Foundations token pass — 4.53:1 on white — so they clear 1.4.11 and may bound a control on
     * their own; the pale card hairlines still may not. One value is deliberately below a text
     * threshold and is recorded where it is defined: #B0562F. (#9AA2B5 was the other and is
     * gone: it was the locked day's words at 2.15:1, and the exemption claimed for it did not
     * reach a card holding no control.)
     */

    /* The page's ground. WHITE SINCE DESIGN V4 (Novo, 2026-10-01): the 2026 Brand Book and CVI
     * put everything on white, and Novo chose the conservative look, so the warm sand ground the
     * 2026 boards drew (#F5F2ED) is gone. A card is told from the ground by its hairline and its
     * soft shadow now, not by standing on a different colour. */
    --board-ground: #ffffff;

    /* The hairline around a panel, and the quieter rule inside one: design v4's one cool rule,
     * #E6E8F2, where the boards drew a warm pair (#E9E4DC, #EDE8E1) to sit on the sand. A hairline
     * carries no text; 1.22:1 on white, a separator and never a control's only boundary. */
    --board-rule: #e6e8f2;
    --board-rule-inner: #e6e8f2;

    /* Design v4's tint: Lasting Blue at 5% on white, for a band and a table head. As drawn
     * (#F2F2FA) rather than computed — color-mix rounds the blue channel to #F9. Lasting Blue on it
     * is 13.50:1, the muted voice 5.65:1. */
    --board-v4-tint: #f2f2fa;

    /* Standings (the advisor's target of 2026-10-04 13:30): the reader's own row, a step stronger
     * than the v4 tint, and the top three teams' rank circles in pale gold, silver and bronze. As
     * the target draws them; they are not tints of one palette colour, which is recorded with the
     * board (Standings.dc.html) for the advisor. Lasting Blue on them: 12.47:1, 13.07:1, 12.71:1
     * and 11.88:1. */
    --board-own-row: #e6e9fb;

    /* My steps (the advisor's polish target of 2026-10-04 14:00, steps.html). The "days missing"
     * chip in a calmer voice: a pale warm ground and a soft red edge, with the Lava Red mark kept.
     * Its words are Lasting Blue, as all status text is (contract §1; advisor 2026-10-05 01:35 -
     * the target's deep red word was a slip): 14.02:1 on the ground. The mark is 3.41:1. The
     * ground and edge are drawn values near Lava Red, not exact tints of it (no single mix with
     * white gives either). The edge is 2.01:1 against the page (1.88:1 against the chip's ground)
     * and identifies nothing on its own: the words and the mark do. Today's tile takes the same
     * step-stronger tint as the reader's own standings row, and a lift heavier than --shadow-md,
     * the one tile on the page that floats. */
    --steps-missing-ground: #fff5f1;
    --steps-missing-edge: #e7a894;
    --steps-missing-word: var(--colour-text);
    /* A SAVED DAY READS AS DONE AT A GLANCE (Kenneth 6/10: "much more visibly green"; contract §1): a soft green ground and a Forest Green top edge, with the
     * green check beside the word "Gemt", which stays Lasting Blue as all status text does. The
     * ground carries nothing on its own; the word and the check do. The ground is a 9 % Forest Green
     * tint, inside the guide's tint rule (advisor 2026-10-06 12:15; it was Kenneth's #EEF7EC, which it
     * matches to the eye). */
    --steps-saved-ground: color-mix(in srgb, var(--nn-forest-green) 9%, var(--nn-snow-white));
    --steps-saved-edge: var(--colour-success);
    --steps-today-ground: var(--board-own-row);
    --steps-today-lift: 0 10px 28px color-mix(in srgb, var(--colour-primary) 14%, transparent);
    --board-medal-gold: #fbefc5;
    --board-medal-silver: #e9ecf3;
    --board-medal-bronze: #f6e1cf;


    /* A day nobody has reached: a quieter ground than the page's, and what it says on it.
     *
     * THE EXEMPTION THIS CLAIMED DOES NOT APPLY, and it is written out rather than deleted
     * because the next reader will find 1.4.3's exception and reach for it again.
     *
     * It said: #9AA2B5 on #EFEBE4 is 2.15:1, but it is "the label of an INACTIVE control, which
     * WCAG 2.2 1.4.3 exempts from a contrast minimum". The exception is real — text that is part
     * of an inactive user interface component has no contrast requirement. It does not reach
     * here, because a locked day is not an inactive component: it is not a component at all.
     * `grid.blade.php` removes the only control that pointed at one, saying in as many words
     * that "a next link onto a locked day would be a control that leads somewhere nothing can
     * be done" — so the card holds no input, no button, no link, and nothing focusable. A
     * disabled button's label is exempt; a paragraph reading "Not reached yet" is prose.
     *
     * What it paints is content: the DATE of a campaign day (11px bold), the empty mark (17px
     * bold) and the sentence (11px). Measured on /steps at 1440 and 390, all three read 2.15:1
     * against the painted ground. #4D5E93 reads 5.29:1 on the same ground, and is the value
     * Foundations.dc.html gives for it — so the board's own answer is also the accessible one.
     *
     * The one other consumer is .gatecard__chip-dot's background, which is a mark beside a word
     * that carries the meaning and is unaffected by being darker. */
    --board-quiet-ground: #efebe4;
    --board-quiet-text: #4d5e93;

    /* The edge of a field on a day card of the grid (MySteps, 2026-09-24): a step lighter than
     * the field edge, so a row of recorded days reads calm. 3.53:1 on the white card and 3.12:1
     * on today's tint, so it still bounds the control (WCAG 2.2 AA 1.4.11). */
    --board-day-edge: #7c87af;

    /* The edge of a field. Foundations.dc.html draws ONE edge, not two: #6675A3 on either
     * ground, which is 60% True Blue on white and the contract's own tint ladder. The warm
     * #d8d2ca and the cool #c9d2e4 that stood here were the pre-contract pair, and holding them
     * is why components named --nn-true-blue-60 directly rather than the semantic token. Both
     * names are kept because the two grounds are still two grounds; they simply agree now. */
    --board-field-edge-warm: #6675a3;
    --board-field-edge: #6675a3;

    /* The tint every computed value sits on, and the quiet ink-blue text on white. */
    --board-tint: #edf1fa;
    --board-text-muted: #4d5e93;

    /* The equals sign between the converter's question and its answer: arithmetic, not a word.
     *
     * MEASURED ON THE GROUND IT IS ON, which is the correction. This said "3.33:1 on white,
     * which is the non-text threshold" — true of white, and the sign is painted on
     * the converter's #EDF1FA tint, where #7E8CB5 measures 2.94:1. Under 3:1 on either reading:
     * as a graphic (1.4.11) or as 28px bold text (1.4.3's large threshold). #6675A3 is 4.00:1
     * on that tint and 4.53:1 on white, and is the edge colour the fields beside it now take.
     *
     * The words on either side still carry the meaning; this is about the mark being visible,
     * not about it having to be read. */
    --board-arith: #6675a3;

    /* On an ink field: the quiet body voice, and the rule between two rows of it. The rule is
     * the 2026 CVI's raised Lasting Blue (#24376F under True Blue); the body voice is unchanged,
     * 10.28:1 on Lasting Blue and 7.20:1 on the raised step. */
    --board-on-ink-muted: #c9d6f0;
    --board-on-ink-rule: #2a2aa8;

    /* The four states, each a ground, a mark and a word. The mark carries the colour; the word
     * is only ever set in a value that clears AA on the ground beside it.
     *
     * #B0562F is the exception and stays one: 4.27:1 on its own rose ground, which is under the
     * 4.5:1 a word needs. Where the product would otherwise set a word in it — the missed day's
     * flag — the word takes --colour-text and the four-pixel rule carries the colour instead,
     * where 1.4.11 asks 3:1 of a graphic rather than 4.5:1 of a word. */
    --board-positive-ground: #e9f4e8;
    --board-positive-text: #2a6b22;
    --board-pending-ground: #fff6e5;
    --board-pending-text: #8a6400;
    --board-negative-ground: #fbe9ef;
    --board-negative-text: #b0562f;
    --board-closed-ground: #f1eee9;

    /* Main.dc.html's own four, which no other board draws:
     *
     *   #E4DFD8  the hairline the landing uses under its bar and above its fact row — a step
     *            warmer than the panel hairline, 1.19:1 on the ground, carrying no text
     *   #B6F0FF  the small uppercase label inside the campaign panel: Ice Blue in the 2026
     *            CVI (#9FB6E4 under True Blue), 12.10:1 on Lasting Blue
     *   #E7EEF9  and #E4F1F0, the two tinted tiles the "three steps" cards put an icon on;
     *            Lasting Blue on them is 12.88:1 and 12.99:1
     */
    --board-rule-landing: #e6e8f2; /* design v4's one rule (was #E4DFD8, a warm step for the sand) */
    --board-on-ink-label: var(--nn-ice-blue);
    --board-tile-blue: #e7eef9;
    --board-tile-green: #e4f1f0;

    /* The rest of what the boards draw, found by counting every declaration in all 28 files
     * rather than by meeting them one screen at a time. Each is on two boards or more.
     *
     *   #F6F4F1  a quiet row inside a white panel — a template that has not been edited, a
     *            captured message, a locked block. Ink on it is 14.42:1
     *   #DED8CF  the left rule on a day nobody has reached, where the warm hairline would
     *            disappear into the quiet ground it stands on
     *   #EDE9E3  the ground of a Locked or Read-only chip; #4D5E93 on it is 5.20:1
     *   #14524E  the word in a Participant chip, 7.73:1 on its #E4F1F0 tile
     *   #1A3074  a raised block inside an ink panel: lighter than the Lasting Blue panel
     *            (1.23:1) and darker than the #2A2AA8 raised step, which is the rule rather
     *            than a surface here. v17 keeps it. White on it is 12.22:1
     *   #E3F0FB  the tile behind a mail icon on a guest screen
     *   #D7E1F4  the rule inside the converter's tinted panel, where the warm rule reads as a
     *            different material against the blue
     */
    --board-quiet-row: #f6f4f1;
    --board-rule-quiet: #ded8cf;
    --board-locked-ground: #ede9e3;
    --board-participant-text: #14524e;
    --board-on-ink-raised: #1a3074;
    --board-tile-mail: #e3f0fb;

    /* The illustration tile of the 2026 boards (their --tile), behind an icon on a guest or a
     * participant card. Lasting Blue on it is 13.48:1. Named here for the screens that move to
     * the 2026 boards; the three tiles above serve the screens that have not yet. */
    --board-tile: #ddf7ff;
    --board-rule-on-tint: #d7e1f4;

    /* === Semantic colour ================================================================
     * [PROJECT] The manual defines a palette, not interface meaning. This mapping is ours,
     * constrained by the manual's rules AND by measured contrast. Components use ONLY these,
     * never --nn-* directly.
     */
    --colour-surface: var(--nn-snow-white);
    --colour-surface-sunken: var(--nn-true-blue-10);
    --colour-surface-raised: var(--nn-snow-white);

    /* === The application's own light-blue ground =========================================
     * [NOVO] permits tints from 0-100% of True Blue AND of the secondary palette, and asks for
     * "a lot of white space to create a calm and clear design" with lighter tints on larger
     * surfaces. The approved Mine skridt design is built on exactly that: white cards floating
     * on a pale Light Blue field, with a slightly stronger tint marking the places a number is
     * typed or read.
     *
     * TINTS OF AN OFFICIAL COLOUR, NOT THE MOCK-UP'S HEX VALUES. The reference renders these a
     * few units more cyan than a true Light Blue tint. Copying its hex would put four
     * unaccounted brand values in the product, so these are stated as tints of #3b97de — the
     * difference is under five units per channel and invisible, and the values stay derivable.
     *
     * CONTRAST, measured against the strongest of them (12%): Lasting Blue text is 13.33:1, so
     * every one of these carries body text at AAA.
     */
    /* The page's own ground is a step paler than the hero band on it, so the band reads as a
     * band without needing the wave field the reference draws — which section 3 bans as a
     * gradient. Two tints, one step apart, doing the job one shaded shape does there. */
    --colour-surface-app: color-mix(in srgb, var(--nn-light-blue) 5%, var(--nn-snow-white));
    /* THE BOARD'S OWN #EDF1FA, not a Light Blue tint. Every computed value in the product sits
     * on this — the day's total, the converter's answer, the selected row, the audience block —
     * and it is the one tint that appears beside a hairline drawn from the same drawing.
     * Lasting Blue on it is 13.29:1. */
    --colour-surface-field: var(--board-tint);

    /* Design v4's band and table-head tint: Lasting Blue at 5%. A section that separates one idea
     * from the next ("How it works") and the head of a table stand on it. */
    --colour-surface-band: var(--board-v4-tint);

    /* Standings: the reader's own row, and the top three teams' rank circles. The circles are
     * decoration — the rank is printed in Lasting Blue on each — so no colour carries the place on
     * its own. */
    --colour-surface-own-row: var(--board-own-row);
    --colour-medal-gold: var(--board-medal-gold);
    --colour-medal-silver: var(--board-medal-silver);
    --colour-medal-bronze: var(--board-medal-bronze);

    /* Design v4's Ice Blue, and only where the 2026 CVI puts it: a small round icon tile, and the
     * one small footprints trail a page may carry. Never a ground for text beyond the tile's icon
     * (Lasting Blue on it is 12.1:1). */
    --colour-surface-tile: var(--nn-ice-blue);
    --colour-surface-badge: color-mix(in srgb, var(--nn-light-blue) 16%, var(--nn-snow-white));

    /* The hero band. Named separately from the app ground because the approved illustration is
     * an image with this tint baked into it — if the two drifted apart, the artwork would show a
     * rectangle around itself. */
    --colour-surface-hero: color-mix(in srgb, var(--nn-light-blue) 8%, var(--nn-snow-white));

    /* The hairline around a white card on that ground, and around a field inside one. Too pale
     * to bound a control on its own — that is what --colour-border-strong is for — and exactly
     * right for separating one surface from another. */
    --colour-border-soft: color-mix(in srgb, var(--nn-light-blue) 22%, var(--nn-snow-white));

    /* === Ink as a SURFACE: Lasting Blue since the 2026 CVI (True Blue before it) ============
     * [NOVO] "Use True Blue and Snow White as much as possible." The manual's own artwork sets
     * white type on full True Blue fields, and supplies a white wordmark specifically for "dark
     * or photographic backgrounds" — so a True Blue surface is the brand's own register, not an
     * invention. It is used for the things that frame the product rather than the things inside
     * it: the application bar, the footer, and one band per public page. White on Lasting Blue
     * is 15.04:1 (it was 15.83:1 on True Blue).
     *
     * TWO STEPS, because a bar and a panel inside a dark band must be distinguishable without a
     * gradient ([NOVO] bans those). The raised step is the 2026 boards' #2A2AA8 (their
     * --raised; #24376F under True Blue), drawn wherever a panel or a rule sits on ink. White on
     * it is 10.53:1 and the quiet body voice is 7.20:1.
     */
    --colour-surface-ink: var(--nn-lasting-blue);
    --colour-surface-ink-raised: var(--board-on-ink-rule);

    /* === Warmth — WHITE SINCE DESIGN V4 (2026-10-01) =======================================
     * The name is historical. This token is the page's ground and resolves to --board-ground, which
     * is white since v4: Novo chose white everywhere, so what follows describes the sand ground it
     * replaced and its contrast is a record, not a current figure. A rule that wanted a contrast
     * AGAINST white rather than the ground takes --colour-surface-band (the v4 layer in app.css).
     *
     * What it was: one warm ground so a long public page has rhythm rather than one continuous blue wash.
     * Sand Grey is a large, light surface carrying a tint of a secondary colour, which is
     * exactly the use [NOVO] permits ("larger surfaces take lighter tints").
     *
     * THE BOARD'S #F5F2ED, not a Sand Grey tint. It is not itself a tint of anything in the
     * palette — reaching it from white needs 19.6% of Sand Grey in red, 22.4% in green and 27.3%
     * in blue, which is three figures and therefore three different tints — and a 20% Sand Grey
     * tint resolved to #f5f3f2, five steps away in one channel. Both were defensible; the board
     * is the specification, and this is the ground every panel, hairline and tint on it was
     * drawn against.
     *
     * Lasting Blue text on it measures 13.47:1, and the muted voice 5.63:1.
     */
    --colour-surface-warm: var(--board-ground);

    /* ROSE MEANS INVALID OR FAILED, AND NOTHING ELSE (ACC-006 decision). The ground of a state
     * that went wrong — the expired link's chip (GuestAuth.dc.html) — and never an accent for
     * neutral facts: the campaign dates and the no-team card stand on the tint,
     * --colour-surface-field. Rose Pink at 25% resolves to #fbe9ef, the boards' value.
     *
     * SINCE DESIGN V4 (2026-10-01) NOTHING STANDS ON IT: the chip is a hairline pill on white with
     * the red dot beside its word (the v4 sign-in layer in app.css overrides the chip's ground), and
     * the old landing's materials band and rose how-it-works tile went with the v4 front page. The
     * token stays until the chip's base rule is rewritten.
     *
     * A SURFACE, NEVER A TEXT COLOUR. Lasting Blue on it measures 12.89:1; Rose Pink itself carries
     * no text anywhere in the product. */
    --colour-surface-rose: color-mix(in srgb, var(--nn-rose-pink) 25%, var(--nn-snow-white));

    /*
     * === THE WARM NEUTRALS THE LOCKED BOARDS DRAW ==========================================
     *
     * The product's borders were Light Blue and True Blue tints — correct on a white page, and
     * wrong on this one. The locked design stands every card on the warm Sand Grey ground, and
     * draws its hairlines, its quiet fields and its untouched days in the SAME warm family. A
     * cool blue-grey hairline around a card on a sand ground reads as a different material.
     *
     * THE DRAWN HEXES, NOT TINTS OF THEM. These were stated as Sand Grey tints, each within four
     * steps of the board in one channel, on the argument that the palette is authoritative over
     * a value read off a drawing. That is settled the other way now: the board file is the
     * visual specification, and a hairline four steps off the drawing is visible the moment it
     * meets one that is not.
     *
     * NONE OF THEM CARRIES TEXT. Every one is a hairline, a disabled ground or the edge of a
     * control, so 1.4.11's 3:1 applies where it applies and no text threshold does.
     */

    /* The hairline around a card standing on the warm ground. */
    --colour-rule-warm: var(--board-rule);

    /* One step quieter: the rule inside a day card, and the track the entry ring runs on. */
    --colour-rule-warm-soft: var(--board-rule-inner);

    /* The same hairline where it crosses a tinted panel. The warm rule is a paper edge and reads
     * as a different material against the converter's blue, so the boards draw this one instead —
     * MyStepsMobile.dc.html closes the converter band with it. */
    --colour-rule-on-tint: var(--board-rule-on-tint);

    /* A campaign day nobody has reached yet: no border, no field, just a quieter ground than the
     * page's. */
    --colour-surface-quiet: var(--board-quiet-ground);

    /* The edge of an input that is not the one being filled in. 4.53:1 on white and ~4.0:1 on
     * the grounds it is drawn on, measured at 1440 and 390 — so it now clears 1.4.11's 3:1 and
     * CAN be a control's only marking, which it could not at the 1.50:1 it used to hold. */
    --colour-border-warm: var(--board-field-edge-warm);

    /* What a day nobody has reached says, on that quiet ground: 5.29:1, measured on /steps at
     * 1440 and 390. It was 2.15:1 and argued to be exempt as "the label of an INACTIVE control";
     * the primitive's own comment says why that exemption does not reach a card holding no
     * control at all. */
    --colour-text-quiet: var(--board-quiet-text);

    /* The edge of a control standing on the field tint, and of the small boxes inside a day
     * card. 4.53:1 on white, 4.00:1 on the tint — it clears 1.4.11's 3:1 and may be a control's
     * only marking, which the 1.52:1 it used to hold could not. `.roster-table__action` names
     * this token because of it. */
    --colour-border-field: var(--board-field-edge);

    /* The equals sign between the converter's question and its answer: arithmetic, not a word.
     * 4.00:1 on the tint it is painted on, which clears the 3:1 a graphic is held to and the
     * same threshold 28px bold text is held to. It was 2.94:1 there, measured 3.33:1 on a white
     * it never sits on — see the primitive. */
    --colour-text-arith: var(--board-arith);

    /* [NOVO] "Never use black text. Use True Blue for dark text." The 2026 CVI puts Lasting
     * Blue in True Blue's place. */
    --colour-text: var(--nn-lasting-blue);         /* 15.04:1 on white — exceeds AAA */
    /* The board's #4D5E93, which a 70% True Blue tint already resolved to exactly. Stated as
     * the board value so the whole quiet voice reads from one place. */
    --colour-text-muted: var(--board-text-muted); /*  6.29:1 on white (the ground since v4), 5.65:1 on the band tint */
    --colour-text-on-primary: var(--nn-snow-white);
    --colour-text-inverse: var(--nn-snow-white);

    /* The accent ON an ink field: an eyebrow above a white heading, or the rule
     * around a step numeral. White is the body colour there, so a second voice is needed that
     * is still clearly subordinate to it and still legible.
     *
     * [PROJECT] It lives here rather than in the component that wanted it because a raw
     * --nn-* used as a text colour inside a stylesheet is invisible to
     * bin/check-design-tokens.php, which reads this file alone — the accent would then be the
     * one text colour in the product with no contrast record. */
    --colour-accent-on-primary: var(--nn-now-turquoise); /* 11.99:1 on Lasting Blue; never on a light ground */

    /* Waiting (sign-ups open, nothing counted yet) on an ink field, as PreStart draws it. */
    --colour-waiting-on-primary: var(--nn-golden-yellow); /* 7.39:1 on Lasting Blue; never on a light ground */

    /* THE QUIET BODY VOICE ON AN INK FIELD, as distinct from the accent above it. The
     * campaign panel on the landing page states four figures on ink, and each of them needs a
     * label, a note or a date beside it that is clearly subordinate to the numeral without being
     * the accent on ink — using the accent for both left the panel with one voice and no
     * hierarchy. The board draws #C9D6F0 — a white stepped down rather than a second hue, so it
     * reads as the same text one shade quieter. It was a 20% True Blue tint, three steps off in
     * one channel and sixteen in another; the board is the specification.
     *
     * 10.28:1 on Lasting Blue and 7.20:1 on the raised step beside it, both comfortably AA. */
    --colour-text-on-primary-muted: var(--board-on-ink-muted);

    /* The same job on white: the eyebrow above a heading on an ordinary surface, and the link
     * colour. It is Lasting Blue, as --colour-primary is, and it is named separately because it
     * MEANS static text on white — a change to the primary action must not restyle it. */
    --colour-accent-on-surface: var(--nn-lasting-blue); /* 15.04:1 on white; the 2026 link colour */

    --colour-primary: var(--nn-lasting-blue);
    --colour-primary-hover: var(--nn-lasting-blue-80); /* #3333A5; white on it 9.73:1 */
    --colour-primary-active: var(--nn-lasting-blue-90); /* #1A1A9A; white on it 12.66:1 */

    /* THE HAIRLINE ON THE WARM GROUND. --colour-border-soft is a Light Blue tint made for a
     * hairline on white, where it is a visible tint; against the warm Sand Grey ground it is
     * 1.06:1 — the same luminance in a different hue, which is not a boundary at all. The bars
     * that frame the product stand on that ground, so they need one that survives it: a 30% True
     * Blue tint reaches 3.0:1 there, which clears the non-text threshold a control's boundary is
     * held to (WCAG 2.2 AA 1.4.11) rather than merely being visible in good conditions. */
    --colour-border-on-warm: var(--nn-true-blue-30);

    --colour-border: var(--nn-true-blue-30);
    --colour-border-strong: var(--nn-lasting-blue);

    /* HAIRLINES ON THE INK FIELD: the 2026 boards' raised step #2A2AA8, the same value as
     * --colour-surface-ink-raised, so ink carries one rule colour rather than two (advisor,
     * 2026-09-25). It separates rows and marks a finished campaign's dot, whose state the words
     * beside it also say. It is 1.43:1 on Lasting Blue: a separator, NEVER the only boundary of a
     * control, and never the only carrier of a state (WCAG 2.2 AA 1.4.1, 1.4.11). Before the
     * CVI it was True Blue's 60% tint, 3.50:1 on True Blue. */
    --colour-rule-on-primary: var(--board-on-ink-rule);

    --colour-focus: var(--nn-lasting-blue);
    /* THE BOARD'S PAIR, AND IT ALSO MEASURES BETTER. Foundations.dc.html draws a disabled
     * control as #4D5E93 on the warm #F1EEE9: 5.44:1, against the 5.14:1 of the True Blue 70/10
     * pair that stood here. So the look decision and the measurement point the same way, which
     * is the only reason this was a transcription rather than a question for Kenneth.
     *
     * The note this replaces is worth keeping in mind rather than in the file: the 60% tint was
     * rejected here because it is 4.53:1 on white but 3.70:1 on the sunken surface it actually
     * sits on. Same lesson as the equals sign above — a token is only as good as the ground it
     * is measured against. */
    /* --board-closed-ground, because a closed control and a disabled one are the same warm grey
     * on the board; the gate is right that a semantic token may not hold a raw hex.
     *
     * DELIBERATELY COUPLED, AND SPLIT THE MOMENT THE BOARD SEPARATES THEM. That primitive is also
     * consumed directly for the closed chip, so a board edit to the closed ground would silently
     * restyle every disabled control. If the two ever differ, add --board-disabled-ground and
     * point this at that instead of editing the shared one. */
    --colour-disabled-surface: var(--board-closed-ground);
    --colour-disabled-text: var(--board-text-muted);  /* 5.44:1 on the disabled surface */

    /* --- Status ---------------------------------------------------------------------------
     * Measured, and the measurement changes the design. On Snow White:
     *
     *   Lava Red       3.66:1    Forest Green  3.49:1    Ocean Green   3.80:1
     *   Light Blue     3.15:1    Golden Yellow 2.03:1    Rose Pink     1.92:1
     *   Granite Grey   2.83:1    Sand Grey     1.71:1    Sea Blue      6.19:1
     *
     * Only True Blue and Sea Blue reach 4.5:1. So a status colour is an ACCENT — a border,
     * an icon, a rule — and never the status text itself, and never the sole carrier of the
     * meaning. Text stays --colour-text; wording and iconography carry the status.
     *
     * This is not us overriding the brand. The manual already conditions it: text may use
     * "other palette colours" only "if readable", and it requires at least AA. Readability
     * decides, and it has.
     *
     * Golden Yellow (2.03:1) and Rose Pink (1.92:1) do not even reach the 3:1 non-text
     * threshold, so they must not be used as a lone indicator against white at all.
     */
    --colour-danger: var(--nn-lava-red);           /* accent only — 3.66:1 */
    --colour-success: var(--nn-forest-green);      /* accent only — 3.49:1 */
    --colour-warning: var(--nn-golden-yellow);     /* accent only — 2.03:1, needs a companion */

    /* The edge of a day nobody has reached yet, on the day grid and its legend (MySteps, 2026):
     * Sand Grey, decoration beside the words that say it — never the only carrier of the state. */
    --colour-edge-unreached: var(--nn-sand-grey);
    --colour-info: var(--nn-sea-blue);             /* 6.19:1 — safe for text */

    /* Text colour for a status message. Deliberately not the status colour. */
    --colour-status-text: var(--colour-text);

    /* === Typography =====================================================================
     * [NOVO] "Always use Apis."
     *
     * The fallback is Arial, and only Arial: "Please use Arial for text if you are not able
     * to use Apis or Apis for Office."
     *
     * Noto Sans is NOT a Latin fallback. The manual restricts it to non-Latin alphabets —
     * "only in this instance" — so it has its own token below. Apis being based on Noto Sans
     * is a fact about how Apis was made, not permission to substitute it.
     */
    /* DESIGN V4 (2026-10-01): the Novo Brand Book of 15 September 2026 names the Novo typeface.
     * Since 2026-10-02 the product serves it itself (the @font-face rules at the top of this
     * file), so every page renders in Novo; Apis and Arial are reached only while a file loads,
     * if it fails, or for a glyph Novo lacks. The mails load no web font, so they render in Arial
     * unless the reader has Novo installed.
     * Apis stays in the stack because bin/check-design-tokens.php (a protected gate) requires it;
     * dropping it is a Class C change, recorded in docs/task-briefs/design-v4-foundation.md. */
    --font-family-base: "Novo", "Apis", Arial, sans-serif;

    /* [NOVO] Non-Latin alphabets only. Never a general fallback. */
    --font-family-non-latin: "Noto Sans", Arial, sans-serif;

    /* [NOVO] Avoid condensed and wide weights. */
    --font-weight-regular: 400;
    --font-weight-medium: 500;

    /* The weight the locked boards set on a navigation destination, a hero fact and the person's
     * own name: heavier than body text, lighter than a heading, and Apis carries it. */
    --font-weight-semibold: 600;
    --font-weight-bold: 700;

    /* Design v4's headings, wordmark and figures (the v4 pages set them at 800). Arial and the
     * fallback faces have no 800 and draw it as their bold; the Novo face, once served, has one. */
    --font-weight-heavy: 800;

    /* THE WEIGHT LADDER BY ROLE (audit 1006, D1; Kenneth's designer: "too many bold texts compete").
     * 800 the H1 and the front hero only; 700 card titles, primary buttons, the one key figure per
     * tile or row, a board's ranked value and the own row; 600 table names, accordion questions, chip
     * labels, eyebrows and table heads, the active nav item and segment; 500 field labels; 400 body,
     * helper text, a table's secondary columns and key/value values. Applied in app.css's last layer. */
    --weight-display: var(--font-weight-heavy);
    --weight-strong: var(--font-weight-bold);
    --weight-emphasis: var(--font-weight-semibold);
    --weight-label: var(--font-weight-medium);
    --weight-plain: var(--font-weight-regular);

    /* [PROJECT] The manual specifies no sizes or line spacing. A restrained scale, sized in
     * rem so it honours the user's browser text size. */
    --font-size-xs: 0.8125rem;
    --font-size-sm: 0.9375rem;
    --font-size-base: 1rem;
    --font-size-lg: 1.125rem;
    --font-size-xl: 1.375rem;
    --font-size-2xl: 1.75rem;
    --font-size-3xl: 2.25rem;

    /* The figure a screen is built around: the three numbers at the top of Mine skridt, and the
     * day's running total. Larger than any heading on the page, deliberately — the number is the
     * content and the label above it is the caption. */
    --font-size-4xl: 2.75rem;

    /* THE DISPLAY SIZE, and the only fluid one.
     *
     * A campaign front page has to carry the campaign, and 2.25rem does not do that on a
     * desktop while being right on a phone. clamp() resolves that without a breakpoint: it
     * starts at the 3xl the rest of the scale uses, grows with the viewport, and stops at
     * 3.5rem so a very wide window does not turn the heading into a poster.
     *
     * It is used by the public landing hero only. Application screens keep the fixed scale,
     * because a fluid heading inside a data view moves when a table forces a scrollbar. */
    /* THE ONE PAGE IN THIS PRODUCT THAT IS A POSTER. The ceiling was 3.5rem, chosen to stop
     * the landing page becoming one; beside a 393px form panel on a 1280px field, 56px read as
     * a sub-heading rather than as the campaign's title. 40px at 390, 72px from 1280. */
    --font-size-display: clamp(2.5rem, 1.5rem + 3.75vw, 4.5rem);

    /* THE TITLE OF AN APPLICATION SCREEN, and the figure a screen is built around.
     *
     * Fluid like the display size and for the same reason — a screen title that is right on a
     * phone is timid on a laptop — but with a much shorter travel, because these sit inside
     * layouts with data in them. Ranges chosen so the step is always visible against
     * --font-size-2xl below it, at every viewport.
     */
    --font-size-title: clamp(2rem, 1.4rem + 2.2vw, 3rem);
    --font-size-figure: clamp(2.75rem, 2rem + 3vw, 4rem);

    /* THE SYSTEM VALUES, ONE PLACE EACH (the advisor's final consistency round, 2026-10-05 16:20).
     * A page's H1 is 44 from the tablet and 34 on a phone on every participant page; the front
     * hero (58/36) and the admin (36/24) keep theirs, and a gate card's title is 32/28. */
    --font-size-h1: 2.75rem;
    --font-size-h1-phone: 2.125rem;
    --font-size-gate-title: 2rem;
    --font-size-gate-title-phone: 1.75rem;

    /* The eyebrow: one style, 12/800, tracked .14em, in capitals (--tracking-eyebrow). */
    --font-size-eyebrow: 0.75rem;

    /* A table's head: 12/800, tracked .1em, in capitals. */
    --font-size-table-head: 0.75rem;
    --tracking-table-head: 0.1em;

    /* The status chip beside a page head: a tint pill, an 8px dot, the word at 14/700. */
    --font-size-chip: 0.875rem;
    --chip-dot: 8px;
    --chip-padding-block: 6px;
    --chip-padding: var(--chip-padding-block) 12px;

    /* The quiet link: 15/600, underlined 3px below the words, an SVG arrow where it leads on. */
    --font-size-quietlink: 0.9375rem;
    --quietlink-underline-offset: 3px;

    /* The card's padding: 24 from the tablet, 20 on a phone (its corners are --radius-2xl). */
    --card-padding: 1.5rem;
    --card-padding-phone: 1.25rem;

    --line-height-tight: 1.25;

    /* THE FACE'S OPTICAL OFFSET (2026-10-06, measured from the ink at 4x): its capitals and figures sit
     * about 0.08em above a box's centre, so a label centred in a pill, a circle or a chip moves down
     * by this much to look centred (the designer's "perfectly aligned"; app.css, the last layer). */
    --optical-nudge: 0.08em;

    /* Display only. At 72px, 1.25 opens eighteen pixels between two lines of a title that has
     * to read as one shape. */
    --line-height-display: 1.1;
    --line-height-base: 1.55;   /* Foundations.dc.html: body copy sits at 15-18px with 1.55 */

    /* Display and title sizes only. At 48–72px the default tracking opens the word out; a
     * heading that has to read as one shape needs it closed slightly. Never applied to body
     * text, where it would cost legibility. */
    --tracking-tight: -0.02em;

    /* One value for uppercase micro-labels — the month separators and the Today badge. Two
     * arbitrary trackings for the same typographic job was the start of a scale that lived
     * nowhere; the manual specifies no tracking at all, so this is a [PROJECT] decision and
     * belongs here rather than in a rule. */
    --tracking-caps: 0.05em;

    /* Design v4's small uppercase eyebrows and status words: wider than the micro-labels above,
     * as the v4 pages track them. */
    --tracking-eyebrow: 0.14em;

    /* And one for a string of characters somebody has to read aloud or type: the MFA setup key
     * and the six-digit code field. A different job from the micro-labels above — legibility of
     * an individual character rather than the texture of a small heading — so a second token
     * rather than a second use of the first. Named here, for the reason recorded above: this is
     * exactly where two arbitrary values would otherwise start living in rules. */
    --tracking-code: 0.08em;

    /* === Spacing ========================================================================
     * [PROJECT] No spacing scale is published. A 4px-based scale, generous by default,
     * because [NOVO] makes whitespace the defining characteristic: "Use a lot of white
     * space to create a calm and clear design."
     */
    --space-3xs: 0.25rem;
    --space-2xs: 0.5rem;
    --space-xs: 0.75rem;
    --space-sm: 1rem;
    --space-md: 1.5rem;
    --space-lg: 2rem;
    --space-xl: 3rem;
    --space-2xl: 4rem;

    /* One step beyond 2xl, for the vertical breathing room a public page needs and an
     * application screen does not. Kept in the same doubling rhythm as the rest. */
    --space-3xl: 6rem;

    --layout-measure: 42rem;            /* readable line length */

    /* THE PRODUCT'S COLUMN. Every screen's content sits on this measure — the participant
     * screens, the admin screens and the bar and footer that frame them — so the frame and the
     * page line up down the whole product. 73rem less the gutters is the 1120px the approved
     * admin references draw content in at 1440. */
    --shell-measure: 73rem;

    /* The wide measure a full-bleed public section uses for its inner content. Distinct from
     * --layout-measure because a hero and a three-column band are not bound by the same rule
     * a paragraph is; both were literals in public.css until they were named here. */
    --layout-measure-wide: 64rem;

    /* A single-field form panel in the one-column range. Without it the hero's panel stretched
     * to the full measure and put a 60rem-wide box around one email address. */
    --layout-measure-narrow: 32rem;
    --layout-gutter: var(--space-sm);

    /* === Borders, radii, elevation ======================================================
     * [PROJECT] Not specified by the manual. Kept restrained: [NOVO] bans gradients outright
     * and bans effects on type, so heavy elevation would sit against the grain.
     */
    --border-width: 1px;
    --border-width-strong: 2px;
    /* Wide enough to carry a distinction on its own, for the cases where the colour cannot:
     * most of the secondary palette sits near 3:1 against a tinted surface, so an accent rule
     * that must be noticed needs width as well as hue. */
    --border-width-accent: 6px;
    --radius-sm: 2px;
    --radius-md: 4px;

    /* THE APPROVED DESIGN'S CARD RADIUS. Mine skridt is built from rounded cards, and 4px does
     * not read as rounded at card scale — it reads as a printing error. [PROJECT], like the rest
     * of this group: the manual specifies no radii. Two steps, because a field inside a card must
     * be visibly less round than the card holding it or the two corners fight. */
    --radius-lg: 8px;
    --radius-xl: 14px;

    /* The panel radius. A card at 640px wide needs a corner you can see from across the room;
     * 14px reads as "nearly square" at that size. The two smaller steps stay for the things
     * INSIDE a panel, which must be visibly less round than the panel holding them. */
    --radius-2xl: 20px;   /* the card: 20 everywhere, at every width (the final consistency round, 2026-10-05) */

    /* The blue figure band (partials/figure-band.blade.php): a step rounder than the cards it
     * sits among, as the v4 pages draw it (28px). */
    --radius-band: 1.75rem;

    --radius-pill: 999px;

    /* Derived from True Blue rather than re-typing its channels: a literal rgb(0 25 101 …)
     * here would be a second copy of the brand value, and a correction to --nn-true-blue
     * would silently leave the shadows on the old one. */
    --shadow-sm: 0 1px 2px color-mix(in srgb, var(--nn-true-blue) 8%, transparent);
    --shadow-md: 0 2px 8px color-mix(in srgb, var(--nn-true-blue) 10%, transparent);

    /* === Focus =========================================================================
     * Accessibility requirement, not a stylistic choice. Focus is always visible and never
     * removed (CLAUDE.md §11, WCAG 2.2 AA).
     */
    --focus-ring-width: 3px;

    --focus-ring-offset: 3px;   /* SharedStates.dc.html draws the ring three pixels clear */

    /* ITEM 12 · FORM CONTROLS (the component sheet Kenneth approved on 2026-10-04): every input and
     * select is the same box. 48px high (the phone touch floor and the desktop size alike), 12px
     * round, 14px inside; a select keeps 40px on its right for its own chevron, 16px from the edge.
     * The admin shell keeps its 40px controls (advisor 2026-10-05 03:30, "admin density"). */
    --control-height: 48px;
    --control-radius: 12px;
    --control-padding: 14px;
    --control-padding-chevron: 40px;
    --control-chevron-inset: 16px;

    /* === Motion ========================================================================= */
    --duration-fast: 120ms;
    --duration-base: 200ms;
    --easing-standard: cubic-bezier(0.2, 0, 0.2, 1);

    /* === Touch =========================================================================
     * Mobile-first: comfortable targets on a phone. 44px is the accessibility floor.
     */
    --touch-target-min: 44px;
    --touch-target-phone: 48px;

    /* === Buttons ========================================================================
     * The component sheet's (design-v4-2026-10-01/components.html, item 5; advisor 2026-10-05
     * 11:30): every primary and every secondary is 48; the large one (.button--lg) is 56, kept
     * for Save on My steps (the front doors are twins at 48, audit 1006 D5); the quiet one is 44. On a phone the 56
     * comes down to 48 and the quiet one rises to the phone floor, so nothing on a phone is
     * below 48. The admin shell keeps its 40 (admin.css).
     */
    --button-height-primary: 56px;
    --button-height-secondary: 48px;
    --button-height-quiet: 44px;
    --font-size-button-main: 1.0625rem;  /* 17px: every screen board's 56px primary */
}

/*
 * Breakpoints.
 *
 * Custom properties cannot be used in media queries, so these exist as documentation and
 * must be kept in step with the values written in component media queries. [PROJECT] —
 * mobile-first, so every query is min-width.
 *
 * The recorded exception is a rule the phone has to take back rather than earn: a table that
 * stacks into blocks, the navigation that becomes a disclosure, panel padding that is a desktop
 * generosity. Those are written as `max-width: 47.99rem`, because stating them mobile-first would
 * mean restating the desktop value in a second query for every component in the family.
 *
 *   sm   30rem   480px
 *   md   40rem   640px
 *   lg   64rem   1024px
 *   xl   80rem   1280px
 */

@media (prefers-reduced-motion: reduce) {
    :root {
        --duration-fast: 0ms;
        --duration-base: 0ms;
    }
}
