/* MODULE: css/tokens.css — the TimeTime design system.
   Defines every custom property named in CONTRACT §3 for both themes.
   Depends on nothing. Must be the FIRST stylesheet loaded; every other
   stylesheet consumes these tokens and hardcodes nothing.

   Cascade strategy (read before editing):
     1. `:root, [data-theme="ceramic"]`  → ceramic atoms (the default).
     2. `@media (prefers-color-scheme: dark)` on a root that has NOT explicitly
        opted into a theme → graphite atoms. The `:not()` chain also covers
        `data-theme="system"`, which the store writes for "follow the OS".
     3. `[data-theme="graphite"]` → graphite atoms, source-ordered after the
        ceramic group so it wins at equal specificity.
     An explicit `[data-theme="ceramic"]` always wins back over the dark media
     query because that query's selector excludes it outright — no specificity
     fight, the rule simply never matches.

     Semantic tokens (--bg, --ink, …) are mapped from the neutral ramp in one
     block matched by `:root, [data-theme]`, so the mapping re-resolves on
     whichever element actually carries the theme attribute (html or body).
*/

/* ============================================================== *
 * 1. CERAMIC — warm off-white body, the default object.
 *    A real warm neutral ramp built around #F2F0EB: hue sits around
 *    40–45°, chroma stays low, so surfaces read as bone/ceramic and
 *    never as flat digital grey.
 * ============================================================== */

