/* ============================================================================
 * atlas-tokens.css — the Atlas design system's token layer.
 *
 * GENERATED-BY-HAND but VALUE-FOR-VALUE from the design lab (/admin/lab), which
 * is the source of truth for every locked decision. If this file and the lab
 * disagree, the lab is right and this is a bug. Values were extracted from the
 * live lab rather than transcribed, so they cannot have drifted at authoring
 * time — see design-system/01-DECISIONS.md for why each is what it is.
 *
 * ── HOW THIS IS SAFE TO SHIP ──────────────────────────────────────────────
 * Loading this file changes NOTHING. The semantic tier is scoped to
 * `:root[data-dsys]`, and `--atlas-*` is referenced ~5,558 times across the
 * app — so defining those names unconditionally WOULD be the cutover, with no
 * way back. The attribute is the switch:
 *
 *     document.documentElement.setAttribute('data-dsys','')   // new system
 *     document.documentElement.removeAttribute('data-dsys')   // legacy
 *
 * Specificity does the work: `:root[data-dsys]` is (0,2,0) and beats the
 * legacy `:root` block at (0,1,0) regardless of load order.
 *
 * ── THE THREE TIERS (F1) ──────────────────────────────────────────────────
 *   1 PRIMITIVE  raw values, named for WHAT THEY ARE. Never referenced by a
 *                component. Safe to define unconditionally — nothing uses them.
 *   2 SEMANTIC   named for WHAT THEY ARE FOR. The only tier components touch.
 *   3 COMPONENT  per-component values that genuinely vary.
 *
 * THE RULE THAT MAKES IT A SYSTEM: a semantic token may reference a PRIMITIVE
 * and nothing else. Semantic→semantic chaining is what produced Atlas's
 * `--atlas-* → --hub-*` mess, where the layer everyone writes is the derived
 * one and the source of truth is the layer nobody uses.
 * ========================================================================== */

/* ═══ TIER 1 · PRIMITIVES ═══════════════════════════════════════════════════
   Unconditional on purpose: defining a variable nothing references is inert,
   and keeping the raw values outside the flag means the two systems share one
   palette rather than two that can drift apart. */
:root {
  /* NEUTRALS — light ramp. Not pure greys: each carries a slight cool bias
     toward the accent, which is what stops a "chosen" neutral reading as an
     unconsidered default. */
  --atlas-p-white:      #ffffff;
  --atlas-p-grey-25:    #f6f7f9;
  --atlas-p-grey-50:    #eef0f3;
  --atlas-p-grey-100:   #e2e5ea;
  --atlas-p-grey-200:   #cbd0d8;
  --atlas-p-grey-500:   #6b7482;
  --atlas-p-grey-700:   #4c5461;
  --atlas-p-grey-900:   #14181e;

  /* NEUTRALS — dark ramp. GRAPHITE, not navy (K3). The retired atlas-night
     was a blue-black, which tints every neutral surface and makes the accent
     fight the background it sits on. */
  --atlas-p-graphite-900: #121419;
  --atlas-p-graphite-800: #191c22;
  --atlas-p-graphite-700: #22262e;
  --atlas-p-graphite-600: #2b303a;
  --atlas-p-graphite-500: #2a2f38;
  --atlas-p-graphite-400: #3b414d;
  --atlas-p-graphite-300: #8a919e;
  --atlas-p-graphite-200: #a3aab6;
  --atlas-p-graphite-100: #e9ecf1;

  /* ROLE COLOURS — OKLCH, fixed L and C, HUE IS THE ONLY VARIABLE (K1).
     Light L .52 / C .145 · dark L .78 / C .135. That constraint is what makes
     four colors read as one family instead of four brand colors.
     Hues: accent 215 · urgent 27 · warn 70 · money 160 */
  --atlas-p-accent-light: #007b9c;   --atlas-p-accent-dark: #17cced;
  --atlas-p-urgent-light: #ad3f38;   --atlas-p-urgent-dark: #ff9488;
  --atlas-p-warn-light:   #9c5500;   --atlas-p-warn-dark:   #eea74c;
  --atlas-p-money-light:  #00814a;   --atlas-p-money-dark:  #5cd299;

  /* IDENTITY HUES (K1 applied, not excepted) — for color that is a NAME, not
     a status: a smart list, a channel, a calendar. Generated at the SAME fixed
     OKLCH lightness and chroma as the role colors (light L .52 / C .145 · dark
     L .78 / C .135) with hue as the only variable, which is the whole of K1 —
     so they cannot clash with the roles or with each other, and none of them
     can shout louder than the rest.
     Proof the constraint is real rather than asserted: hue 215 and hue 160
     reproduce `accent` and `money` EXACTLY, because they are the same formula.
     ⚠️ Identity is not status. These are for a thing's NAME. A status still
     uses the role colors, or the meaning drains out of green and red. */
  --atlas-p-h1-light: #007b9c;  --atlas-p-h1-dark: #17cced;
  --atlas-p-h2-light: #3f63bb;  --atlas-p-h2-dark: #8db5ff;
  --atlas-p-h3-light: #884aa1;  --atlas-p-h3-dark: #d89cf3;
  --atlas-p-h4-light: #a63e6d;  --atlas-p-h4-dark: #fa91bc;
  --atlas-p-h5-light: #ad3f38;  --atlas-p-h5-dark: #ff9488;
  --atlas-p-h6-light: #9c5500;  --atlas-p-h6-dark: #eea74c;
  --atlas-p-h7-light: #6f6e00;  --atlas-p-h7-dark: #bdbf4e;
  --atlas-p-h8-light: #00814a;  --atlas-p-h8-dark: #5cd299;

  /* ACTIVITY TYPES (A25) — same L/C, same family. Note IS the accent, by
     alias rather than by copy, so it moves if the accent moves.
     Hues: note 215 (=accent) · call 185 · text 286 · email 338 */
  --atlas-p-act-call-light:  #008274;  --atlas-p-act-call-dark:  #16d3c3;
  --atlas-p-act-text-light:  #6458b7;  --atlas-p-act-text-dark:  #b0aaff;
  --atlas-p-act-email-light: #9b4285;  --atlas-p-act-email-dark: #ef94d5;

  /* AI GRADIENT (A30) — Coral. The ONE gradient in Atlas (L6), which is what
     makes it mean "AI" by construction rather than by convention. Theme-
     independent: the mark is the same object in both. */
  --atlas-ai-a: #e0248c;
  --atlas-ai-b: #ff7a4d;

  /* ELEVATION — the two raw lift values (F12/P12). Primitives, so the semantic
     `--atlas-lift` can alias them the same way `--atlas-accent` aliases
     `--atlas-p-accent-light/-dark`. Both values previously appeared verbatim in
     four separate blocks, which is four places for one decision to drift.
     Dark is genuinely DIFFERENT, not dimmed: a light drop shadow is invisible
     on graphite, so dark carries an inset top highlight and a deeper shadow. */
  --atlas-p-lift-light: 0 1px 2px rgba(20,24,30,.05), 0 12px 26px -18px rgba(20,24,30,.28);
  --atlas-p-lift-dark:  inset 0 1px 0 rgba(255,255,255,.04), 0 12px 26px -18px rgba(0,0,0,.75);

  /* CONTROL-SCALE lift. The card lift above CANNOT RENDER on a small element:
     its working layer is `0 12px 26px -18px`, and a -18px spread shrinks the
     shadow box by 18px per side — so on a 31px segmented pill it is
     mathematically absent, and the remaining `.05` layer is invisible. Measured
     on the real control, 2026-08-13.

     So a raised CONTROL needs its own shadow: tight blur, no big negative
     spread, slightly higher alpha to survive at small size. This is TWO NAMED
     ROLES, not the shadow SCALE P12 bans — the same shape as K13's two border
     weights, which exist because one hairline could not do both structure and
     emphasis. Color is derived from the surface, never black (B1). */
  --atlas-p-lift-ctl-light: 0 1px 1px rgba(20,24,30,.04), 0 2px 5px -1px rgba(20,24,30,.13);
  --atlas-p-lift-ctl-dark:  inset 0 1px 0 rgba(255,255,255,.06), 0 2px 5px -1px rgba(0,0,0,.55);
}

/* ═══ TIER 2 · SEMANTIC ═════════════════════════════════════════════════════
   LIGHT IS THE DEFAULT. The legacy system had `:root` = atlas-night, so dark
   was the base and light was a 42-token patch on top — which is why anything
   new was dark-by-default until someone remembered to correct it, and why
   light mode always felt like a retrofit. Inverting that is the single most
   load-bearing change in this file. */

/* `.lab-scope` rides along on every semantic block below, and is deliberately
   NOT gated on [data-dsys]. The design lab is the system's own documentation
   surface: it must render in the new system whether or not the reader has the
   flag on, and it themes at SCOPE level (its Light/Dark/Density toggles set
   attributes on the lab root, not on <html>) so it can preview both without
   flipping the whole app around the reader.

   This replaced a parallel `--lab-*` vocabulary that duplicated all 66 values
   in design-lab.js. Two definition sites meant the lab could drift from the
   thing it documents — and it did: the lab had the 40px spacing step for
   weeks while this file did not. ONE vocabulary, one definition site.

   ⚠️ `.ash-shell` RIDES ALONG FOR THE SAME REASON, added 2026-08-13 after the
   shell shipped without it and rendered completely unstyled for anyone without
   the flag. The shell IS the new system's frame — it has no legacy form to fall
   back to — so gating its tokens on [data-dsys] does not give you the old shell,
   it gives you NO shell: every declaration reading an undefined custom property
   is dropped at computed-value time, so the grid falls back to auto, the rail
   loses its ground and rows collapse to their line height. Anything that exists
   ONLY in the new system belongs on this line. */
/* ⚠️ A PORTALLED ELEMENT IS PORTALLED INTO THIS SCOPE, NOT ONTO <body>.
   The tooltip was mounted on <body>, so every `var(--atlas-*)` it used resolved
   to nothing — and an unresolved token is not an error, the declaration is just
   dropped: `z-index: var(--atlas-z-tooltip)` became `auto` and the tooltip
   painted UNDER the global rail.

   The tempting fix — adding `.tip-pop` to this selector — is the WRONG one, and
   failed on the first try: the vocabulary is split across several blocks (color,
   structure, each theme), so it would have to be added to all of them and to
   every future one, and a theme block keyed on `[data-theme]` cannot reach a
   body child at all. `AtlasKit.portalHost()` returns this scope instead, so an
   element that must escape the layout still inherits the whole vocabulary AND
   the theme. See kit.js. */
/* ⚠️ `.atlas-scope` IS THE GENERAL FORM OF THE `.ash-shell` LINE ABOVE, added
   2026-08-17 for the boot loading screen. The shell earned its place by being
   new-system-only with no legacy form to fall back to; the loading screen is
   the same case, and it will not be the last. A CLASS rather than a third id
   means the next one wears it instead of appending a fourth selector to three
   blocks.
   ⚠️ Its dark variant is an ANCESTOR selector (`[data-theme] .atlas-scope`),
   not the mirrored-attribute form the shell uses, and that is deliberate: the
   shell mirrors because it mounts long after boot, while this has to be right
   on the FIRST FRAME with no JS beyond the pre-paint theme script. An ancestor
   selector needs nobody to remember to copy anything down. */
:root[data-dsys], .lab-scope, .ash-shell, .atlas-scope {
  /* SURFACES — a four-step ladder. Elevation in dark is carried mostly by the
     ladder rather than by shadow (P3), because a light drop shadow is
     invisible on graphite. */
  --atlas-bg:    var(--atlas-p-grey-25);    /* the page ground */
  --atlas-bg-1:  var(--atlas-p-white);      /* the sheet — the default surface */
  --atlas-bg-2:  var(--atlas-p-grey-50);    /* hover, selected, recessed wells */
  --atlas-bg-3:  var(--atlas-p-grey-100);   /* pressed */

  /* LINES — two weights only (K13), and the second is a DARKER line, not just
     a thicker one: a 1.75px border snaps to 1px on any 1× display (C27), so
     weight alone cannot carry the distinction. */
  --atlas-line:   var(--atlas-p-grey-100);
  --atlas-line-2: var(--atlas-p-grey-200);

  /* INK — three steps. Hierarchy is carried by WEIGHT before size (L11), so
     the ramp stays short deliberately. */
  --atlas-ink:   var(--atlas-p-grey-900);
  --atlas-ink-2: var(--atlas-p-grey-700);
  --atlas-ink-3: var(--atlas-p-grey-500);

  /* ROLES */
  --atlas-hue-1: var(--atlas-p-h1-light); --atlas-hue-2: var(--atlas-p-h2-light); --atlas-hue-3: var(--atlas-p-h3-light); --atlas-hue-4: var(--atlas-p-h4-light); --atlas-hue-5: var(--atlas-p-h5-light); --atlas-hue-6: var(--atlas-p-h6-light); --atlas-hue-7: var(--atlas-p-h7-light); --atlas-hue-8: var(--atlas-p-h8-light);
  --atlas-accent: var(--atlas-p-accent-light);
  --atlas-urgent: var(--atlas-p-urgent-light);
  --atlas-warn:   var(--atlas-p-warn-light);
  --atlas-money:  var(--atlas-p-money-light);
  --atlas-on-accent: var(--atlas-p-white);

  /* ACTIVITY (A25) — note aliases the accent on purpose. */
  --atlas-act-note:  var(--atlas-accent);
  --atlas-act-call:  var(--atlas-p-act-call-light);
  --atlas-act-text:  var(--atlas-p-act-text-light);
  --atlas-act-email: var(--atlas-p-act-email-light);

  /* ELEVATION — never nested; a lifted card inside a lifted card is the
     clearest structural tell there is. TWO roles, by SCALE not by depth:
     `lift` for an earned card, `lift-ctl` for a small raised control (see the
     primitives — the card value cannot physically render at control size). */
  --atlas-lift: var(--atlas-p-lift-light);
  --atlas-lift-ctl: var(--atlas-p-lift-ctl-light);

  /* SCRIM — one value for every overlay (P25). */
  --atlas-scrim: rgba(16,20,28,.44);
}

/* DARK — an override of the light base, which is the correct direction.
   Dark needs a DIFFERENT lift, not a dimmed one: a light drop shadow is
   invisible on graphite, so dark adds an inset top highlight and a deeper
   shadow. The scrim is heavier for the same reason — the same alpha over an
   already-dark page barely registers. */
/* `atlas-dark` as well as `dark` because T3 (renaming the theme ids) is decided
   and NOT built — the live catalog still ships `atlas-paper` / `atlas-dark`, so
   an element keyed only on the new name is dark for nobody today. */
:root[data-dsys][data-theme="dark"], .lab-scope[data-theme="dark"], .ash-shell[data-theme="dark"],
[data-theme="dark"] .atlas-scope, [data-theme="atlas-dark"] .atlas-scope {
  --atlas-bg:    var(--atlas-p-graphite-900);
  --atlas-bg-1:  var(--atlas-p-graphite-800);
  --atlas-bg-2:  var(--atlas-p-graphite-700);
  --atlas-bg-3:  var(--atlas-p-graphite-600);

  --atlas-line:   var(--atlas-p-graphite-500);
  --atlas-line-2: var(--atlas-p-graphite-400);

  --atlas-ink:   var(--atlas-p-graphite-100);
  --atlas-ink-2: var(--atlas-p-graphite-200);
  --atlas-ink-3: var(--atlas-p-graphite-300);

  --atlas-hue-1: var(--atlas-p-h1-dark); --atlas-hue-2: var(--atlas-p-h2-dark); --atlas-hue-3: var(--atlas-p-h3-dark); --atlas-hue-4: var(--atlas-p-h4-dark); --atlas-hue-5: var(--atlas-p-h5-dark); --atlas-hue-6: var(--atlas-p-h6-dark); --atlas-hue-7: var(--atlas-p-h7-dark); --atlas-hue-8: var(--atlas-p-h8-dark);
  --atlas-accent: var(--atlas-p-accent-dark);
  --atlas-urgent: var(--atlas-p-urgent-dark);
  --atlas-warn:   var(--atlas-p-warn-dark);
  --atlas-money:  var(--atlas-p-money-dark);
  --atlas-on-accent: #10131a;

  --atlas-act-note:  var(--atlas-accent);
  --atlas-act-call:  var(--atlas-p-act-call-dark);
  --atlas-act-text:  var(--atlas-p-act-text-dark);
  --atlas-act-email: var(--atlas-p-act-email-dark);

  --atlas-lift: var(--atlas-p-lift-dark);
  --atlas-lift-ctl: var(--atlas-p-lift-ctl-dark);
  --atlas-scrim: rgba(0,0,0,.62);

  color-scheme: dark;
}

/* LEGACY DARK THEMES → dark (Step 4, the code half).
   The 8 retired themes collapse into two, and the collapse is FREE for light:
   anything not matched below already falls through to the light base. But the
   SIX dark ones would land on light without this block, which would flip a
   dark-theme user to light the moment the flag is set — a jarring change that
   reads as a bug rather than a migration.

   The split is taken from `ATLAS_THEME_CATALOG`'s own `isLight` flag, not from
   inspection: light = atlas-paper · blush · frost · golden-hour, and those
   need no rule here. (Measuring `--hub-bg` per theme does NOT tell you this —
   legacy light values key on `[data-light-theme]`, a separate attribute, which
   is one of the two mechanisms this file replaces.) */
:root[data-dsys][data-theme="atlas-night"],
:root[data-dsys][data-theme="atlas-dark"],
:root[data-dsys][data-theme="slate"],
:root[data-dsys][data-theme="forest"],
:root[data-dsys][data-theme="synthwave"],
:root[data-dsys][data-theme="terminal"] {
  --atlas-bg:    var(--atlas-p-graphite-900);
  --atlas-bg-1:  var(--atlas-p-graphite-800);
  --atlas-bg-2:  var(--atlas-p-graphite-700);
  --atlas-bg-3:  var(--atlas-p-graphite-600);

  --atlas-line:   var(--atlas-p-graphite-500);
  --atlas-line-2: var(--atlas-p-graphite-400);

  --atlas-ink:   var(--atlas-p-graphite-100);
  --atlas-ink-2: var(--atlas-p-graphite-200);
  --atlas-ink-3: var(--atlas-p-graphite-300);

  --atlas-hue-1: var(--atlas-p-h1-dark); --atlas-hue-2: var(--atlas-p-h2-dark); --atlas-hue-3: var(--atlas-p-h3-dark); --atlas-hue-4: var(--atlas-p-h4-dark); --atlas-hue-5: var(--atlas-p-h5-dark); --atlas-hue-6: var(--atlas-p-h6-dark); --atlas-hue-7: var(--atlas-p-h7-dark); --atlas-hue-8: var(--atlas-p-h8-dark);
  --atlas-accent: var(--atlas-p-accent-dark);
  --atlas-urgent: var(--atlas-p-urgent-dark);
  --atlas-warn:   var(--atlas-p-warn-dark);
  --atlas-money:  var(--atlas-p-money-dark);
  --atlas-on-accent: #10131a;

  --atlas-act-note:  var(--atlas-accent);
  --atlas-act-call:  var(--atlas-p-act-call-dark);
  --atlas-act-text:  var(--atlas-p-act-text-dark);
  --atlas-act-email: var(--atlas-p-act-email-dark);

  --atlas-lift: var(--atlas-p-lift-dark);
  --atlas-lift-ctl: var(--atlas-p-lift-ctl-dark);
  --atlas-scrim: rgba(0,0,0,.62);

  color-scheme: dark;
}

/* SYSTEM (T8) — three states, not two. An explicit choice stamps data-theme;
   "System" stamps NOTHING, so only prefers-color-scheme separates the two.

   ⚠️ The guard is ABSENCE of data-theme, not `:not([data-theme="light"])`.
   The latter is correct only after the T3 rename, and T8 is not shipped yet:
   the boot script ALWAYS stamps a theme (`atlas-paper` or `atlas-dark`), and
   the four retired LIGHT ids are `atlas-paper · blush · frost · golden-hour`.
   None of those equals `light`, so `:not([data-theme="light"])` MATCHED every
   one of them — handing a light user the entire dark palette on a dark OS,
   with the flag on. Keying on absence makes this block dormant until the boot
   script can actually stamp nothing, which is correct: a feature that is not
   built should be inert, not wrong. Found 2026-08-13 while arming the lift. */
@media (prefers-color-scheme: dark) {
  :root[data-dsys]:not([data-theme]) {
    --atlas-bg:    var(--atlas-p-graphite-900);
    --atlas-bg-1:  var(--atlas-p-graphite-800);
    --atlas-bg-2:  var(--atlas-p-graphite-700);
    --atlas-bg-3:  var(--atlas-p-graphite-600);

    --atlas-line:   var(--atlas-p-graphite-500);
    --atlas-line-2: var(--atlas-p-graphite-400);

    --atlas-ink:   var(--atlas-p-graphite-100);
    --atlas-ink-2: var(--atlas-p-graphite-200);
    --atlas-ink-3: var(--atlas-p-graphite-300);

    --atlas-hue-1: var(--atlas-p-h1-dark); --atlas-hue-2: var(--atlas-p-h2-dark); --atlas-hue-3: var(--atlas-p-h3-dark); --atlas-hue-4: var(--atlas-p-h4-dark); --atlas-hue-5: var(--atlas-p-h5-dark); --atlas-hue-6: var(--atlas-p-h6-dark); --atlas-hue-7: var(--atlas-p-h7-dark); --atlas-hue-8: var(--atlas-p-h8-dark);
  --atlas-accent: var(--atlas-p-accent-dark);
    --atlas-urgent: var(--atlas-p-urgent-dark);
    --atlas-warn:   var(--atlas-p-warn-dark);
    --atlas-money:  var(--atlas-p-money-dark);
    --atlas-on-accent: #10131a;

    --atlas-act-note:  var(--atlas-accent);
    --atlas-act-call:  var(--atlas-p-act-call-dark);
    --atlas-act-text:  var(--atlas-p-act-text-dark);
    --atlas-act-email: var(--atlas-p-act-email-dark);

    --atlas-lift: var(--atlas-p-lift-dark);
    --atlas-lift-ctl: var(--atlas-p-lift-ctl-dark);
    --atlas-scrim: rgba(0,0,0,.62);

    color-scheme: dark;
  }
}

/* ═══ ELEVATION · THE ONE SEMANTIC TOKEN DEFINED OUTSIDE THE FLAG ═══════════
   Every other semantic token is scoped to `:root[data-dsys]` because defining
   it unconditionally WOULD BE the cutover. `--atlas-lift` is the deliberate
   exception, and the reason is the shadow sweep (F12/P12/C2).

   Atlas carries ~687 hardcoded neutral shadows whose single correct answer is
   this token. Those declarations render TODAY, for everyone — the sweep is NOT
   behind the flag, unlike the rest of this file. And an undefined custom
   property makes the whole declaration INVALID AT COMPUTED-VALUE TIME, so
   `box-shadow: var(--atlas-lift)` falls back to `none` rather than to anything
   sensible. Scoped to the flag, therefore, a sweep would silently DELETE every
   shadow it touched for every user who has not opted in — which is everyone.

   Measured on the running app before this block existed (flag off):
       --atlas-lift                   → (EMPTY)
       box-shadow: var(--atlas-lift)  → none

   So the token must resolve with the flag OFF. That is safe precisely because
   it is inert until something references it: `var(--atlas-lift)` had ZERO uses
   across index.html, atlas.js and design-lab.js when this was added, so this
   block changes nothing on its own — it arms the token for the sweep.

   The dark list mirrors the flagged blocks above deliberately, so the two
   cannot disagree about what "dark" means. `atlas-dark` is what the boot script
   stamps today; `dark` is the post-T3 rename; the rest are the retired ids the
   file still bridges. */