:root,
[data-theme="ceramic"] {
  color-scheme: light;

  /* Neutral ramp — raw material. Consume the semantic tokens, not these.
     RETUNED to the Dieter Rams palette card (DR01/DR06). The reference's own
     hex values sit at the load-bearing steps, so every surface downstream
     inherits the material rather than approximating it:
         #F5F2E9  ceramic case      → 100
         #EDEBE2  surrounding field → 150   (BODY)
         #ECE9E0  recessed face     → 200
         #D9D2C6  seams and rims    → 300
         #BFB1A8  DR01 warm greige  → 400
         #736356  DR01 muted ink    → 600 (see the note there)
         #261201  DR01 near-black   → 900 as #261F1B */
  --ramp-000: #fffefb; /* specular: the lit top edge of a raised part */
  --ramp-050: #faf8f1; /* raised control face — a cap top catches more sky */
  --ramp-100: #f5f2e9; /* CERAMIC — the product case */
  --ramp-150: #edebe2; /* BODY — the surrounding field */
  --ramp-200: #ece9e0; /* FACE — recessed dial and control surfaces */
  --ramp-250: #e3dfd4; /* sunk: the floor of a recessed well */
  --ramp-300: #d9d2c6; /* EDGE — seams, fine rims, quiet separators */
  --ramp-400: #bfb1a8;
  --ramp-500: #9d8f83;
  /* Ink ladder, measured against the body: 15:1 / 7.5:1 / 4.8:1. */
  --ramp-600: #6b5b4e; /* ink-3, tertiary. The palette card's muted ink is
                          #736356, and that measures 4.32:1 on --bg-sunk —
                          below AA, and --bg-sunk is exactly where the mode
                          switcher and the segmented groups sit. Darkened by
                          the smallest amount that clears it: 4.88:1 there,
                          5.4:1 on the body. A paint chip is not a
                          contrast-tested UI ink, and the well is the case
                          that decides it. */
  --ramp-700: #574a40; /* ink-2, secondary */
  --ramp-800: #3a3029;
  --ramp-900: #261f1b; /* INK — DR01 #261201, warmed off pure brown-black */

  /* Signal family */
  --signal: #bf1b1b; /* DR06 — the depleting disc. A deeper, less orange red
                        than the previous #e4380d: on the palette card this is
                        the functional red, and against a warm ceramic field it
                        reads as painted lacquer rather than as a screen hue. */
  --signal-dim: #7d1414; /* spent / overtime counter-arc: a real step DOWN in
                            lightness from --signal, so everything derived from
                            it darkens in both themes rather than going milky
                            in ceramic and dark in graphite. */
  --signal-warn: #ed8008; /* DR06 orange — approach ramp, hue toward amber */
  --signal-text: #bf1b1b; /* signal as TYPE. The retuned --signal is dark
                             enough to be its own text colour now: 5.19:1 on
                             the body, 4.66:1 on --bg-sunk. The old build
                             needed a separate darker ink because the signal
                             was an orange; this one does not. */

  /* DR06 #ED3F1C. A RESTRICTED HIGHLIGHT, and the restriction is measured:
     white on it is 3.94:1, which fails AA. It may light an edge, a lamp or an
     active rim. It must never be a fill that carries text. Anything with a
     label on it uses --signal-key. */
  --signal-light: #ed3f1c;

  /* DR06 #736B1E. A quiet status detail, never a third accent competing with
     the red — one saturated colour is the rule, and this is dark and desaturated
     enough to read as a painted marking rather than as a second signal. */
  --olive: #736b1e;

  --accent-cool: #2c6b7a; /* alarm + links, used sparingly */
  /* The previous signal was an orange at 4.31:1 under white, so keys needed
     their own darker surface. The retuned #bf1b1b measures 6.20:1 under
     white on its own, so the key and the disc can finally be the SAME red —
     which is what the palette card shows and what a real instrument does.
     One pigment, used everywhere it means the same thing. */
  --signal-key: var(--signal);
  --on-signal: #ffffff;
  /* THE OTHER TWO KNOCKOUT INKS. --on-signal is the glyph for the BRIGHT signal
     and nothing else. The landscape face (css/bar.css) also knocks type out of
     --signal-dim and --signal-warn, and reusing --on-signal there is a real
     contrast failure, not a rounding error: --signal-dim is a DARK red in both
     themes, so white-on-dim is fine in ceramic and #14090A-on-dim measured
     2.1:1 in graphite. Each ground gets the glyph its own luminance demands.
     --signal-warn is the same #ED8008 in both themes and is a mid-tone, so its
     glyph is dark in both. */
  --on-signal-dim: #ffffff; /* on #7D1414 */
  --on-signal-warn: #261f1b; /* on #ED8008 */
  --on-accent: #f7fbfc;

  --hairline: rgba(46, 40, 30, 0.16); /* never pure black */
  --hairline-strong: rgba(46, 40, 30, 0.28);

  /* ─────────────────────────────────────────────────────────────────────
     DEPTH — PHASE6. Three legitimate sources, nothing else.

       1. contact crescent — ONE thin dark line at a raised part's LOWER edge
       2. reveal          — a 1px seam where two parts meet
       3. overhang cast   — a real shadow from geometry that actually overhangs

     Everything that previously simulated a curved surface (stacked penumbras,
     inset top highlights, bevels, floating drops) is deleted. Look at
     docs/reference/braun-sk-rt20-panel.jpg: a single dark crescent under each
     knob is the entire depth cue on the whole object. That is the budget.
     ───────────────────────────────────────────────────────────────────── */

  /* The dial well. The chassis lip physically overhangs the face, so it casts —
     but only just, and only from the top. One line, one soft follow. */
  --shadow-well:
    inset 0 1px 0 rgba(46, 40, 30, 0.22),
    inset 0 2px 3px rgba(46, 40, 30, 0.07);

  /* Raised control: a hairline reveal all round, and the contact crescent.
     No top highlight — that is a bevel, and bevels are what read as dated. */
  --shadow-raise:
    0 0 0 1px rgba(46, 40, 30, 0.1),
    0 1px 0 rgba(46, 40, 30, 0.14);

  /* Pressed: the part sits down, so the crescent closes. The fill also darkens
     (see .btn:active) — that does the real work. */
  --shadow-press: 0 0 0 1px rgba(46, 40, 30, 0.12);

  /* Small controls use the same crescent — there is no separate "small groove"
     any more, because there is no groove. Kept as an alias so existing
     consumers do not break. */
  --shadow-well-s: inset 0 1px 0 rgba(46, 40, 30, 0.18);

  /* Fields are cut into the panel: a top reveal, nothing else. */
  --shadow-field: inset 0 1px 0 rgba(46, 40, 30, 0.16);

  /* Panels do not float. They are bounded by a reveal. */
  --shadow-panel: 0 0 0 1px rgba(46, 40, 30, 0.09);

  /* Vignelli's structural bar: not decoration, the grid made visible. */
  --rule-heavy: 2px;
  --rule-ink: var(--ramp-900);

  /* No top wash. The SK/RT20 panel is one flat colour edge to edge.
     Kept as `none` so any remaining consumer degrades to flat rather than
     throwing an invalid-value error. */
  --wash-top: none;
}