:root {
  --atlas-lift: var(--atlas-p-lift-light);
}

:root[data-theme="dark"],
:root[data-theme="atlas-dark"],
:root[data-theme="atlas-night"],
:root[data-theme="slate"],
:root[data-theme="forest"],
:root[data-theme="synthwave"],
:root[data-theme="terminal"] {
  --atlas-lift: var(--atlas-p-lift-dark);
  --atlas-lift-ctl: var(--atlas-p-lift-ctl-dark);
}

/* ═══ SAFE AREA · THE SECOND FAMILY DEFINED OUTSIDE THE FLAG (MB16) ═════════
   Same argument as elevation directly above, and the same measured failure
   mode: an undefined custom property is INVALID AT COMPUTED-VALUE TIME, so
   'padding-bottom: var(--atlas-safe-b)' does not fall back to something
   sensible — it computes to 0, and a button lands under the Android gesture
   bar. This repo has already shipped that once. Flag-scoping these would arm
   that failure for everyone who has not opted in, which is everyone.

   THE CHAIN IS THREE LINKS AND EACH ONE EARNS ITS PLACE:

     1. var(--safe-area-inset-*)  — Capacitor 8.3.0+ reads the REAL native
        insets and injects them under these names. It does that because
        ANDROID WEBVIEW BELOW 140 RETURNS WRONG VALUES (usually 0) from
        env(safe-area-inset-*). On a broken WebView this is the only correct
        value that exists.
     2. env(…, 0px)               — mobile web browsers, where nothing is
        injected and env() is right.
     3. max(…, floor)             — the backstop, and ONLY the backstop. See
        the mobile block at the foot of this file: the floors are 0 HERE so
        DESKTOP is untouched (env() is 0 in a desktop browser, and a floor
        would push every shell-mounted surface down by it).

   ⚠️ ALL FOUR, not two: Android's 3-button navigation bar moves to the SIDE in
   landscape, so left/right are real values rather than decoration.

   ⚠️ A BARE env() IS A BUG. Six live in rebuilt module CSS today and every
   safe-area use in index.html is bare — all of them Android-broken. */
:root {
  --atlas-safe-t: max(var(--safe-area-inset-top,    env(safe-area-inset-top,    0px)), 0px);
  --atlas-safe-b: max(var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)), 0px);
  --atlas-safe-l: max(var(--safe-area-inset-left,   env(safe-area-inset-left,   0px)), 0px);
  --atlas-safe-r: max(var(--safe-area-inset-right,  env(safe-area-inset-right,  0px)), 0px);
}

/* ═══ BROWSER CHROME · THE INSET NOTHING REPORTS ════════════════════════════
   ⭐ A MOBILE BROWSER'S BOTTOM BAR IS NOT A SAFE AREA, AND env() SAYS NOTHING
   ABOUT IT. Measured on an iPhone 17 / iOS 26.4 in Mobile Safari, 2026-09-08:

       100lvh 754   ← large viewport: the bar is minimised
       100svh 714   ← small viewport: the bar is shown
       100dvh 714   ← tracks whichever state is current
       env(safe-area-inset-bottom) ......... 0

   So there is a 40px band at the foot of the LARGE viewport that the browser
   owns, and no API — not env(), not visualViewport, not innerHeight — reports
   it. A bottom-anchored overlay reads `100dvh`, which is CORRECT while the bar
   is shown and forty pixels too generous the moment a scroll minimises it: the
   sheet re-lays out against 754, puts its footer in the band, and reaching for
   the commit expands the bar back over it. Painting a marker into that band
   confirmed it — the page stops painting as itself at the small-viewport edge.

   `100dvh - 100svh` IS that band, and it is self-zeroing, which is the whole
   reason to express it this way rather than measure it in JS:
     · bar shown ....... dvh == svh → 0px, today's behaviour exactly
     · bar minimised ... 754 - 714 → 40px, the correction
     · native app ...... no browser chrome, all three units equal → 0px
     · desktop ......... equal → 0px
   Nothing opts in and nothing can regress on a surface that has no chrome.

   ⚠️ DEFINED HERE, UNCONDITIONALLY, FOR MB16's REASON — an undefined custom
   property is invalid at computed-value time, so a consumer writing
   `max(var(--a-kb-h,0px), var(--atlas-chrome-b))` would have the WHOLE
   declaration thrown away rather than fall back to zero. Same argument as the
   safe-area family directly above, and the same placement.

   ⚠️ CONSUMERS USE max(), NEVER A SUM, when combining it with `--a-kb-h`.
   Both describe an obstruction at the bottom edge, and a raised keyboard hides
   the browser bar entirely — adding them lifts twice and leaves a bar-sized
   gap, which is the two-restorers trap wearing different clothes. */
:root {
  --atlas-chrome-b: calc(100dvh - 100svh);
}

/* SYSTEM (T8), unflagged half — same absence guard as the flagged block above,
   for the same reason documented there. Dormant until the boot script can stamp
   no theme at all; inert beats wrong. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --atlas-lift: var(--atlas-p-lift-dark);
    --atlas-lift-ctl: var(--atlas-p-lift-ctl-dark);
  }
}

/* ═══ NON-COLOUR SEMANTICS ══════════════════════════════════════════════════
   Theme-independent, so they sit outside the light/dark blocks — but still
   behind the flag, because they replace legacy values of the same names.
   (`.lab-scope` and `.atlas-scope` again ungated — see the note on the light
   base. A scope that gets colour and not type is half-styled, which is worse
   than either.) */
:root[data-dsys], .lab-scope, .ash-shell, .atlas-scope {
  /* TYPEFACE (Y7/Y9) — ONE family. The legacy display/body/serif split is
     retired: hierarchy comes from WEIGHT, not from a second face (L11), and
     the old chain named faces that were never actually loaded, so body and
     serif silently rendered as fallbacks for months. */
  --atlas-font: 'Onest', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --atlas-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* RADIUS — in px, never rem (Y6/P17). Layout must not scale with the text
     size control, or density and text size fight each other. */
  --atlas-r-xs: 3px;
  --atlas-r-sm: 5px;
  --atlas-r-md: 7px;
  --atlas-r-lg: 10px;
  --atlas-r-full: 999px;
  /* THE BROWSER'S OWN CORNER (Brandon, 2026-08-13). A surface that runs to the
     bottom of the viewport meets the WINDOW's rounded corner, and a square
     corner sitting inside a rounded one reads as an overhang. This is the only
     radius in Atlas that is not ours — it exists to MATCH the OS, so it is
     named for what it tracks rather than for a size in the scale. */
  --atlas-r-window: 10px;

  /* BORDERS — two weights, no more (K13). See --atlas-line-2 on why the
     emphasis weight is always paired with a darker line. */
  --atlas-bw: 1px;
  --atlas-bw-em: 1.75px;

  /* MOTION — named by ROLE (F9), and NAMESPACED (C33). A bare `--atlas-base`
     collided with the type scale's base and silently killed three animations,
     because an invalid duration drops the whole shorthand. */
  --atlas-dur-instant: 100ms;   /* hover, focus, press — direct manipulation */
  --atlas-dur-fast:    150ms;   /* toggles, menus, small reveals */
  --atlas-dur-base:    200ms;   /* modals, expand/collapse */
  --atlas-dur-slow:    300ms;   /* drawers and full-panel transitions only */
  /* THE KEYBOARD IS NOT A UI ANIMATION — it is a system one, and anything that
     tracks it has to borrow ITS timing or the two visibly disagree. UIKit
     animates the keyboard over 0.25s on a private curve; this is that curve's
     standard cubic-bezier approximation. Not for anything else. */
  --atlas-kb-dur:      250ms;
  --atlas-kb-ease:     cubic-bezier(.38,.7,.125,1);
  /* ── THE SPRING VOCABULARY (MB116, Phase-1 Batch 1-B, 2026-09-12) ────────
     CSS has no spring(); these are pre-baked `linear()` encodings (Safari
     17.2+, the sanctioned path — 03-CEILING §1) generated by
     scripts/gen-spring-linear.mjs — RE-RUN THE GENERATOR AND RE-PASTE to
     change one, never hand-edit the point lists. Each spring ships with its
     natural settling duration; using a spring at a foreign duration changes
     its physics, so consumers take BOTH tokens together.
     ⚠️ A linear() curve is STATIC — it cannot inherit a gesture's release
     velocity. Anything a finger can catch goes through K.spring (kit/kit.js),
     which integrates the same constants seeded with the measured velocity;
     these serve PROGRAMMATIC motion only.
     ⚠️ THE KEYBOARD ENVELOPE IS DELIBERATELY NOT HERE. MB116 names UIKit's
     mass 3 / stiffness 1000 / damping 500 — that system is OVERDAMPED
     (ζ≈4.56): its zero-velocity curve settles in seconds, and the real
     keyboard motion is velocity-seeded, which a static linear() cannot
     express. `--atlas-kb-ease` above stays the measured bezier (verified on
     the sim, MB124); the true envelope is a device-pass item. */
  --atlas-ease-sheet: cubic-bezier(0.32, 0.72, 0, 1);  /* the iOS sheet curve (Ionic's, via Vaul) — the CSS stand-in where a spring isn't warranted */
  /* sheet ENTRANCE — Apple's shipped open asymmetry (08-APPLE-CANON §2: 0.45/0.9) · response 0.45s ζ0.9 · natural duration 610ms */
  --atlas-spring-open: linear(0, 0.02, 0.0703, 0.1396, 0.219, 0.3025, 0.3855, 0.4652, 0.5396, 0.6076, 0.6686, 0.7227, 0.7699, 0.8108, 0.8457, 0.8753, 0.9001, 0.9208, 0.9379, 0.9519, 0.9632, 0.9723, 0.9795, 0.9853, 0.9897, 0.9931, 0.9957, 0.9976, 0.999, 1, 1.0007, 1.0011, 1.0014, 1.0015, 1.0015, 1.0015, 1.0014, 1.0013, 1.0012, 1.001, 1.0009);
  --atlas-spring-open-dur: 610ms;

  /* the drawer spring (02-STANDARD §1: dampingRatio 0.8, response 0.3s) — K.spring uses these constants for gesture releases · response 0.3s ζ0.8 · natural duration 410ms */
  --atlas-spring-sheet: linear(0, 0.0205, 0.0732, 0.1465, 0.2317, 0.322, 0.4123, 0.4993, 0.5804, 0.6542, 0.72, 0.7776, 0.8271, 0.8689, 0.9036, 0.932, 0.9548, 0.9727, 0.9865, 0.9968, 1.0042, 1.0093, 1.0125, 1.0144, 1.0151, 1.015, 1.0144, 1.0134, 1.0122, 1.0108, 1.0094, 1.0081, 1.0068, 1.0057, 1.0046, 1.0037, 1.0029, 1.0022, 1.0017, 1.0012, 1.0008);
  --atlas-spring-sheet-dur: 410ms;

  /* the house standard spring (rule 5: 1.0/0.4s) — state changes, future consumers · response 0.4s ζ1 · natural duration 590ms */
  --atlas-spring-std: linear(0, 0.023, 0.0793, 0.1541, 0.2373, 0.3223, 0.4048, 0.4821, 0.5529, 0.6166, 0.673, 0.7225, 0.7655, 0.8026, 0.8344, 0.8615, 0.8844, 0.9038, 0.9201, 0.9338, 0.9453, 0.9548, 0.9627, 0.9693, 0.9748, 0.9793, 0.983, 0.9861, 0.9886, 0.9907, 0.9924, 0.9938, 0.9949, 0.9959, 0.9966, 0.9973, 0.9978, 0.9982, 0.9985, 0.9988, 0.999);
  --atlas-spring-std-dur: 590ms;

  /* the NAV push/pop spring (02-STANDARD §6: the careful native re-derivation, 0.38s damping 0.95) — K.drill (MB47/Phase-3 3-B) · response 0.38s ζ0.95 · natural duration 470ms */
  --atlas-spring-nav: linear(0, 0.0167, 0.0592, 0.1182, 0.1867, 0.2596, 0.3334, 0.4053, 0.4738, 0.5376, 0.5963, 0.6496, 0.6975, 0.7401, 0.7777, 0.8108, 0.8395, 0.8645, 0.886, 0.9044, 0.9202, 0.9336, 0.945, 0.9546, 0.9626, 0.9694, 0.975, 0.9797, 0.9835, 0.9867, 0.9894, 0.9915, 0.9933, 0.9947, 0.9958, 0.9968, 0.9975, 0.9981, 0.9986, 0.9989, 0.9992);
  --atlas-spring-nav-dur: 470ms;

  /* ⭐ AMBIENT — the one duration for a change nobody asked for (2026-08-28).
     Every duration above answers a DISCRETE act: you pressed, you opened, you
     toggled, and the motion is the app answering you inside the 100ms budget.
     The nav pill minimizing on scroll is not that — nothing was tapped, the app
     is reacting to your attention rather than to a decision, and at
     `--atlas-dur-base` it read as a snap even though 200ms is correct for
     anything you actually pressed (Brandon: *"I just want that animation much
     slower so it doesnt feel as snappy"*).
     ⚠️ IT IS NOT "SLOW" — `--atlas-dur-slow` is a full-panel transition, which
     is still a response to an act. This is a different KIND, which is why it is
     a new role on the ladder rather than a bigger number on an existing one. */
  --atlas-dur-ambient: 420ms;
  --atlas-ease-out: cubic-bezier(.16,1,.3,1);   /* entering */
  /* Symmetric ease, for a change with no moment of arrival. `--atlas-ease-out`
     starts FAST — which is what makes a response feel immediate and what makes
     an ambient change feel snappy. */
  --atlas-ease-ambient: cubic-bezier(.4,0,.2,1);
  --atlas-ease-in:  cubic-bezier(.4,0,1,1);     /* exiting */
  --atlas-press: 1px;                            /* how far a control depresses */
  /* ⭐ THE TOUCH PRESS IS A SCALE, NOT A FILL (P41b, 2026-08-28). A pointer
     press can be a colour step because the cursor is a 1px arrow and you can
     SEE the element it is on. A thumb covers the thing it presses, so the one
     channel a fill uses is the one channel that is hidden — and what is left
     visible is a grey border of a box that now reads as SELECTED rather than
     pressed. Motion is the channel a finger cannot cover.
     ⚠️ .985 is not a new number: it is what the nav pill already presses at,
     where it was MEASURED against trap 6 (each edge moves ~2.5px and a press
     held at the element's own edge keeps `:active`). Tokenised here because it
     now has a second consumer. */
  --atlas-press-scale: .985;
  /* The SMALL-CONTROL press scale (D-2 / P41c as amended 2026-09-12). .985 was
     measured on the ~390px card, where 3%/edge would be ~6px of travel; a
     44px control at .985 moves a third of a pixel and reads as nothing under a
     thumb. Two tiers, one axis: wide things press subtle, small things press
     visible — the same 1-2px of edge travel either way. */
  --atlas-press-scale-ctl: .97;
  /* ⭐ THE PRESS MOTION IS A TOKEN, AND IT IS DEFINED ONLY ON TOUCH (D-2,
     2026-09-12). Every `:active` rule reads
         transform: var(--atlas-press-t, <its own pointer motion>)
     — on a fine pointer the token is UNDEFINED, so the fallback IS the
     family's own depress/nudge, byte-identical to what shipped (desktop
     untouched, MB115). Under `pointer: coarse` the block at the end of this
     file defines the token and the whole system's press becomes a small
     scale in one place. Two tiers:
       --atlas-press-t      small controls → scale(.97)
       --atlas-press-t-row  wide rows/cards → scale(.985)
     ⚠️ The fallback is mandatory at every consumer — an unresolved var() in
     `transform` is IACVT → no press AT ALL (trap 60), and drawers can mount
     outside every token scope. `--atlas-press-in` is the same shape for the
     press-in transition: undefined at rest (fallback `none` = instant down,
     P46), defined by SCROLLER contexts below so a scrolling finger never
     flashes a press (the ~150ms delaysContentTouches rule, 02-STANDARD §7). */

  /* LAYERS (P14/P16) — a ladder, never a literal. The live app reached
     2,147,483,350 by picking numbers to beat other numbers. */
  /* SCROLLBARS — the sitewide rule lives once, in index.html, and reads
     `--scrollbar-thumb`. That token is already re-pointed for us further down,
     in the LEGACY-ALIAS block where every other legacy name is bridged; a
     second copy here was pure redundancy and check 19 found it within a minute
     of being written. Six COMPONENTS also used to declare the whole treatment
     themselves — that is the part that mattered, and they are gone. */

  /* ── COLUMN WIDTHS ───────────────────────────────────────────────────────
     THREE names, because there are three jobs, and naming them separately is
     what stops someone splitting the difference at 260 and drifting.

     `subrail` — a navigating column INSIDE a surface: the CRM's scope picker,
                 Drive's tree, Huddle's channel list. Scanned, one line per row.
                 A CROSS-SURFACE PROMISE: if CRM's is 224 and Drive's is 260 the
                 content pane jumps every time you switch tools, which reads as
                 the app shifting under you — the thing the shell's fixed
                 geometry exists to prevent (N5). One number.

     `panel`   — a column that is READ, not scanned: the lead profile's info
                 column. Wider on purpose. 224 is sized for a label that can
                 truncate gracefully; an email, an address or an agent's full
                 name truncating on every record is a panel failing at its one
                 job. Started at 300 — the live app's own number — and went to
                 340 once it was on screen beside a content pane with room to
                 spare (Brandon, 2026-08-13). The test is not "how much space is
                 left", it is WHETHER A LABEL AND ITS VALUE FIT ON ONE LINE:
                 "Assigned agent   Nadeem Hussain" is the widest ordinary pair
                 in the panel, and at 300 it wrapped.

     `rail`    — see `--atlas-rail-w` further down: THE GLOBAL RAIL, 56px
                 collapsed. It already existed.

     ⚠️ AND THAT IS WHY THIS ONE IS `subrail`. It shipped as `--atlas-rail-w`
     for about an hour — a name that was ALREADY TAKEN by the global rail, two
     blocks below. Nothing errored: the later declaration simply won, the CRM's
     scope rail inherited 56px, its labels truncated to single letters, and the
     table overflowed the space left over. A COLLIDING TOKEN NAME IS NOT A
     REDEFINITION, IT IS A SILENT REASSIGNMENT — the same failure as the
     `fmt.date` collision, in CSS. `npm run lint:design` check 19 now refuses a
     name declared twice outside a theme block.

     ⚠️ THE SUBRAIL IS NOT A COMPONENT — the WIDTH is the shared thing, plus
     the chrome (free-edge hairline, section eyebrow, row rhythm). A picker and
     an info panel do not share a row asset: one is scanned, the other is read. */
  --atlas-subrail-w: 224px;
  /* ⭐ THE EMPTY DROP TARGET — the one place the system asks for a REGION rather
     than a control, so it is sized in rows rather than in pixels-by-eye: 160px
     is a hair over three `--atlas-row`s, which is what a folder with a few
     things in it would have occupied. Big enough that "put files here" is
     obviously the offer, small enough that it does not become the tab.
     ⚠️ ONLY the empty state gets a bordered zone. Once files exist the LIST is
     the target and the zone disappears — a permanent dashed rectangle above a
     populated list is chrome advertising a gesture nobody is making. */
  --atlas-drop-h: 160px;
  --atlas-panel-w:   340px;
  /* THE SHOULDER — one hairline across the whole lead surface, and the two
     blocks above it end on the same y. The panel starts at the display top and
     the content pane starts below the bar, so the panel block is TWICE the bar
     and the content toolbar is exactly ONE. Derived, not chosen: both fall out
     of --atlas-bar-h, so the line cannot drift when the bar changes. */
  --atlas-shoulder-h: calc(var(--atlas-bar-h) * 2);

  --atlas-z-sticky:  100;
  --atlas-z-nav:     200;
  --atlas-z-drawer:  300;
  --atlas-z-modal:   400;
  /* ⭐ A DRAWER RAISED FROM A MODAL (2026-09-16). The drawer rung sits BELOW
     modal, which is right for the ordinary case — a drawer is the surface, a
     modal interrupts it. But a modal can itself raise a drawer (Market's
     listing overlay opens the send sheet and the buyers list), and that drawer
     has to outrank the thing that opened it or it paints underneath, clipped.
     Market's send sheet borrowed `popover` for this and its own comment said
     what to do when a SECOND surface needed the same thing: give the ladder the
     rung rather than let each family borrow. The buyers drawer was the second,
     one week later. Sits between modal and popover so a popover raised from
     THAT drawer still lands on top. */
  --atlas-z-modal-drawer: 450;
  --atlas-z-popover: 500;
  --atlas-z-toast:   600;
  --atlas-z-tooltip: 650;
  --atlas-z-confirm: 700;
  /* P14's ladder ends at `splash 800` and the token was never built, because
     nothing had needed the top rung until the boot screen was rebuilt. A
     takeover that covers the app — including the rail, the bar and any confirm
     that happens to be open — is exactly what that rung is for. Added
     2026-08-17 rather than reaching for a literal (P16). */
  --atlas-z-splash:  800;

  /* POPOVER GEOMETRY (A37) — every floating panel is placed by ONE primitive,
     so the numbers it places with are tokens, not arguments at a call site.
     `gap` is the distance from the control that opened it — enough to read as
     a separate layer, not so much that it stops looking attached.
     `gutter` is the minimum clearance to the edge of the band it must stay
     inside; it is what makes "never touches the edge" a value rather than a
     hope. */
  --atlas-pop-gap:    4px;
  --atlas-pop-gutter: 8px;

  /* SPACE — THE NAME IS THE PIXEL VALUE. `--atlas-s14` is 14px. No mental
     arithmetic, no "why is there no s7".

     ⚠️ RENAMED 2026-08-13, and the old names meant DIFFERENT VALUES: the scale
     was `sN = N × 4px`, so `--atlas-s2` was 8px and is now 2px. That rename was
     only safe because `--atlas-s*` had ZERO references anywhere in the app when
     it happened — it would have been a silent, invisible 4× error later, which
     is the C33 failure exactly. `--lab-s*` was renamed in the same commit so
     the two systems cannot disagree about what `s2` means.

     TWO TIERS, and the distinction is what the value is FOR:
       · LAYOUT  4·8·12·16·20·24·32·40 — P10's grid, the relationships BETWEEN
         elements. Reach here first.
       · COMPONENT-INTERNAL  2·6·10·14·18·22·26 — the half-steps. Padding INSIDE
         a control, where a 4px grid is too coarse: the gap between an icon and
         its label, the inset of a 22px tag. Measured need, not theory — 37% of
         the lab's spacing was already on these, the same share as the grid
         itself (Brandon's call, 2026-08-13: "component padding needs them").

     Spacing is LAYOUT, so it stays px and never scales with the text-size
     control (P10/Y6) — that orthogonality is what makes Compact × Extra-large
     a usable combination rather than a fight. */
  --atlas-s2:  2px;    /* component-internal */
  --atlas-s4:  4px;    /* grid */
  --atlas-s6:  6px;    /* component-internal */
  --atlas-s8:  8px;    /* grid */
  --atlas-s10: 10px;   /* component-internal */
  --atlas-s12: 12px;   /* grid */
  --atlas-s14: 14px;   /* component-internal */
  --atlas-s16: 16px;   /* grid */
  --atlas-s18: 18px;   /* component-internal */
  --atlas-s20: 20px;   /* grid */
  --atlas-s22: 22px;   /* component-internal */
  --atlas-s24: 24px;   /* grid */
  --atlas-s26: 26px;   /* component-internal */
  --atlas-s32: 32px;   /* grid */
  --atlas-s40: 40px;   /* grid — section gap */

  /* DENSITY — compact is the default (K8). 48px rows, not 40: 40 is the
     bottom of the convention band, and this team already said the UI felt
     tight. */
  --atlas-row: 48px;
  --atlas-pad: 24px;
  --atlas-gap: 40px;
  --atlas-lh: 1.5;

  /* TYPE — every size derives from ONE base so the text-size control moves
     all of them together. Steps stay modest (~1.07–1.13×) because weight
     carries hierarchy, not size (L11). Layout px above is deliberately NOT
     scaled — that is what keeps density and text size orthogonal (Y6). */
  /* ⚠️ `--atlas-text-scale` IS DELIBERATELY NOT DECLARED HERE, AND THAT IS THE
     WHOLE FIX (2026-08-17). This block's selector is
     `:root[data-dsys], .lab-scope, .ash-shell` — so declaring `1` in it meant
     **`.ash-shell` re-declared the token and shadowed the value inherited from
     `<html>`**, where `atlasSetTextScale()` writes it as an inline style. The
     root font-size moved 16 → 20px and **not one pixel of any rebuilt surface
     followed**, because everything inside the shell recomputed from `15px × 1`.
     Text size therefore never worked in the rebuild at all — it works in the
     legacy app, which reads the root font-size directly through `rem`.
     Same shape as C50: a value set at the root, silently overridden in the one
     scope that matters. The FALLBACK carries the default instead, so an
     un-set app is still 1 and an inherited value is honoured. */
  /* ⚠️ THE LITERAL MOVED OUT INTO `--atlas-type-px` (MB48) so this stays the
     ONE declaration of `--atlas-type-base`. A phone needs a bigger base, and
     the obvious way to write that — a second `--atlas-type-base` in a
     phone-scoped block — is the very collision that broke text-size (trap 17),
     and the design linter flags it by name. A phone sets the SIZE instead;
     nothing else declares that token, so it inherits from `<html>` and cannot
     be shadowed by any scope below it. */
  --atlas-type-base: calc(var(--atlas-type-px, 15px) * var(--atlas-text-scale, 1));
  --atlas-fs-2xs:  calc(var(--atlas-type-base) * 0.767);   /* ~11.5 — micro caps */
  --atlas-fs-xs:   calc(var(--atlas-type-base) * 0.833);   /* ~12.5 — meta */
  --atlas-fs-sm:   calc(var(--atlas-type-base) * 0.867);   /* ~13   — the floor (F8) */
  --atlas-fs-md:   calc(var(--atlas-type-base) * 0.933);   /* ~14   — secondary body */
  --atlas-fs-base: var(--atlas-type-base);                 /* ~15   — body */
  --atlas-fs-lg:   calc(var(--atlas-type-base) * 1.067);   /* ~16   — headers */
  --atlas-fs-xl:   calc(var(--atlas-type-base) * 1.333);   /* ~20   — rare */
  --atlas-fs-2xl:  calc(var(--atlas-type-base) * 1.867);   /* ~28   — stat values */

  /* WEIGHT — THREE, and the jump between them must be REAL (L11/Y1/B5).
     This is the system's PRIMARY hierarchy device, which made its absence from
     the token file the sharpest gap in it: the lab used raw 600 and 700 in 75
     places with nothing to point at. 500-vs-600 is invisible at UI sizes and is
     the classic failure, so there is deliberately no `medium` between 400 and
     600, and nothing above 700 — B5: more than three reads as indecision.

     ⚠️ Flag-scoped like everything else here, which is correct because the
     surfaces that consume them are built behind the flag too (05-SURFACES §2).
     But note the failure mode is the one --atlas-lift had: with the flag off
     `font-weight: var(--atlas-fw-bold)` is invalid at computed-value time and
     falls back to NORMAL — silently, and on the property carrying the system's
     hierarchy. Do not reach for these while tokenising a LEGACY surface that
     is not behind the flag. */
  --atlas-fw-normal: 400;   /* body, and everything that is not doing a job */
  --atlas-fw-semi:   600;   /* labels, table headers, the quieter emphasis */
  --atlas-fw-bold:   700;   /* the real jump — names, values, headers */

  /* CONTROL HEIGHTS — a family, because A21 requires a FIXED height on every
     small labelled element (with `line-height: 1`): centring a line box is not
     centring the glyphs, and at the inherited 1.5 the reserved descender space
     sits empty below a label with no descenders, so the text rides high.
     Enumerated as tokens so the six of them cannot drift apart the way six
     hardcoded numbers in six files would. Sizes are A21 + A24. */
  --atlas-h-count:   20px;  /* count chip (A22) — 18px until 2026-08-13, when the
                               redraw needed 2px more so a 1px ring was not
                               sitting on the glyph. The breadcrumb row follows
                               it deliberately: both are the smallest labelled
                               row in the system and should not drift apart. */
  --atlas-h-mark:    19px;  /* checkbox + radio — the mark controls (A24) */
  --atlas-h-mark-sm: 16px;  /* the same mark IN A DENSE ROW. A24's 19px is
                               sized to stand alone beside a label; in a table
                               it repeats once per row and competes with the
                               data, so the selection column takes the smaller
                               one. Still above the 14px where a tick stops
                               reading, and the LABEL is not the hit target
                               here — the whole row is. */
  --atlas-h-tag:     22px;  /* tag, picker meta-pill */
  --atlas-h-add:     24px;  /* INLINE ADD (A39) — the small `+` that lives in a
                               section header. 24px is not an aesthetic choice:
                               it is the floor WCAG 2.5.8 allows for a pointer
                               target, and this control is the smallest thing in
                               the system anyone is asked to HIT. It was 20px
                               while it was a CRM-local rule, which is below it. */
  --atlas-h-status:  24px;  /* status pill (K9) */
  /* ⭐ THE UPLOAD BAR — the system's first progress primitive, added 2026-08-18
     with the deal's Documents tab. It is 2px because it does not GET a row of
     its own: an uploading file is a real row from the instant it is dropped,
     and the bar rides its bottom edge, replacing the 1px hairline while the
     transfer runs. Twice the hairline so it reads as deliberate rather than as
     a rendering artifact, and no taller, because a fat bar turns a two-second
     upload into the loudest thing on the page.
     ⚠️ Determinate ONLY. An indeterminate bar is a spinner wearing a
     progress bar's clothes — if the percentage is unknown, use the spinner. */
  --atlas-h-progress: 2px;

  /* ⭐ THE CATCH — the one easing in the system that OVERSHOOTS, and it is
     reserved for something ARRIVING rather than something moving. A press
     translates and settles (P39); a dropped file lands, dips under its own
     weight and comes back. Shared by the checklist row and the Documents list
     as of 2026-08-18, which is exactly why it is a token: two surfaces
     hand-rolling the same bezier is two surfaces that will diverge on the
     first tweak, and "a drop feels like this" is one decision. */
  --atlas-ease-land: cubic-bezier(.22, 1.2, .36, 1);
  --atlas-h-mode:    26px;  /* composer mode-pill */
  --atlas-h-pill:    28px;  /* pill (A3) */
  /* AN ACTION THAT SITS INSIDE A FIELD (A42) — Huddle's send, and anything else
     that rides the bottom edge of a one-line input. It is 32px because that is
     the field's own CONTENT ROW at rest (one line of body text plus its
     padding), so the control fills the row exactly: bottom-anchored, and
     therefore optically centred while the field is one line, WITHOUT making the
     field any taller. 28px left 4px of air above it and read low; 34px would
     have grown the composer by 2px, which is the thing this rung exists to
     avoid. (Brandon, 2026-08-15.) */
  --atlas-h-infield: 32px;
  --atlas-h-control: 34px;  /* segmented, select, stepper — container controls (A24) */
  /* Navigation shares ONE row height across both levels — a rail item and a
     tool-header tab are the same size, so moving between "which tool" and
     "which view" is not also a change of scale. Deliberately NOT a control
     height: a nav item is a surface, not a control (P41). */
  --atlas-h-nav:     40px;  /* rail item + tool-header tab (N1/N3) */
  --atlas-h-btn:     44px;  /* ACTION BUTTON (A21 applied to the family that was
                               missing it). It had no fixed height at all —
                               padding + line-height + border — so the AI
                               button, whose border is `bw-em` rather than `bw`,
                               came out 1.5px TALLER than the commit button
                               beside it. Invisible at DPR 1, where the engine
                               rounds 1.75px to 1px, and plainly visible on a
                               retina display: a family whose height depends on
                               its border weight is a family that disagrees with
                               itself the moment one member is emphasised.
                               44 is the height it already had (43) rounded to
                               an even number, so pinning it changes nothing you
                               can see — the fix is structural. */
  --atlas-h-btn-sm:  36px;  /* THE SAME BUTTON IN A DENSE CONTROL ROW — the
                               exact argument `--atlas-h-mark-sm` already makes
                               for the mark controls. 44px is right when a
                               button is the thing you came to press (a drawer
                               footer, a modal); it is wrong in an INLINE row
                               that also holds fields and a segmented control,
                               where the button sets the height of the whole
                               band and 44 makes it bulky (Brandon, on the
                               composer footer, 2026-08-14). Paired with
                               `--atlas-h-control` (34px) for the fields beside
                               it: two ladders, kept apart on purpose, because a
                               button and a field are different families and
                               forcing them to one number is how a system ends
                               up with 34px primary buttons. */

  /* THE FIELD FAMILY — the missing third member of the ladder
     (`surfaces/transactions.md` §9). A text field is the only control in the
     system that had NO height token: 36px was a literal in four places in
     kit.css (`.a-tb`, `.a-search input`, `.pick-field`, `.drw-in`), all
     sharing one shell. Naming today's value moves nothing; the `-lg` rung is
     what a PAGE-SCALE form needs, and it is 36 + one full layout step.

     TYPE DOES NOT CHANGE WITH THE BOX. Value type stays `fs-base` at both
     rungs — the ladder is about the CONTAINER. A second type scale for one
     surface is exactly what Y18 exists to prevent, so "big" here means a
     bigger target and more air, never bigger words. */
  --atlas-h-field:    36px;  /* drawer, rail, toolbar — today's value, named */
  --atlas-h-field-lg: 44px;  /* a page-scale form (36 + `--atlas-s8`) */
  /* AND THE CONTAINER CONTROLS NEED THE SAME RUNG, for the same reason: a
     34px segmented control beside a 44px field reads as the one control that
     did not get the memo. §9 flags the 34-vs-36 gap as "either a real
     disagreement or a deliberate split nobody wrote down"; this does not
     settle that — it adds the LARGE rung a page-scale form needs and leaves
     the dense pair exactly as it is. */
  --atlas-h-control-lg: 44px;

  /* SHELL GEOMETRY (N5) — the shell's grid TRACKS. These live here rather than
     in shell.css because a token defined in two files is the drift ONE
     VOCABULARY exists to stop, and because the whole point of N5 is that these
     three numbers are the only place any offset is expressed: a page inside the
     shell starts at 0,0 of its own cell and never re-derives a position. If you
     find `calc(48px + …)` in a shell-mounted page, that is the bug. */
  /* THE SLAB (N8) — the accent deepened toward a dark anchor, in BOTH themes.
     Named because it appears in three places that must never drift: the
     collapsed rail's ground, the 3px edge left behind when it expands, and the
     ring that separates the unread dot from the glyph beneath it. The raw 215°
     is a mid-tone in light and a bright cyan in dark; a full-height slab of
     either is an argument nobody made. */
  --atlas-rail-slab: color-mix(in oklch, var(--atlas-accent) 70%, #06121a);
  --atlas-rail-w:      56px;   /* collapsed global rail — the default (N1) */
  --atlas-rail-w-open: 208px;  /* expanded */
  --atlas-bar-h:       48px;   /* the one top bar: tool nav left, global cluster right (N3/N4) */
  /* ── THE MOBILE FAMILY'S DESKTOP BASE (MB1) ───────────────────────────────
     These five name things that only EXIST on a phone — a tap floor, a nav
     pill, a sheet. They are nonetheless declared HERE, unconditionally, and
     re-pointed in the mobile block at the foot of this file.

     ⚠️ THE REASON IS THE ELEVATION LESSON, NOT TIDINESS. MB6 makes the sheet
     ONE primitive with a detent argument — the same component at both
     viewports — so it will read these names on a desktop too. An undefined
     custom property is INVALID AT COMPUTED-VALUE TIME, so
     'border-radius: var(--atlas-sheet-r)' would not fall back to the drawer's
     radius; it would compute to 0 and ship a desktop drawer with square
     corners, silently. Defining a token only inside the conditional block is
     how a shared component gets a hole instead of a value.

     Each base value is the DESKTOP-CORRECT answer, not a placeholder:
       tap-min    0  — a mouse needs no hit floor, and min-height:0 is a no-op
       pill-*     0  — there is no pill, so a scroller has nothing to clear
       sheet-r       the drawer radius, so the shared primitive is right here
       sheet-*   100% — a desktop drawer is full height; the detents are the
                        phone's answer to a viewport it has to share. */
  --atlas-tap-min:           0px;
  --atlas-pill-h:            0px;
  /* ⚠️ THIS ONE IS ITS REAL VALUE, NOT 0, AND THE DIFFERENCE IS DELIBERATE.
     `pill-h` and `pill-clear` are CONSUMED on a desktop — a scroller pads by
     the clearance — so 0 is the correct desktop answer for them. `pill-h-min`
     is read by the PILL and nothing else, and the pill does not exist here at
     all, so its base is free to be the value it actually means. That is what
     lets the design lab render the minimized state truthfully on a desktop
     page: a 0 here made the lab specimen collapse to no height, which reads as
     a broken demo rather than as a token that was never meant to resolve. */
  /* ⚠️ MINIMIZE IS A SCALE, NOT A HEIGHT (Brandon, 2026-08-22, and the first
     version was wrong). Dropping the cell height only took the TOP edge down,
     because the pill is bottom-anchored — so it sagged rather than shrank, and
     the width never moved at all. The right model is that MINIMIZE IS THE PRESS,
     FURTHER AND HELD: the press already contracts the whole pill inward about
     its own centre, so scrolling uses the same gesture, deeper and sustained.
     One way of receding instead of two unrelated ones.
     Centre-origin on purpose: the bottom edge rises as it contracts, which is
     what "shrinks inward" means. It is not pinned to its resting spot. */
  /* ⚠️ ITS REAL VALUE, NOT 0 — same reasoning as --atlas-pill-clear above: only
     the pill reads it, and the pill does not exist on a desktop, so the base is
     free to be what it means and the lab can render the specimen truthfully.
     ⚠️ THIS WAS DELETED BY ACCIDENT ON 2026-08-22 and the failure is worth
     keeping: a scripted edit sliced from a comment above it to the line below,
     swallowing this declaration on the way past. min-width:var(--atlas-pill-cell)
     then became INVALID rather than wrong, so the cells collapsed to their
     content — a 134px pill instead of 354 — with nothing thrown. The assert
     checked that the TARGET was gone and never that the NEIGHBOUR survived. */
  /* LETTER-SPACING — two rungs, and they exist because two surfaces already
     WROTE them (kit.css and transactions/deal.css) against a family that was
     never declared. Those declarations were invalid at computed-value time, so
     the property fell back to `normal` and the tracking simply never happened —
     silently, in shipped surfaces, until the undefined-token check was widened
     to read module CSS on 2026-08-22.
     `wide` is the uppercase micro-label tracking the system already uses at
     .09em (Y-law's fourth hierarchy channel); `tight` is the small negative a
     large display line wants so it does not read as gappy. */
  --atlas-ls-wide:   .09em;
  --atlas-ls-tight:  -.01em;
  --atlas-pill-cell:         68px;
  --atlas-pill-min-scale:    0.86;
  --atlas-pill-inset:        0px;
  --atlas-pill-clear:        0px;
  --atlas-sheet-r:           var(--atlas-r-lg);
  --atlas-sheet-partial-max: 100dvh;
  --atlas-sheet-full:        100dvh;


  /* ICON SIZES — five steps, named by ROLE (F4), because a glyph's size is
     decided by WHAT IT SITS NEXT TO, not by taste. Measured before writing:
     the lab had 76 sized icon calls across ELEVEN distinct values
     (11·12·13·14·15·16·18·20·22·24·30) and no token — 16 and 12 were already
     the two real clusters, so this ladder names what the system had converged
     on and deletes the seven strays. Sizes are LAYOUT, so px, and they do NOT
     scale with the text-size control (Y6/P17): a glyph beside a label that
     grew is a fixed mark next to bigger type, which is correct — scaling it
     would land on fractional pixels and render soft.

     Steps are ~1.2x apart deliberately. Two icon sizes 2px apart are the
     glyph form of B5's adjacent-weights failure: nobody can perceive the
     difference, so it does no work and only creates a decision to get wrong. */
  --atlas-ic-xs: 12px;  /* INSIDE another element — a count, a check in a pill, a sort arrow */
  --atlas-ic-sm: 14px;  /* beside META text (fs-xs/2xs) — breadcrumb separator, inline caret */
  --atlas-ic-md: 16px;  /* THE DEFAULT — beside body text in a control: toolbar, buttons, fields */
  --atlas-ic-lg: 20px;  /* STANDALONE, carrying its own meaning — timeline entry (A28), nav item, empty state */
  --atlas-ic-xl: 24px;  /* feature scale — rare, and never decoration */
}

/* ══ THE PHONE READS BIGGER (MB48) ═════════════════════════════════════════
   Brandon: *"globally the actual substance and content should be bigger on
   mobile so it doesn't look like a cheap mobile version of a desktop app."*

   ⭐ ONE TOKEN, EVERY SURFACE. Every size in the ramp is
   `calc(--atlas-type-base × n)` and the base is now `--atlas-type-px × scale`,
   so ONE number moves the CRM cards, the Huddle rail and its messages, the
   Directory, and the lead profile's Activity, Tasks, Showings and Details —
   each at its own step. Patching them one at a time is how four surfaces end up
   disagreeing about what "body" means.

   ⚠️ 15 → 17px, WHICH IS THE PLATFORM'S NUMBER, NOT A NUDGE. iOS body text is
   17pt and Android's is 16sp. Measured before this: the CRM cards AND the
   Directory rows both rendered at 14px/400, lh 21 — already identical, so this
   was never "one surface is smaller than another", it was all of it being
   desktop-sized. A timid +1 would have carried the same layout risk for none of
   the effect.

   ⚠️ IT SETS THE SIZE, NOT THE BASE, and that is the whole reason it works.
   `--atlas-type-base` is declared on `:root[data-dsys], .lab-scope, .ash-shell,
   .atlas-scope`; `a-phone` sits on `<html>`, ABOVE all of them — so declaring
   the base here would be inherited in and then shadowed by the shell's own
   declaration, which is precisely how text-size shipped broken for months
   (see the note beside `--atlas-text-scale`). Nothing else declares
   `--atlas-type-px`, so there is nothing below to shadow it.

   ⚠️ LAYOUT IS UNTOUCHED, ON PURPOSE (Y6). Rows, rails and paddings hold their
   size while the words inside them grow — the same separation the text-size
   control depends on. Verified on a 390px frame: base resolving to
   `calc(17px * 1)` on CRM, Huddle and Directory, `body.scrollWidth` still 390,
   zero overflowing or clipped boxes.

   ⚠️ AND IT DOES NOT REPLACE THE `font-size:16px` INPUT PINS. At base 17,
   `--atlas-fs-md` is 15.9 — still under iOS's 16px zoom threshold, so the
   hardcodes that stop a field zooming the page on focus are load-bearing and
   must not be "cleaned up" into tokens. */
html.a-phone{ --atlas-type-px: 17px }

/* DENSITY + TEXT SIZE — orthogonal axes (Y6). Density moves LAYOUT, text size
   moves TYPE, and because they touch different properties they cannot fight. */
:root[data-dsys][data-density="comfortable"], .lab-scope[data-density="comfortable"], .ash-shell[data-density="comfortable"] {
  --atlas-row: 58px;
  --atlas-pad: 28px;
  --atlas-gap: 48px;
  --atlas-lh: 1.6;
}
:root[data-dsys][data-text="large"]  { --atlas-text-scale: 1.125; }
:root[data-dsys][data-text="xlarge"] { --atlas-text-scale: 1.25; }
/* ═══ MOBILE (MB1) · THE FOURTH CONDITIONAL AXIS ════════════════════════════
   Density moves LAYOUT, text size moves TYPE, theme moves COLOR — and this
   moves REACH. Same mechanism as the two blocks above, deliberately: there is
   NO separate mobile token set, because there is no separate mobile app. The
   native shell REMOTE-LOADS this web app (capacitor.config.json → server.url),
   so mobile is these same modules at a narrower viewport.

   ⚠️ THE BREAKPOINT CANNOT BE A TOKEN. var() is not legal in a media query and
   @custom-media has zero native browser support (PostCSS only — and this CSS
   is hand-written and served directly, with no build step). So it is a
   DOCUMENTED CONSTANT, and a constant needs a detector to be real. Measured
   2026-08-22: NINE rebuilt modules carry TWELVE distinct breakpoints —
   720(x4) 900(x2) 1000(x2) 620 640 760 820 560 1100 880. Nobody chose those;
   each module picked one.

   ⚠️ AND A SINGLE WIDTH CANNOT EXPRESS MB2 (iPad is desktop, both
   orientations). The numbers do not close:

       iPhone 16 Pro Max     440 x 956  portrait  ·   956 x 440  landscape
       iPad mini (smallest)  744 x 1133 portrait  ·  1133 x 744  landscape

   A line low enough to keep the iPad mini on desktop (744) sits BELOW
   phone-landscape width (956), so phone landscape would take the desktop shell
   at 440px tall. Hence two clauses, verified against all four cases:

       max-width: 720px                          → phone portrait  (iPad 744 excluded)
       (max-height: 500px) and (pointer: coarse) → phone landscape (iPad landscape is 744 tall)

   720 is also already the most-used value in the codebase, so the sanctioned
   number is the one the code had converged on rather than a new opinion. */
@media (max-width: 720px), (max-height: 500px) and (pointer: coarse) {

  /* SAFE-AREA FLOORS — mobile only, which is why the unconditional block uses
     0. A floor on desktop would push every surface down by it, since env() is
     0 in a desktop browser.

     BOTH FLOORS ARE 24px BECAUSE THAT IS ANDROID'S STANDARD SYSTEM-BAR HEIGHT,
     and Android is the ONLY platform where the chain can fail — iOS reports
     env() correctly, always. So the floor is sized to the exact failure it
     exists for rather than picked. ⚠️ It clears GESTURE navigation (24dp), not
     3-button (~48dp); the escalation for that gap now exists as the
     `html.a-android` floor in the block directly below (native Android, where
     the platform is proven, gets 34px iOS-parity — and the Capacitor-injected
     real inset wins above it). Left/right stay 0: a side inset exists only in
     landscape 3-button, and flooring it would add dead space to every
     portrait. */
  :root {
    --atlas-safe-t: max(var(--safe-area-inset-top,    env(safe-area-inset-top,    0px)), 24px);
    --atlas-safe-b: max(var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)), 24px);
  }

  /* ⭐ NATIVE ANDROID GETS THE iOS-PARITY BOTTOM — 34px, the home-indicator
     inset every bottom clearance in this app was tuned against (2026-09-18,
     Brandon: footers sat "ever so slightly" low and the nav pill rode the
     system-nav zone on Android; *"ideally the proportions are exactly the same
     on android as it is on iOS"*). Android's REAL gesture-nav inset is 24 CSS
     px — smaller than iOS's 34 — so with the chain working perfectly,
     everything still sat 10px closer to the system bar than the design
     intends. `max()` keeps a LARGER real inset in charge: a 3-button bar
     (~48px, injected by Capacitor's SystemBars since 8.3) still wins. This is
     also the escalation the 24px-floor note above reserved for the
     broken-env() 3-button case — a stronger floor on the one platform whose
     class proves where we are, rather than a larger guess for everyone.
     ⚠️ `:not(.a-kb-open)` IS LOAD-BEARING: while the keyboard is up, Android's
     injected bottom inset is 0 (the keys cover the bar) and the base 24px
     floor is what today's keyboard-state geometry was measured with — this
     floor stepping in there would grow every keyboard-open band by 10px.
     At-rest parity only; typing-state geometry stays byte-identical.
     ⚠️ `html.a-android` is stamped by shell.js from Capacitor, native Android
     only — iOS and every browser never match this rule. */
  html.a-android:not(.a-kb-open) {
    --atlas-safe-b: max(var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)), 34px);
  }

  :root[data-dsys], .lab-scope, .ash-shell, .atlas-scope {

    /* REACH — the touch ladder. Apple's 44pt minimum applied to the controls a
       finger actually lands on. NOT applied to everything under 44: a status
       pill never clicks (C34), and a count, tag or progress bar is a report.
       Two of the four answers were ALREADY in the ladder as -lg twins, which is
       the clearest evidence that re-pointing the BASE names is right — a
       surface needs no edit to become touch-correct. */
    --atlas-h-control: 44px;   /* segmented, select, stepper — was 34 */
    --atlas-h-field:   44px;   /* text input — was 36 */
    --atlas-h-btn-sm:  44px;   /* a button is a button — was 36 */
    --atlas-h-nav:     44px;   /* nav item / tab — was 40 */

    /* THE HIT AREA IS NOT THE BOX (A24's mobile consequence). Inflating every
       small control to 44 would destroy the density this app is FOR — a row of
       44px mode pills is a composer nobody wants. So dense controls keep their
       visual size and EXTEND their target with a transparent ::before.
       --atlas-h-mark is 19px and --atlas-h-add is 24px; both are pressed, and
       neither may grow. This token is the floor that ::before is sized to. */
    --atlas-tap-min:   44px;   /* re-point of the desktop base above */

    /* THE NAV PILL (MB3) — floating and detached, not a full-width bar. The
       desktop rail's twin: --atlas-rail-w is what the shell is on a desktop,
       this is what it is on a phone. -min is the minimized height for the
       scroll behaviour, which is a property of the SURFACE and not a global
       (MB4). -clear is what a scroller pads at the bottom so its last row stays
       reachable — DERIVED, so it is never re-typed at a call site and cannot
       drift from the pill it clears. */
    --atlas-pill-h:      56px;
    --atlas-pill-inset:  12px;
    /* ⭐ THE CALL BAR'S HEIGHT (MB129) — ZERO UNLESS ONE IS UP, which is what
       makes every rule below safe to write once. `html.has-cbar` re-points it
       and the whole bottom-chrome system follows: the pill lifts, the pill's
       clearance grows, and the focus-mode give-backs grow, because all three
       are DERIVED from this one term rather than typed at a call site. MB44's
       scar is a clearance paid twice reading as a white block, and the cure is
       that there is only ever one place to pay it.
       ⚠️ It INCLUDES the safe-area inset (the bar sits on the screen edge and
       pads itself), which is why every consumer takes `max(safe-b, cbar-h)`
       rather than adding them — adding would count the inset twice the moment
       a bar appeared. */
    /* ⚠️⚠️ THE DEFAULT LIVES IN THE `var()` FALLBACK, NOT IN A DECLARATION HERE
       — trap 17, and it bit again while building this. `--atlas-cbar-h` is
       written by JS on `<html>`, and this block's scope (`.ash-shell` et al) is
       INSIDE that: declaring `0px` here shadowed the root value for the entire
       shell subtree, so the bar mounted, the token read 99px on `<html>`, and
       the pill sat at 41px exactly as if nothing had happened. Nothing throws;
       the token resolves, to the wrong one, only in the scope that matters.
       Every consumer therefore reads `var(--atlas-cbar-h, 0px)`. */
    --atlas-pill-clear:  calc(var(--atlas-pill-h) + var(--atlas-pill-inset) + max(var(--atlas-safe-b), var(--atlas-cbar-h, 0px)));

    /* SHEETS (MB6) — a drawer, on a phone. The partial detent is CONTENT-DRIVEN
       with a CAP rather than a fixed fraction, because a three-row picker that
       opens to half the screen is a sheet that ignored its own content.
       ⚠️ dvh, never vh — vh does not account for browser chrome, and
       max-height:88vh on the legacy sheet is exactly how the note composer
       ended up unreachable behind the keyboard.
       ⚠️ The RADIUS is a genuinely new asset, raised under L10: the ladder stops
       at --atlas-r-lg (10px), which reads square across a full-width sheet;
       legacy used 22px; iOS sits at 10–16pt. Top corners only. */
    /* ── ONE SHEET MOTION, SHARED (MB43) ──────────────────────────────
       Brandon: *"all slide up sheets should carry a tokened slide up and slide
       down animation that they all share."* There were two answers before this
       and one of them was "none": the drawer transitioned its own transform at
       `--atlas-dur-slow`, and a picker sheet simply appeared. A sheet that pops
       into existence reads as a bug next to one that slides.

       ⚠️ IN AND OUT ARE DIFFERENT DURATIONS ON PURPOSE, and that is the one
       asymmetry worth having: arriving should feel considered, leaving should
       feel immediate — you have already decided. Same reason the press states
       across this app are instant-in and eased-out, inverted, because a sheet
       is the opposite kind of gesture. */
    /* ⭐ THE ENTRANCE IS A SPRING (MB116, 2026-09-12) — Apple's shipped open
       asymmetry (0.45s response, ζ0.9), pre-baked to linear() above. The
       curve covers ~80% of the travel in its first third, so a 610ms natural
       duration READS faster than the old 300ms ease-out while the settle is
       what makes it feel physical. The EXIT stays MB43's fast ease-in —
       leaving is a decision already made, and nothing bounces on close. */
    --atlas-sheet-dur-in:   var(--atlas-spring-open-dur, var(--atlas-dur-slow));
    --atlas-sheet-dur-out:  var(--atlas-dur-fast);
    --atlas-sheet-ease-in:  var(--atlas-spring-open, var(--atlas-ease-out));
    --atlas-sheet-ease-out: var(--atlas-ease-in);
    /* ⚠️ 60 → 80dvh (Brandon, 2026-08-25: *"every sheet that is from a drawer
       or a picker caps height so low… set a max height that is much higher"*).
       At 60 a form-bearing drawer — Edit lead details is the one he photographed
       — was cut off at the third field with most of the screen unused behind it.

       IT IS A CAP, NOT A HEIGHT, and that half was already right: the sheet is
       `height:auto`, so a three-row picker still stops at three rows. Only the
       ceiling moved, so nothing that already fit changes at all.

       ⚠️ RAISED 80→96 AND 92→99 (Brandon, 2026-08-27: *"instead of cap 2/4 of
       the display they can expand up pretty much 99% if they have the content
       to fill it"*). The model is unchanged and is the point: `height:auto`
       with a CAP, so a three-row picker still opens three rows tall — only a
       sheet that HAS the content grows into the new room.

       96 rather than 100 is the iOS convention and keeps both things the strip
       of scrim was for: it says there is something behind this, and it gives
       tap-outside-to-dismiss somewhere to land. At 844 that strip is ~34px —
       thin, but a real target. `is-full` stays meaningfully different at 99. */
    /* ⚠️⚠️ MEASURED FROM BELOW THE TOP INSET, NOT FROM THE SCREEN EDGE
       (2026-08-29 audit, MA-4). At a bare 99dvh a full sheet's top sat at y=9
       on a device with a 62px status bar: the header at 22..79 and the × close
       at 34..66 — the sheet's only exit, entirely under the battery and clock
       (MB18: nothing interactive inside an inset). The partial cap did the same
       thing the moment a keyboard shrank it. UIKit's large detent stops just
       below the status bar for exactly this reason. The strip of scrim above
       the sheet is now the inset plus a breath, which still says "there is
       something behind this" and still gives tap-outside somewhere to land.
       ⚠️ A browser cannot show the old bug — `env()` is 0 there, so 99dvh looked
       perfect at 390×844 — which is how it shipped. */
    /* ⭐⭐ THE MEDIUM DETENT (MB116/MB49-amended, Phase-1 Batch 1-C,
       2026-09-12). The value above this line's history kept climbing —
       60 → 80 → ~97dvh — until "partial" and full were 16px apart and there
       was NO medium anywhere in the system (12-SHEETS' founding measurement).
       The engine has real physics now, so partial becomes a real detent:
       **~64dvh, the middle of MB116's ruled 60–66 band.** Still a CAP, not a
       height — a three-row menu is three rows tall, which is also why
       "menu-auto" needs no token of its own: short content under the cap IS
       menu height, and a nested picker over a drawer caps here too. The 18
       tall sheets that had grown to rely on ~97dvh pass `detent:'full'` at
       their open calls — migrated in the SAME commit as this value, per the
       12-SHEETS §4a regression rule. ⚠️ While the KEYBOARD is up the cap
       escalates to full's (the kit.css rule beside MB30's block): 64dvh minus
       a 335px keyboard is a crushed sheet, and iOS's own sheets grow when the
       keyboard appears. MB49's interim 80dvh figure retires with this — under
       the two-detent model nothing rests there and nothing caps there. */
    --atlas-sheet-partial-max: 64dvh;
    --atlas-sheet-full:        calc(100dvh - var(--atlas-safe-t) - var(--atlas-s8));
    --atlas-sheet-r:           16px;
  }
}