/* ============================================================== *
 * 2. GRAPHITE — night. Same object, different material.
 * ============================================================== */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="ceramic"]):not([data-theme="graphite"]) {
    color-scheme: dark;

    /* The ramp is a DEPTH ladder, not a lightness ladder: index 050 is
       always the raised face, 250 always the floor of a well. Light still
       comes from above at night, so raised stays lighter than the body. */
    /* A DARK PAINTED OBJECT, not an inverted page. The old ramp bottomed out
       at #030304 — that is the absence of a surface, and a black hole in the
       middle of a product photograph is exactly what makes dark themes read
       as software. Real dark Braun housings are a warm charcoal enamel: they
       hold their hue, they still return light at the edges, and they never
       reach black. So the floor lifts off zero and the whole ramp carries the
       same 40-45 degree warmth the ceramic does. */
    --ramp-000: #ffffff; /* specular highlight only, never a surface */
    --ramp-050: #34302a; /* raised control face — painted cap top */
    --ramp-100: #2a2721; /* panel surface */
    --ramp-150: #221f1a; /* BODY — warm enamel, not blue-black */
    --ramp-200: #1c1a15; /* surface-2, nested fills */
    --ramp-250: #16130f; /* sunk: the floor of a recessed well */
    --ramp-300: #100e0a; /* deepest sunk — still not black */
    --ramp-400: #4a443c;
    --ramp-500: #6b6459;
    --ramp-600: #9a9186; /* ink-3 — warmed, and re-measured on the LIFTED
                            raised face rather than the old near-black one */
    --ramp-700: #b8b0a4; /* ink-2 */
    --ramp-800: #d2cbc0;
    --ramp-900: #ede9e0; /* INK */

    /* At night the palette card's #bf1b1b goes muddy against a dark ground —
       a dark red on a dark body is nearly invisible, which is a real failure
       for the one element the whole app exists to show. So graphite keeps the
       brighter member of the same DR06 family, #ed3f1c, where it has the
       luminance to carry the disc. Same pigment family, different member,
       chosen by the ground it sits on. */
    --signal: #ed3f1c;
    --signal-dim: #8e3a1e;
    --signal-warn: #ed8008;
    --signal-text: #ff6236;
    --signal-light: #ff6a3a;
    --olive: #9a9040;
    --accent-cool: #63b7c8;
    --signal-key: var(--signal); /* already clears contrast on the raw fill */
    --on-signal: #14090a; /* dark glyph on the signal key — ET66, and the
                             only combination that clears contrast at night */
    --on-signal-dim: #ede9e0; /* --signal-dim is DARK at night too, so its glyph
                                 flips back to ink. See the note in the ceramic block. */
    --on-signal-warn: #14090a; /* on #ED8008 */
    --on-accent: #06171b;

    --hairline: rgba(232, 230, 225, 0.14);
    --hairline-strong: rgba(232, 230, 225, 0.26);

    --shadow-well:
    inset 0 1px 0 rgba(0, 0, 0, 0.85),
    inset 0 2px 3px rgba(0, 0, 0, 0.35);

  --shadow-raise:
    0 0 0 1px rgba(0, 0, 0, 0.55),
    0 1px 0 rgba(0, 0, 0, 0.5);

  --shadow-press: 0 0 0 1px rgba(0, 0, 0, 0.6);

  --shadow-well-s: inset 0 1px 0 rgba(0, 0, 0, 0.8);

  --shadow-field: inset 0 1px 0 rgba(0, 0, 0, 0.75);

  --shadow-panel: 0 0 0 1px rgba(232, 230, 225, 0.1);

  --wash-top: none;
  }
}