/* ═══ COMPATIBILITY LAYER ═══════════════════════════════════════════════════
   The legacy vocabulary mapped onto the new semantics.

   WHY THIS EXISTS: the two systems do not share names. Legacy says
   `--atlas-text` / `--atlas-border` / `--atlas-brand`; this file says
   `--atlas-ink` / `--atlas-line` / `--atlas-accent`. Without this block,
   flipping the flag would produce new BACKGROUNDS with legacy TEXT — a
   half-migrated surface that is worse than either system on its own.

   THIS IS THE ONE SANCTIONED PLACE for semantic→semantic aliasing, and it is
   temporary by construction: every legacy name here is deleted the moment its
   call sites move to the new one. It is a bridge with a demolition date, not
   a tier.

   THE INTERESTING PART: three families do not survive translation, and that is
   the point rather than a gap —
     · `-soft`  was a TINTED surface. L3 forbids tint, so it maps to the
       neutral ladder. Every tinted pill in the app flattens the moment the
       flag flips.
     · `-glow`  was a colored shadow. L4 forbids it, so it maps to `none`.
       ~4 glow tokens stop rendering rather than being restyled.
     · `-grad`  was the violet→blue AI gradient. A30 replaced it with Coral,
       and L6 allows exactly one gradient — so all four legacy stops collapse
       onto the two real ones.
   Those three are not lossy conversions. They are the AI tells being deleted
   wholesale, which is most of what this migration is FOR. */
:root[data-dsys] {
  /* type */
  --atlas-font-display: var(--atlas-font);
  --atlas-font-body:    var(--atlas-font);
  /* --atlas-font-mono is NOT a legacy alias — it is one of ours, declared in
     the token block above. Re-declaring it here was a second definition of the
     same name (check 19), harmless only because the values happened to match. */
  --atlas-font-serif:   var(--atlas-font);

  /* ink */
  --atlas-text:   var(--atlas-ink);
  --atlas-text-2: var(--atlas-ink-2);
  --atlas-text-3: var(--atlas-ink-3);
  --atlas-text-4: var(--atlas-ink-3);   /* the 4-step ramp collapses to 3 (L11) */

  /* lines */
  --atlas-border:        var(--atlas-line);
  --atlas-border-strong: var(--atlas-line-2);
  --atlas-border-focus:  var(--atlas-accent);

  /* the accent was called "brand" */
  --atlas-brand:        var(--atlas-accent);
  --atlas-brand-bright: var(--atlas-accent);
  --atlas-brand-ring:   var(--atlas-accent);
  --atlas-brand-soft:   var(--atlas-bg-2);   /* was a tint (L3) */
  --atlas-brand-grad:   var(--atlas-accent); /* was a gradient (L6) */
  --atlas-bg-tint:      var(--atlas-bg-2);   /* was a tint (L3) */

  /* roles — the base names already match, so only the variants map */
  --atlas-money-bright: var(--atlas-money);
  --atlas-money-ring:   var(--atlas-money);
  --atlas-money-soft:   var(--atlas-bg-2);
  --atlas-warn-bright:  var(--atlas-warn);
  --atlas-warn-ring:    var(--atlas-warn);
  --atlas-warn-soft:    var(--atlas-bg-2);
  --atlas-urgent-bright: var(--atlas-urgent);
  --atlas-urgent-ring:   var(--atlas-urgent);
  --atlas-urgent-soft:   var(--atlas-bg-2);

  /* "celebrate" has no equivalent — closing a deal is money (K-color) */
  --atlas-celebrate:        var(--atlas-money);
  --atlas-celebrate-bright: var(--atlas-money);
  --atlas-celebrate-ring:   var(--atlas-money);
  --atlas-celebrate-soft:   var(--atlas-bg-2);

  /* AI — four legacy gradient stops collapse onto the two real ones (A30) */
  --atlas-ai:        var(--atlas-ai-a);
  --atlas-ai-bright: var(--atlas-ai-a);
  --atlas-ai-ring:   var(--atlas-ai-a);
  --atlas-ai-soft:   var(--atlas-bg-2);
  --atlas-ai-grad-start: var(--atlas-ai-a);
  --atlas-ai-grad-mid:   var(--atlas-ai-a);
  --atlas-ai-grad-mid2:  var(--atlas-ai-b);
  --atlas-ai-grad-end:   var(--atlas-ai-b);

  /* transaction status — mapped by MEANING, which is the whole argument for a
     semantic palette: "under contract" is warn because it is pending, not
     because someone picked amber. */
  --atlas-st-listing:   var(--atlas-accent);
  --atlas-st-uc:        var(--atlas-warn);
  --atlas-st-closed:    var(--atlas-money);
  --atlas-st-cancelled: var(--atlas-urgent);

  /* elevation — three legacy steps onto the ONE lift (F12) */
  --atlas-shadow-sm: var(--atlas-lift);
  --atlas-shadow-md: var(--atlas-lift);
  --atlas-shadow-lg: var(--atlas-lift);

  /* GLOWS ARE DELETED (L4). Not restyled — removed. */
  --atlas-glow-brand:  none;
  --atlas-glow-money:  none;
  --atlas-glow-warn:   none;
  --atlas-glow-urgent: none;

  /* radius */
  --atlas-radius-sm:   var(--atlas-r-sm);
  --atlas-radius:      var(--atlas-r-md);
  --atlas-radius-lg:   var(--atlas-r-lg);
  --atlas-radius-xl:   var(--atlas-r-lg);
  --atlas-radius-pill: var(--atlas-r-full);

  /* ── THE THIRD VOCABULARY ────────────────────────────────────────────────
     Atlas has not two token vocabularies but THREE, and this is the oldest:
     the `--surface-*` / `--text-*` / `--border-*` intermediate layer plus the
     original short aliases (`--bg`, `--brand`, `--text`).

     WHY IT MATTERS MORE THAN ITS SIZE SUGGESTS: `body { background: var(--bg) }`.
     Without this block the flag flips every `--atlas-*` and `--hub-*` token
     correctly and the page still renders NAVY, because the single most visible
     surface in the app reads from a vocabulary neither of them covers. Found by
     checking the computed body background on the running app rather than the
     tokens — the tokens all said the right thing.

     Same deal as the block above: temporary, deleted per-name as call sites
     migrate. */
  --surface-page:     var(--atlas-bg);
  --surface-canvas:   var(--atlas-bg);
  --surface-card:     var(--atlas-bg-1);
  --surface-elevated: var(--atlas-bg-2);
  --surface-overlay:  var(--atlas-bg-1);

  --border-subtle:  var(--atlas-line);
  --border-default: var(--atlas-line);
  --border-strong:  var(--atlas-line-2);

  --text-primary:   var(--atlas-ink);
  --text-secondary: var(--atlas-ink-2);
  --text-tertiary:  var(--atlas-ink-3);
  --text-inverse:   var(--atlas-on-accent);

  --accent-primary:       var(--atlas-accent);
  --accent-primary-hover: var(--atlas-accent);
  --accent-primary-soft:  var(--atlas-bg-2);   /* was a tint (L3) */
  --accent-focus-ring:    var(--atlas-accent);
  --accent-ai:            var(--atlas-ai-a);
  --accent-ai-hover:      var(--atlas-ai-a);
  --accent-ai-border:     var(--atlas-ai-a);
  --accent-ai-soft:       var(--atlas-bg-2);

  --state-danger:  var(--atlas-urgent);
  --state-warning: var(--atlas-warn);
  --state-success: var(--atlas-money);
  --state-info:    var(--atlas-accent);

  --hover-subtle: var(--atlas-bg-2);
  --hover-medium: var(--atlas-bg-3);

  /* Three shadow steps onto the ONE lift (F12). --shadow-color has no
     equivalent — the lift is a complete value, not a color to compose with. */
  --shadow-color: rgba(20,24,30,.28);
  --shadow-sm: var(--atlas-lift);
  --shadow-md: var(--atlas-lift);
  --shadow-lg: var(--atlas-lift);

  --scrollbar-thumb:       var(--atlas-line-2);
  --scrollbar-thumb-hover: var(--atlas-ink-3);

  /* The original short aliases. `--bg` is the one that paints <body>. */
  --brand:       var(--atlas-accent);
  --brand-light: var(--atlas-accent);
  --brand-dim:   var(--atlas-ink-3);
  --bg:   var(--atlas-bg);
  --bg2:  var(--atlas-bg-2);
  --bg3:  var(--atlas-bg-3);
  --border:  var(--atlas-line);
  --border2: var(--atlas-line-2);
  --text:  var(--atlas-ink);
  --text2: var(--atlas-ink-2);
  --text3: var(--atlas-ink-3);
  --green: var(--atlas-money);
  --amber: var(--atlas-warn);

  /* ── THE INVERSION (Step 3) ──────────────────────────────────────────────
     `--hub-*` now derives FROM `--atlas-*`, not the other way round.

     Legacy has it backwards: `--atlas-bg: var(--hub-bg)`, so the vocabulary
     everyone actually writes (5,558 uses) is the DERIVED one and the source of
     truth is the layer nobody uses (938). Under the flag this block wins for
     BOTH names, so there is no cycle in either direction — and the ~938
     remaining `var(--hub-*)` call sites inherit the new system for free,
     rather than needing a rename. */
  --hub-bg: var(--atlas-bg);           --hub-bg-1: var(--atlas-bg-1);
  --hub-bg-2: var(--atlas-bg-2);       --hub-bg-3: var(--atlas-bg-3);
  --hub-bg-tint: var(--atlas-bg-2);
  --hub-border: var(--atlas-line);     --hub-border-strong: var(--atlas-line-2);
  --hub-border-focus: var(--atlas-accent);
  --hub-text: var(--atlas-ink);        --hub-text-2: var(--atlas-ink-2);
  --hub-text-3: var(--atlas-ink-3);    --hub-text-4: var(--atlas-ink-3);
  --hub-brand: var(--atlas-accent);    --hub-brand-bright: var(--atlas-accent);
  --hub-brand-soft: var(--atlas-bg-2); --hub-brand-ring: var(--atlas-accent);
  --hub-brand-grad: var(--atlas-accent);
  --hub-money: var(--atlas-money);     --hub-money-bright: var(--atlas-money);
  --hub-money-soft: var(--atlas-bg-2); --hub-money-ring: var(--atlas-money);
  --hub-warn: var(--atlas-warn);       --hub-warn-bright: var(--atlas-warn);
  --hub-warn-soft: var(--atlas-bg-2);  --hub-warn-ring: var(--atlas-warn);
  --hub-urgent: var(--atlas-urgent);   --hub-urgent-bright: var(--atlas-urgent);
  --hub-urgent-soft: var(--atlas-bg-2);--hub-urgent-ring: var(--atlas-urgent);
  --hub-celebrate: var(--atlas-money); --hub-celebrate-bright: var(--atlas-money);
  --hub-celebrate-soft: var(--atlas-bg-2); --hub-celebrate-ring: var(--atlas-money);
  --hub-ai: var(--atlas-ai-a);         --hub-ai-bright: var(--atlas-ai-a);
  --hub-ai-soft: var(--atlas-bg-2);    --hub-ai-ring: var(--atlas-ai-a);
  --hub-st-listing: var(--atlas-accent); --hub-st-uc: var(--atlas-warn);
  --hub-st-closed: var(--atlas-money);   --hub-st-cancelled: var(--atlas-urgent);
  --hub-shadow-sm: var(--atlas-lift);  --hub-shadow-md: var(--atlas-lift);
  --hub-shadow-lg: var(--atlas-lift);
  --hub-glow-brand: none;              --hub-glow-money: none;
  --hub-glow-warn: none;               --hub-glow-urgent: none;
  --hub-radius-sm: var(--atlas-r-sm);  --hub-radius: var(--atlas-r-md);
  --hub-radius-lg: var(--atlas-r-lg);  --hub-radius-xl: var(--atlas-r-lg);
  --hub-radius-pill: var(--atlas-r-full);
  --hub-font-display: var(--atlas-font); --hub-font-body: var(--atlas-font);
  --hub-font-mono: var(--atlas-font-mono); --hub-font-serif: var(--atlas-font);
}