[data-theme="graphite"] {
  color-scheme: dark;

  /* See the note in the media-query block above: depth ladder, not
     lightness ladder. Keep these two graphite blocks in sync. */
  --ramp-000: #ffffff;
  --ramp-050: #34302a; /* raised control face — painted cap top */
  --ramp-100: #2a2721;
  --ramp-150: #221f1a; /* BODY — warm enamel, not blue-black */
  --ramp-200: #1c1a15;
  --ramp-250: #16130f;
  --ramp-300: #100e0a; /* deepest sunk — still not black */
  --ramp-400: #4a443c;
  --ramp-500: #6b6459;
  --ramp-600: #9a9186; /* ink-3 — measured on the lifted raised face */
  --ramp-700: #b8b0a4;
  --ramp-800: #d2cbc0;
  --ramp-900: #ede9e0;

  --signal: #ed3f1c;
  --signal-light: #ff6a3a;
  --olive: #9a9040;
  --signal-dim: #8e3a1e;
  --signal-warn: #ed8008;
  --signal-text: #ff6236;
  --accent-cool: #63b7c8;
  /* Graphite's key already clears contrast on the raw fill. */
  --signal-key: var(--signal);
  --on-signal: #14090a;
  --on-signal-dim: #ede9e0;
  --on-signal-warn: #14090a;
  --on-accent: #06171b;

  --hairline: rgba(232, 230, 225, 0.14);
  --hairline-strong: rgba(232, 230, 225, 0.26);

  --shadow-well:
    inset 0 1px 0 rgba(0, 0, 0, 0.85),
    inset 0 2px 3px rgba(0, 0, 0, 0.35);

  --shadow-raise:
    0 0 0 1px rgba(0, 0, 0, 0.55),
    0 1px 0 rgba(0, 0, 0, 0.5);

  --shadow-press: 0 0 0 1px rgba(0, 0, 0, 0.6);

  --shadow-well-s: inset 0 1px 0 rgba(0, 0, 0, 0.8);

  --shadow-field: inset 0 1px 0 rgba(0, 0, 0, 0.75);

  --shadow-panel: 0 0 0 1px rgba(232, 230, 225, 0.1);

  --wash-top: none;
}

/* ============================================================== *
 * 3. SEMANTIC SURFACE + INK MAPPING
 *    Matched on `:root` and on any themed element so the mapping
 *    re-resolves wherever data-theme is actually set.
 * ============================================================== */

:root,
[data-theme] {
  /* The reference's own vocabulary, aliased onto the ramp. These exist so the
     palette can be read in the language of the material it came from; they are
     NOT a second palette. Every one points at a step that has been contrast
     tested above. */
  --field: var(--ramp-150); /* warm surrounding field   #edebe2 */
  --ceramic: var(--ramp-100); /* product case             #f5f2e9 */
  --face: var(--ramp-200); /* recessed dial / controls #ece9e0 */
  --edge: var(--ramp-300); /* seams, fine rims         #d9d2c6 */
  --muted-ink: var(--ramp-600); /* secondary print — see the ramp-600 note */

  --bg: var(--ramp-150); /* the body of the object */
  --bg-sunk: var(--ramp-250); /* floor of a recessed well */
  --bg-raised: var(--ramp-050); /* face of a raised control */
  --surface: var(--ramp-100); /* panels, cards */
  --surface-2: var(--ramp-200); /* nested fill inside a panel */

  --ink: var(--ramp-900);
  --ink-2: var(--ramp-700);
  --ink-3: var(--ramp-600);
}

/* ============================================================== *
 * 4. THE TYPEFACE — Inter, variable, self-hosted, offline.
 *
 *    assets/fonts/inter-var.woff2 — 42KB, subset, already in the repo.
 *    Verified by reading the font's own tables:
 *      fvar : 2 axes — opsz 14..32 (default 14), wght 100..900 (default 400)
 *      GSUB : calt, case, cv01, cv08, ss01, tnum, zero
 *      head : unitsPerEm 2048
 *      OS/2 : capHeight 1490, xHeight 1118, ascender 1984, descender -494
 *
 *    Two facts from those numbers that the whole system is built on:
 *
 *    1. tnum maps all ten digits onto ONE advance of 1328/2048 = 0.64844em,
 *       and ss01 / cv01 / zero all preserve that advance. So the tabular
 *       digits stay tabular with every retained feature enabled — a running
 *       timer cannot jitter.
 *    2. ascender - capHeight = 1984 - 1490 = 494 = |descender|. The font is
 *       vertically symmetric about cap height, so a lining figure is dead
 *       centre in a `line-height: 1` box with no nudging. That is why the
 *       numerals sit on the dial's centre axis for free.
 *
 *    format('woff2-variations') is the variations-aware format hint; browsers
 *    that only know format('woff2') still accept the file, so no second src
 *    entry is needed. No local() entry: a user's own copy of "Inter" must
 *    never be substituted for the subset we ship and test against.
 * ============================================================== */

@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter-var.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ============================================================== *
 * 5. GEOMETRY, TYPE, MOTION — theme-independent.
 * ============================================================== */

:root {
  /* Radii. Braun radii are small and consistent; only the dial is round. */
  --radius-s: 4px;
  --radius-m: 8px;
  --radius-l: 16px;
  --radius-pill: 999px;

  /* Space — 4px base, doubling at the top end. */
  --space-1: 0.25rem; /*  4px */
  --space-2: 0.5rem; /*  8px */
  --space-3: 0.75rem; /* 12px */
  --space-4: 1rem; /* 16px */
  --space-5: 1.5rem; /* 24px */
  --space-6: 2rem; /* 32px */
  --space-7: 3rem; /* 48px */
  --space-8: 4rem; /* 64px */

  /* ---------------------------------------------------------------- *
   * TYPE — International Typographic Style, one typeface, three weights.
   * PHASE4 §3. Hierarchy comes from size, weight and space only.
   * ---------------------------------------------------------------- */

  /* Inter first, system stack behind it. The fallback is not decoration:
     font-display: swap paints the system face for the first frame, and it
     is what renders if the woff2 ever fails to load. */
  --font-sans:
    "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Text",
    "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial,
    "Noto Sans", sans-serif;
  /* --font-num is now the same typeface — Inter's tabular figures are the
     numerals of this instrument. The name is kept because 24 call sites use
     it, and because the fallback stack behind it is the system's DISPLAY cut
     rather than its text cut. It is ALWAYS paired with --font-num-variant /
     --font-num-feat (both applied globally on body in base.css) so every
     digit occupies an identical advance and the readout never jitters. */
  --font-num:
    "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Display",
    "Segoe UI Variable Display", "Segoe UI", Roboto, "Helvetica Neue", Arial,
    sans-serif;
  --font-mono:
    ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;

  /* THREE WEIGHTS IN THE WHOLE APP. There is no fourth.
     --wt-start is the one permitted exception, and it is permitted for the
     single START key and nothing else. No italics anywhere, ever. */
  --wt-light: 300; /* the timer numerals */
  --wt-regular: 400; /* prose, values */
  --wt-medium: 500; /* titles, labels, controls */
  --wt-start: 600; /* THE START KEY ONLY */

  /* Optical size. The whole point of a variable font: display type is cut
     tight and closed at opsz 32, an 11px legend is cut open and airy at
     opsz 14. Applied via font-variation-settings, which is why these are
     stored ready-to-use rather than as bare numbers.
     NOTE: these carry the opsz axis ONLY. wght is deliberately left to the
     font-weight property — font-variation-settings inherits, so a bundle
     that pinned "wght" would silently freeze the weight of every child that
     set font-weight but not font-variation-settings. */
  --vf-display: "opsz" 32;
  --vf-title: "opsz" 20;
  --vf-body: "opsz" 16;
  --vf-label: "opsz" 14;
  --vf-micro: "opsz" 14;

  /* OpenType features. Only the seven retained in the subset exist.
     --feat-num is verified tabular-safe: tnum, ss01 and cv01 all resolve to
     glyphs with the same 1328/2048em advance. */
  --feat-text: "calt" 1, "cv01" 1;
  --feat-caps: "calt" 1, "case" 1, "cv01" 1, "cv08" 1;
  --feat-num: "tnum" 1, "calt" 1, "cv01" 1, "ss01" 1;
  --feat-zero: "tnum" 1, "calt" 1, "cv01" 1, "ss01" 1, "zero" 1;
  --font-num-variant: tabular-nums lining-nums;
  --font-num-feat: var(--feat-num);

  /* THE 8px BASELINE GRID.
     Every line-height below resolves to a whole multiple of this, computed
     at its own size — not eyeballed. Line heights are given in rem rather
     than as ratios precisely so they land on the grid regardless of the
     size they are paired with, and still scale with a user's root font
     size. Ratios cannot do both. */
  --baseline: 0.5rem; /* 8px */

  /* --- the scale, PHASE4 §3 -------------------------------------- *
     token     size                        wt  opsz  track     lh
     display   clamp(3.5rem,13vw,8.5rem)  300   32   -0.035em  1
     title     1.333rem                   500   20   -0.011em  1.5rem
     body      1rem                       400   16    0        1.5rem
     label     0.6875rem                  500   14    0.14em   1rem
     micro     0.625rem                   500   14    0.16em   1rem
     ---------------------------------------------------------------- */

  --fs-display: clamp(3.5rem, 13vw, 8.5rem); /* 56px … 136px */
  --fs-title: 1.333rem; /* 21.33px */
  --fs-body: 1rem; /* 16px */
  --fs-label: 0.6875rem; /* 11px */
  --fs-micro: 0.625rem; /* 10px */

  /* Grid maths, worked:
       display 56px  × 1      =  56px = 7 units      (the 3.5rem floor)
       display 136px × 1      = 136px = 17 units     (the 8.5rem ceiling)
       title   21.33px        →  24px = 3 units
       body    16px           →  24px = 3 units
       label   11px           →  16px = 2 units
       micro   10px           →  16px = 2 units
     The display is fluid between its two bounds, so only the bounds can be
     grid-exact — and they are, at both target viewports named in PHASE4 §5:
     13vw resolves below the floor at 390px (→ 56px) and above the ceiling
     at 1280px (→ 136px). line-height 1 is also what centres the figures:
     see the symmetry note above the @font-face. */
  --lh-display: 1;
  --lh-title: 1.5rem; /* 24px */
  --lh-body: 1.5rem; /* 24px */
  --lh-label: 1rem; /* 16px */
  --lh-micro: 1rem; /* 16px */

  --tr-display: -0.035em;
  --tr-title: -0.011em;
  --tr-body: 0em;
  --tr-label: 0.14em; /* uppercase ALWAYS carries its tracking */
  --tr-micro: 0.16em;

  /* Relative corrections, not steps on the scale — they are always applied
     against whatever size they land in, which is why they are em and not
     rem. A monospace face at a nominal size reads larger than the grotesk
     beside it; 0.875 puts the two back on the same optical footing. */
  --fs-mono-adjust: 0.875em;
  --fs-sup-adjust: 0.75em;

  /* The measure. 62 characters sits in the comfortable middle of the
     45–75 band, which is where a column of prose stops being a wall. */
  --measure: 62ch;

  /* Optical centring for a signed readout. In overtime the string gains a
     leading "+", which pushes the digits off the dial's centre axis by half
     the sign's advance — the numerals visibly jump sideways at the moment
     the timer crosses zero, which is the exact moment nobody should be
     distracted. This is the measure that puts them back.

     The value is the advance the sign occupies: tnum makes "+" tabular too,
     so it takes a whole digit width — 0.64455em at wght 300 / opsz 32,
     measured in-browser, NOT the 0.66162em the default instance's hmtx
     suggests — plus the display's negative tracking. Written as a calc so
     the derivation lives in the value.

     APPLIED AS margin-inline-end, deliberately, not as text-indent. Both
     centre the digits, but a negative text-indent pushes the "+" outside
     the element's own box: the readout is a shrink-to-fit flex item, so the
     sign would be rendered outside its bounds and one `overflow: hidden` on
     any future ancestor would silently amputate it — an overtime readout
     that reads as remaining time. An end margin instead reserves the
     mirrored space INSIDE the layout, and the arithmetic works out
     identically whether the box is shrink-to-fit and centred by its parent
     or full-width with text-align: center. In both cases the digits land on
     the parent's centre axis and nothing overflows anything. */
  --type-display-sign: calc(0.64455em + var(--tr-display)); /* 0.60955em */

  /* The composites. `font:` shorthand order — weight size/line-height family.
     WARNING, and it is a real one: the `font` shorthand RESETS
     font-feature-settings, font-variant-numeric and font-variation-settings
     to their initial values, which would strip the app's global tabular
     figures. Never use one of these alone. Either use the matching .t-*
     class from css/type.css, or follow the shorthand immediately with:
         font-variation-settings: var(--vf-<role>);
         font-variant-numeric: var(--font-num-variant);
         font-feature-settings: var(--feat-<text|caps|num>);
         letter-spacing: var(--tr-<role>);
     --type-display keeps longhands in type.css instead: a clamp() inside the
     font shorthand is needlessly fragile for the one element that matters
     most. It is composed here for completeness and parity. */
  --type-display: var(--wt-light) var(--fs-display) / var(--lh-display)
    var(--font-num);
  --type-title: var(--wt-medium) var(--fs-title) / var(--lh-title)
    var(--font-sans);
  --type-body: var(--wt-regular) var(--fs-body) / var(--lh-body)
    var(--font-sans);
  --type-label: var(--wt-medium) var(--fs-label) / var(--lh-label)
    var(--font-sans);
  --type-micro: var(--wt-medium) var(--fs-micro) / var(--lh-micro)
    var(--font-sans);

  /* --- legacy scale, mapped onto the Swiss one ------------------- *
     These names predate PHASE4 and are consumed ~110 times across files
     this agent does not own, so they are re-pointed, never removed. The
     ratio is now the perfect fourth (1.333) that --fs-title establishes.
     --step--1 is deliberately 12px and NOT --fs-label's 11px: it carries
     real sentences (.field-hint, .set-note, .mth-note, .dr__note), and
     11px prose on a phone is a legibility regression. 11px is for engraved
     legends — .t-label — where the tracking and the caps do the work. */
  --step--1: 0.75rem; /* 12px — captions, hints, unit labels; lh 16px */
  --step-0: var(--fs-body); /* 16px — body */
  --step-1: 1.125rem; /* 18px — large control labels */
  --step-2: var(--fs-title); /* 21.33px — titles */
  --step-3: clamp(1.5rem, 1.144rem + 1.582vw, 1.777rem); /* 24 … 28.4px */
  --step-4: clamp(1.777rem, 1.293rem + 2.152vw, 2.369rem); /* 28.4 … 37.9px */
  --step-5: var(--fs-display); /* the hero readout */

  /* Legacy line heights stay unitless ratios because they are inherited by
     subtrees at mixed sizes; each is tuned to land on the 8px grid at the
     step it is normally paired with. Grid-exact work uses --lh-* above. */
  --lh-tight: var(--lh-display); /* 1 — display numerals */
  --lh-snug: 1.3333; /* grid-true at --step--1: 12 → 16px = 2 units */
  --lh-base: 1.5; /* grid-true at --step-0:  16 → 24px = 3 units */
  --track-tight: var(--tr-title); /* -0.011em */
  --track-0: var(--tr-body);
  --track-wide: 0.06em;
  --track-caps: var(--tr-label); /* 0.14em — engraved-label spacing */
  --track-caps-wide: var(--tr-micro); /* 0.16em */

  /* Motion. Snappy: a physical control answers immediately. */
  --dur-press: 70ms; /* button travelling down */
  --dur-fast: 120ms;
  --dur-med: 240ms;
  --dur-slow: 480ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* decisive, no bounce */
  --ease-spring: cubic-bezier(0.34, 1.42, 0.64, 1); /* slight overshoot */
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  /* Metrics */
  --meter-h: calc(var(--space-6) - var(--space-1)); /* 28px — every horizontal
                        meter (sequence bar, day ribbon, estimate bars) shares
                        one height so the dashboard column reads as one system */
  --hairline-w: 1px;
  --tap: 44px; /* minimum touch target */
  --ring-w: 2px;
  --ring-offset: 3px;
  --container-max: 68rem;

  /* iPhone safe areas, exposed so every agent uses the same source. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);

  /* Stacking order */
  --z-base: 0;
  --z-raised: 10;
  --z-sticky: 100;
  --z-overlay: 500;
  --z-modal: 600;
  --z-toast: 700;
}