/* ═══ TIER 3 · COMPONENT ════════════════════════════════════════════════════
   Deliberately thin. A component token earns its place only when the value
   genuinely varies per component — otherwise it is a semantic token with a
   longer name, and every one of those is a place the system can drift. */
:root[data-dsys] {
  --atlas-ctl-h:     36px;   /* toolbar buttons, selects, search — one height */
  --atlas-ctl-h-sm:  30px;   /* in-composer and in-row controls */
  --atlas-mark:      19px;   /* checkbox / radio — the mark controls (A24) */
  --atlas-tbl-head:  38px;   /* table header, shorter than a row (T-A1) */
}

/* ── PRESS ON TOUCH IS A SCALE (D-2 / P41c amendment, 2026-09-12) ──────────
   On a pointer the press is a 1px depress plus the step-down the cursor can
   see. A thumb COVERS what it presses — the one visible channel is the
   element's own edges, so the motion becomes a small scale (the card proved
   it at .985; controls get .97 so the travel is comparable). `pointer:
   coarse`, never a width — an iPad is a touch screen at desktop width.
   Desktop is untouched: this block only re-points the token. */
@media (pointer: coarse){
  :root[data-dsys], .lab-scope, .ash-shell, .atlas-scope {
    --atlas-press-t: scale(var(--atlas-press-scale-ctl, .97));
    --atlas-press-t-row: scale(var(--atlas-press-scale, .985));
  }
  /* Inside a scroller, the press waits ~150ms so a scroll never flashes it
     (iOS delaysContentTouches — WWDC14-235's own number). Declared on the
     scroller, INHERITED by every control inside it, read by each family's
     `:active` transition — no specificity war, whatever the family's own
     weight. A tap released before 150ms simply never shows the press, which
     is exactly how a UITableView behaves. */
  [data-kb-scroll], .tbl2-fill, .drw-b, .pop {
    --atlas-press-in: transform 0s linear 150ms;
  }
}
