/* ══════════════════════════════════════════════════════════════════════════
   ThinqProfit — DESIGN TOKENS.  GENERATED FILE — DO NOT EDIT.

   Extracted verbatim from app/styles.css by scripts/design/build-kit.mjs.
   Edit the source stylesheet and rebuild; an edit here is lost on the next
   build and, worse, drifts silently in the meantime.

   Drop this in ahead of your own CSS. It declares every token on :root, on
   [data-theme="light"], and on the density and font-stack attributes. The
   two-ground rule depends on --lift-c / --ink-c / --drop-c being present, so
   do not cherry-pick a subset.
   ══════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════════
   THE TOKEN SUBSTRATE — ported from design-suite `src/tokens.css` @ 2e4e334.

   Every value below is justified in `docs/design/DESIGN.md`; do not change one
   without reading the section that owns it. The record's own rule applies here
   verbatim: several "obvious improvements" were already tried and measured as
   wrong, and the measurement is in the section, not in the diff.

   THREE THINGS THIS FILE IS, IN ORDER:
     1. the design-suite tokens, under design-suite NAMES, on two grounds
     2. an ALIAS layer mapping our 83 legacy names onto them, so 5,183 lines of
        CSS below keep working while Phases 5–6 rewrite them file by file
     3. the handful of tokens that are genuinely OURS (control heights, the
        transition property lists, the categorical calendar hues), each named
        and pointed at a governed value rather than a literal

   WHY THE ALIAS LAYER IS NOT PERMANENT: an alias is a second name for one
   decision, which is survivable. A second VALUE for one decision is not — that
   is the mirror the record forbids ("never mirror a token as a literal"), and
   it is why `applyTheme()` no longer emits a palette. CSS declares; JS reads.

   Fonts are self-hosted — see the @fontsource imports in app/main.tsx and
   DESIGN.md §5. A CDN @import for Hanken Grotesk + IBM Plex Mono stood here;
   an @import is not discovered until this stylesheet has parsed, so it put a
   second round trip in front of first paint. Hanken Grotesk is retired with
   it: §5 records that it came from the previous theme and was never
   re-examined.
   ══════════════════════════════════════════════════════════════════════════ */

/* ══ THE FOUR FLIP TOKENS — what makes a theme a token swap  §37 ═══════════
   Everything else here is a colour DECISION. These four are the DIRECTION of
   the theme, as rgb triples, so one alpha ladder reads correctly under either
   ground:
     --ink-c    the ink base.   dark: white   light: a warm near-black
     --lift-c   surface overlays (row hover, chip fills, inset washes).
                dark: white raises a surface; light: black lowers it
     --drop-c   shadow ink. BLACK IN BOTH — a shadow is an absence of light,
                not an inversion of the palette
     --grid-c   chart gridlines and crosshair. The engine cannot read CSS
                variables, so `themes.ts` resolves these off :root and hands
                them to the controller.
   A neutral IS a choice once there are two grounds: upstream's light theme
   shipped with invisible row hovers, invisible dividers and an invisible
   strike spine because 55 hard-coded rgba(255,255,255,…) scrims were exempted
   as "not a colour choice". */
:root {
  --ink-c: 255,255,255;
  --lift-c: 255,255,255;
  --drop-c: 0,0,0;
  --grid-c: 255,255,255;
}

:root {
  /* Dark is the default ground; tell the UA so native widgets (select popups,
     date pickers, unstyled scrollbars) render dark instead of UA-white. */
  color-scheme: dark;

  /* ── ground ─────────────────────────────────────────────── §2 · PORT §6.1
     ══ THE CAST FLIPPED WARM → COOL, AND THE BRAND MOVED OFF THE SURFACES ══
     This block used to read "TINTED, not neutral — every surface carries a few
     degrees of the brand hue so the product feels like Thinq everywhere WITHOUT
     spending the accent to do it". That reasoning was sound for a CORAL accent
     painted into the panel fill. The ported system solves the same problem with
     a different mechanism and the old sentence is now false, so it is gone
     rather than left to mislead.

     The ported answer (PORT §6.1): brand presence is an AMBIENT GLOW on the
     canvas — one user-selectable variable driving five gradient stops — and the
     panels are translucent, so it bleeds through every surface without any of
     them owning a hue. That is why the surface ramp itself is now COOL neutral
     (generated at hue 252, chroma rising as lightness falls so dark surfaces do
     not read as dead grey under the green/red data colours sitting on them).

     MEASURED, not carried over. R − min(G,B) is 2 / 2 / 3 / 5 — `surface.neutral`
     is a ceiling of 6 and cool sits far under it. Adjacent steps:
       bg→panel 1.050 · panel→panel2 1.113 · panel2→elev 1.149
     bg→panel is DELIBERATELY shallower than the 1.079 this ladder shipped with.
     §24b spent lightness on that step because a solid panel had nothing else to
     separate with. Here it does: PORT §1.1 invariant 1 makes depth "surface
     value + blur + a 1px inset white catch-light", and P2 puts the glass on.
     During P1 alone the workspace therefore reads slightly flatter than before
     — that is the phase boundary, not a regression, and P2 closes it. */
  --bg: #000000;      --bg-rgb: 0,0,0;      /* pure black page base  */
  --canvas: #000000;                        /* pure black chart ground      */

  --panel: rgba(0, 0, 0, 0.28);  --panel-rgb: 0,0,0; /* neutral translucent glass widget surface */
  --panel2: rgba(18, 18, 18, 0.35); --panel2-rgb: 18,18,18;/* inset / nested translucent glass */
  --elev: rgba(24, 24, 24, 0.85); --elev-rgb: 24,24,24;    /* menus, popovers */
  --surface: rgba(0, 0, 0, 0.28);
  --surface-2: rgba(18, 18, 18, 0.35);

  /* ── GLASS ELEVATION & SURFACE TOKENS ── PORT §3, §6.2 */
  --glow-rgb: 0, 0, 0;
  --surface-canvas:          #000000;
  --surface-canvas-gradient: none;

  --surface-chrome:          rgba(0, 0, 0, 0.25);
  --surface-panel:           rgba(0, 0, 0, 0.28);
  --surface-panel-solid:     #0A0A0A;
  --surface-overlay:         rgba(16, 16, 16, 0.92);
  --surface-modal:           rgba(14, 14, 14, 0.96);
  --surface-scrim:           rgba(0, 0, 0, 0.70);
  --surface-raised:          rgba(255, 255, 255, 0.06);

  --surface-g1: var(--surface-chrome);
  --surface-g2: var(--surface-panel);
  --surface-g3: var(--surface-overlay);
  --surface-g4: var(--surface-modal);

  --glass-chrome:  blur(20px) saturate(170%);
  --glass-panel:   blur(28px) saturate(200%);
  --glass-overlay: blur(36px) saturate(210%);
  --glass-modal:   blur(42px) saturate(220%);
  --scrim-blur:    blur(4px);

  --sweep-stop-1: rgba(255, 255, 255, 0.040);
  --sweep-stop-2: rgba(235, 235, 240, 0.025);
  --sweep-stop-3: rgba(245, 240, 235, 0.015);
  --sweep-stop-4: rgba(230, 225, 240, 0.008);
  --sweep-angle: 38deg;
  --sweep-reach: 620px;

  --edge-top-1: inset 0 1px 0 0 rgba(255, 255, 255, 0.08);
  --edge-top-2: inset 0 1px 0 0 rgba(255, 255, 255, 0.12);
  --edge-top-3: inset 0 1px 0 0 rgba(255, 255, 255, 0.16);

  --radius-control: 999px; /* chip, tab, segmented, pill button — capsule */
  --radius-field:   6px;   /* text input, select, stepper, menu item      */
  --radius-surface: 10px;  /* panel, card, popover, toast                 */
  --radius-modal:   14px;  /* dialog, drawer                              */
  --radius-dot:     50%;   /* status dots, avatars                        */

  --size-display: 28px;
  --size-heading: 18px;
  --size-title:   14px;
  --size-body:    13px;
  --size-label:   12px;
  --size-micro:   11px;
  --weight-semibold: 600;
  --dur-fast: 120ms;
  --z-dropdown: 100;

  /* ── hairlines ── PORT §1.1 invariant 1
     These were WARM (rgba(255,158,122,…)) and carried "brand presence in the
     structure itself". The ported system forbids exactly that: brand is the
     ambient glow, and a hairline is a CATCH-LIGHT — the 1px inset white edge
     that, with surface value and blur, IS the elevation model. Achromatic
     white-alpha, at PORT §1.1's own 0.03–0.08 band for frame hairlines. */
  --hair-w: rgba(var(--lift-c),.06);
  --hair-w2: rgba(var(--lift-c),.12);

  /* PANEL EDGE — §10. Separate from the hairlines because they do different
     jobs: a hairline divides content INSIDE a panel and must stay under the
     numbers; the edge divides one WIDGET from the next and has to be seen
     across a six-panel workspace. Measured against --panel:
       .09 → 1.14:1 invisible · .16 → 1.53:1 · .30 → 1.84:1 · .48 → 2.99:1
     RESTING EDGE IS NEUTRAL: six docked widgets means six coral edges, and
     §15's test is "how many of these are on screen at once?" — more than three
     is chrome, and chrome is neutral. The active edge keeps coral at .30, a
     1.20x step: an orientation signal has to be FOUND when looked for, not
     SEEN when not. */
  --edge: rgba(255,255,255,.16);
  --edge-focus: rgba(255,255,255,.24);

  /* ══ ACCENT — ACHROMATIC ══════════════════════════════ §4 · PORT §1.1 (2)
     THE ACCENT NO LONGER HAS A HUE, AND THAT IS THE POINT.

     §4 placed coral by a sound argument — a brand accent that is not green and
     not red, so "you can act on this" never collides with "the market moved".
     The ported system reaches the same goal by a stricter route and PORT §1.1
     makes it invariant 2: `--bull` / `--bear` mean P&L and market direction
     ONLY — "not accents, not selection, not focus, not ITM shading" — which
     only holds if the accent itself carries no hue to compete with them.

     Coral cleared §4's own test but not this one. Its hue sits at 16°, in the
     red-orange region, ~26° from --down at 350°; PORT §5.2's rule is that the
     accent is achromatic "so green and red mean one thing only", and 26° of
     separation is a near-miss rather than an exemption. The CVD palette is the
     single documented exception (it takes a violet, because against blue/orange
     direction colours a silver accent stops being distinguishable from bull).

     WHAT THIS COSTS AND WHY IT IS PAID: the product loses a chromatic brand
     mark on its controls. It does not lose brand — PORT §6.1 moves brand to the
     ambient glow, which is bigger, user-selectable, and bleeds through every
     translucent surface. Owner decision, 2026-08-12.

     `--silver` (the METAL white ramp's mid stop, r≈g≈b, declared below for the
     wordmark) is the same family; the accent is its bright end. */
  --accent-base: #F3F4F6;  --accent-base-rgb: 243,244,246;   /* PORT §5.2 */
  /* hover is a LIGHTNESS move, never a hue shift — PORT §5.2 states this
     explicitly and it is the whole discipline of an achromatic accent. */
  --accent-hi: #FFFFFF;

  /* ── the coral names, kept as ALIASES ──────────────────────────────
     65 call sites across styles.css / panels.css name --coral, --coral-rgb,
     --coral-ring-rgb and --coral-lt. Re-pointing them is one decision under an
     old name — the record's own definition of a survivable alias — where
     rewriting 65 sites in the same commit that changes the palette is how a
     re-skin becomes a rewrite and stops being revertable. The names go in P3,
     with their call sites, not here.
     THE NAMES NOW LIE, so they are marked: there is no coral in the product. */
  --coral: var(--accent-base);  --coral-rgb: var(--accent-base-rgb);   /* legacy name */
  --coral-lt: var(--accent-hi);                                        /* legacy name */
  /* THE ACCENT AS A BOUNDARY — §37. Every 1px ring and active panel edge points
     HERE, never at the accent directly. The §37 problem it was written for is
     GONE on dark — an achromatic accent has no hue to darken and no terracotta
     to fall into — but the token stays because the LIGHT ground still needs a
     boundary solved against paper, and because a rule pointing at the accent
     directly is the defect §37 records. */
  --coral-ring: var(--accent-base);  --coral-ring-rgb: var(--accent-base-rgb);
  /* The lockup ramp. Was a six-stop coral gradient; with an achromatic accent
     the mark and the wordmark are one material, so it IS the METAL white ramp
     declared below rather than a second copy of it. */
  --coral-grad: var(--silver-grad);

  /* ── mark: PLATINUM (cool, deliberately not the accent) ── §7 */
  --plat: #C3CCE2;  --plat-rgb: 195,204,226;  --plat-lt: #EAEFFA;
  /* ══ THE WORDMARK'S SILVER — NEUTRAL, AND NOT `--plat` ═══════════════════
     Owner directive 2026-08-05: the mark carries coral, the wordmark is silver,
     and the wordmark is ONE colour end to end (a lone differently-coloured "q"
     read as a rendering fault and was reported as one).

     `--plat` is the wrong silver and Mark.tsx says why: "PLATINUM — cool white
     metal with a faint iridescent shift (violet into blue)". Pointing the
     wordmark at it made the word read BLUISH — report two. The WHITE ramp next
     to it exists for exactly this: "neutral steel … r=g=b at every stop …
     nothing in the ramp shares a hue with anything else in the system, because
     it has no hue at all." `--silver` is that ramp's mid stop; `--silver-grad`
     is the whole ramp, for the sheen across the splash wordmark. */
  --silver: #C9C9CE;  --silver-rgb: 201,201,206;
  --silver-grad: linear-gradient(104deg,#6E6E72,#FFFFFF 26%,#E9E9EB 46%,#8C8C90 66%,#FDFDFE 88%,#B6B6BA);  /* lint-tokens-ok: METAL white ramp, the brand gradient itself */
  --brand-q: var(--silver);
  --plat-grad: linear-gradient(104deg,#7C84A2,#FFFFFF 26%,#DEE6F6 46%,#98A0BC 66%,#F6F0FF 88%,#AEB6CC);

  /* ── data. NEVER used for brand or chrome ───────────────── §4 · PORT §5.3
     ══ TWO INDEPENDENT DIRECTION CHANNELS ═════════════════════════════════
     PORT §5.3 splits market direction in two, and the split is the point: a
     trader may run one palette on the CANDLES and a different one on P&L and
     section colour ("Tokyo Nikkei candles above SGX P&L" is a supported
     configuration, PORT §14.4). One pair of tokens cannot express that.

       --bull      / --bear         P&L, sections, every DOM direction colour
       --bull-chart/ --bear-chart   the candle series, read by the ENGINE only

     --up / --down stay the substrate names for the DOM channel because 5,183
     lines already read them; --bull / --bear alias onto them in the alias
     layer, so both spellings are one value and there is nothing to keep in
     sync. The chart pair is NEW and has no legacy name.

     NOTHING MAY ASSUME GREEN = UP. Two of the ported palettes (shanghai-sse,
     korea-krx) are inverted, red-up, and they are shipped in P6.

     Defaults are the ported system's: DOM = Singapore SGX (jade/rose), candles
     = Tokyo Nikkei (mint/crimson). */
  /* up · hue 173.4 — jade teal. §4 chose the old green's lightness to match
     coral; with the accent achromatic there is no lightness to match, so this
     is PORT §5.3's value unmodified. 7.60:1 on --panel. */
  --up: #a6f953;    --up-rgb: 166,249,83;
  /* down · hue 349.7 — rose. §4's note that "hue CANNOT clear coral" is now
     void: coral is retired, so the pair is placed on its own merits and the
     separation is no longer carried by lightness under protest. 5.15:1. */
  --down: #ff3d3d;  --down-rgb: 255,61,61;

  /* ── the CANDLE channel — read by the engine, never by CSS ── PORT §14.4
     `themes.ts` resolves these off :root and hands them to the WebGL
     controller, which cannot read custom properties. They are deliberately
     absent from every CSS rule: a DOM element wearing a candle colour is the
     bug the two-channel split exists to prevent.
     --bear-chart is 3.92:1 on --panel — PORT §5.5 marks it "passes as a
     graphical object (>= 3:1); do not use as text", and a candle is a
     graphical object. */
  --bull-chart: #cbe95d;  --bull-chart-rgb: 203,233,93;
  --bear-chart: #ff3d51;  --bear-chart-rgb: 255,61,81;

  /* Direction at reduced strength. PORT §5.3 fixes both alphas: .18 for the
     bed under an OI bar or a filled cell, .08 for the tick flash. Declared as
     tokens rather than written inline at 40-odd call sites. */
  --bull-muted: rgba(var(--up-rgb),.18);   --bear-muted: rgba(var(--down-rgb),.18);
  --bull-tint:  rgba(var(--up-rgb),.08);   --bear-tint:  rgba(var(--down-rgb),.08);

  --warn: #E49B68;  --warn-rgb: 228,155,104;
  --info: #808CF5;  --info-rgb: 128,140,245;
  /* ── SEMANTIC STATUS, DISTINCT FROM DIRECTION ────────────── PORT §5.4
     NEW. The system had --warn and --info but no success/danger pair, so every
     "order placed" and "order rejected" surface reached for --up / --down —
     which is precisely invariant 2's failure mode: the UI shouts market-
     direction green at a user who just saved a workspace layout. A successful
     order confirmation is --success, NOT --bull; a failed one is --danger, NOT
     --bear. Beds are the 12% wash PORT §5.4 specifies. */
  --success: #3EBA97;  --success-rgb: 62,186,151;
  --danger:  #E56B6F;  --danger-rgb: 229,107,111;
  --info-bed:    rgba(var(--info-rgb),.12);
  --success-bed: rgba(var(--success-rgb),.12);
  --warn-bed:    rgba(var(--warn-rgb),.12);
  --danger-bed:  rgba(var(--danger-rgb),.12);
  --pnl-up: #67D574;    --pnl-up-rgb: 103,213,116;
  --pnl-down: #FF6B90;  --pnl-down-rgb: 255,107,144;
  --pnl-up-bed:   rgba(var(--pnl-up-rgb),.12);
  --pnl-down-bed: rgba(var(--pnl-down-rgb),.12);

  /* Ink for text sitting ON a solid semantic fill — real decisions with
     computed contrast, not stray literals. RE-MEASURED against the ported
     fills, because an ink is only correct for the exact colour under it:
       on-up 7.65:1 (was 10.5 on the old green) · on-down 5.35:1
       on-accent 17.97:1
     --ink-on-accent is PORT §5.5's `--text-on-accent` #0A0A0C. The old #2E0F06
     was a warm near-black solved for a coral fill and still measures 16.07:1
     here, but it carries a red cast that has no reason to exist on an
     achromatic accent — the ported value is both more contrasty and neutral. */
  --ink-on-up: #08120D;  --ink-on-down: #1A0509;  --ink-on-accent: #0A0A0C;
  /* SCRIMS DO NOT FLIP. A chart overlay floats over a candle series and has to
     stay legible there on BOTH grounds — it is an ON-SCRIM colour, a separate
     role from --ink1. Taking --ink1 put the order-line close at 4.36:1 on the
     light ground: dark ink on a dark surface. */
  --ink-on-scrim: rgba(255,255,255,.94);
  /* Lifted variants for text on a TINT of the same hue — a tint eats contrast,
     so the label needs raising off it. RE-SOLVED on the ported pair by holding
     HSL hue and saturation EXACTLY and moving only lightness, which is §37's
     own derivation rule applied within a ground rather than across two:
       --up-lift   173.21° vs --up   173.41°  (0.20° drift)  12.90:1 on --panel
       --down-lift 349.21° vs --down 349.72°  (0.51°)         9.72:1
     Picking these by eye is what produces a "lifted" variant that is quietly a
     different colour from the thing it is lifting. */
  --up-lift: #a6f953;  --down-lift: #ff3d3d;
  /* TEXT-STRENGTH ROLE TOKENS — §37. Every coloured-TEXT rule points at these,
     never at --up / --down / --warn. On dark they ARE those tokens, so the
     dark render is byte-identical. On light they resolve to text-strength
     variants, because there the base tokens are FILL strength (3:1) and would
     fail AA as text. One rule, two grounds — the alternative is a
     [data-theme] override per selector, i.e. the fork this architecture
     exists to avoid. */
  --up-ink: var(--up);
  --down-ink: var(--down);
  --warn-ink: var(--warn);
  /* `partial` is the one order state that is neither a direction nor a fault.
     --info is the only hue that says that: 210°, the far side of the wheel
     from both the up/down pair (173°/350°), so a trader who cannot separate
     red from green still separates this from both. The old note also cited the
     accent at 16° — the accent is achromatic now and reserves no hue at all,
     which only widens the space this argument was made in. */
  --info-ink: var(--info);

  /* DIMMED — for REFERENCE readouts, not signals. The OHLC strip states what a
     bar did; it is not telling you to look. At full strength a five-value row
     of saturated green sat beside the headline price and out-shouted it. Same
     hue, lower volume — re-solved on the ported pair by the same hold-H-and-S,
     move-L-only method as the lifts: 6.37:1 and 4.60:1 on --panel (the old
     pair sat at 7.94 and 5.05 against a lighter --panel). */
  --up-dim: #12A898;  --down-dim: #F22347;

  /* ── INDICATOR LINES ────────────────────────────────────── §20
     A DEDICATED family. An indicator must never wear the brand accent or a
     semantic colour: it would be claiming a meaning it does not have — coral
     says "act here", green/red say "the market moved".
     Placed by elimination: ≥30° from every reserved hue. RE-MEASURED after the
     port moved four of the six (accent 16 → achromatic, i.e. it reserves
     nothing · warn 35 → 40 · up 163 → 173 · down 340 → 350; info 208 → 210 and
     plat 223 hold). All three still clear, and the band only widened:
       ind-1 256° — tightest clearance 33.1° (--plat)
       ind-2 281° — 58.5°        ind-3 306° — 44.1° (--down)
     The values themselves DO NOT MOVE. §20 placed them and the placement still
     holds; re-picking them because a neighbouring hue shifted would re-argue a
     settled decision for no measured reason. The 66–119° band stays
     66–119° band is DISCARDED despite passing the maths — it is yellow-green
     through green, and any green line on a candle chart reads as "up" whatever
     its hue angle says. That risk is semantic, not chromatic, and the
     calculation cannot see it. Rotated in sRGB hue at constant HSL S/L, the
     space §20's table measures; an OKLCH rotation to 256° produced a blue. */
  --ind-1: #9F84EB;  --ind-1-rgb: 159,132,235;  /* periwinkle 256° · 6.57:1 */
  --ind-2: #BA79D8;  --ind-2-rgb: 186,121,216;  /* violet     281° · 6.41:1 */
  --ind-3: #D883D0;  --ind-3-rgb: 216,131,208;  /* orchid     306° · 7.59:1 */
  --ind-4: #9AA3B8;  --ind-4-rgb: 154,163,184;  /* slate — low chroma, 4th   */

  /* ── WIDGET LINK CHANNELS ──────────────────────── §20 · PORT §5.4
     The teardown's "connective tissue". Distinct from every semantic hue so a
     link marker is never mistaken for state — PORT §5.4 restates the same rule
     ("for cross-widget symbol linking only, never for status or direction")
     and ships SIX where this system had four. The two extra are declared here
     so a fifth linked widget stops recycling channel 1.
     The four legacy --group-N names alias onto the ported channel nearest each
     one's own hue family, so no linked workspace changes which channel it is
     on — only the exact pigment moves. */
  --channel-1: #6E9BF5;  --channel-2: #C084F0;  --channel-3: #E8A5C8;
  --channel-4: #E0B04A;  --channel-5: #5FBFA8;  --channel-6: #9AA3AD;
  --group-1: var(--channel-2);  /* violet */
  --group-2: var(--channel-1);  /* blue   */
  --group-3: var(--channel-4);  /* amber  */
  --group-4: var(--channel-5);  /* teal   */
  /* session/connection dot. WAS --up's green, and PORT §10.6 names that exact
     defect: "a live-feed dot must not use the P&L green" — under invariant 2
     --bull means market direction and nothing else, so a connected socket
     wearing it is the UI claiming the market went up. --success is the token
     that says "this worked", which is what a live feed is. */
  --live: var(--success);

  /* ── ink: the ink base at descending alpha ─────────────────────────
     Written against --ink-c so the SAME ladder serves both grounds. The ALPHAS
     are not shared — they are re-solved per theme to reproduce that ground's
     measured ratios; see the light block and §37.

     ══ PORT §5.1 — THE ALPHAS ARE RE-SOLVED, THE ARCHITECTURE IS NOT ══
     PORT §5.1 ships the ramp as four flat hexes (#FFFFFF · #F3F4F6 · #A1A1AA ·
     #6B7280). Those are DARK-GROUND values and this system has two grounds, so
     adopting them literally would strand the light theme on white ink — the
     exact fork §37 exists to prevent. The alpha ladder against --ink-c is kept
     and each alpha is SOLVED so that, composited on --panel, it lands on the
     ported hex:
       ink1  a 1.00 -> #FFFFFF  (--text-primary)    18.91:1
       ink2  a  .95 -> #F4F4F4  (--text-secondary)  17.05:1   was .72
       ink3  a  .61 -> #A3A2A4  (--text-tertiary)    7.63:1   was .52
     ONE TIER DELIBERATELY NOT ADOPTED: PORT §5.1's --text-disabled #6B7280 is
     alpha .405 and measures 3.89:1 — PORT §5.5 itself marks it "fails body
     (>= 3:1 only)" and rules it "for disabled controls ONLY". --faint is a
     READABLE ink in this system (metadata, timestamps, hints) and the contrast
     gate holds it to 4.5:1 on every surface, so mapping it to the ported
     disabled grey would fail 22 labels — the identical defect recorded below.
     It is re-solved as its own tier instead, and the ported disabled value is
     declared separately as --ink-disabled for controls that are actually
     disabled. This is PORT §5.5's own rule 1, applied rather than quoted. */
  --ink1: rgba(var(--ink-c),1);
  --ink2: rgba(var(--ink-c),.95);
  --ink3: rgba(var(--ink-c),.61);
  /* .36 gave 3.29:1 — clears the 3:1 NON-TEXT bar and fails the 4.5:1 text
     bar. It was defined as an ink and set to an icon level, so every label
     using it failed: 22 distinct failures from one value. .46 was the floor
     that cleared 4.5:1 on canvas, panel AND elev.

     §24b RE-SOLVED IT: that floor is a function of the LIGHTEST surface text
     lands on, and §24b raised --elev from #161313 to #2B2828. .46 measured
     4.30:1 there — the gate caught it on the first run. The ink ladder is
     downstream of the surface ladder; moving one without the other is how a
     lighter theme quietly loses AA. .48 restores it at 4.62:1 on --elev, and
     only improves the darker surfaces below it.

     RE-SOLVED AGAIN FOR THE PORT, for exactly the reason above: --elev moved
     from #2B2828 to #2A2631, so the floor moved with it. .58 measures 5.97:1
     there and 6.80:1 on --panel — above the 4.5 bar on every surface with the
     ~10% margin the record spends elsewhere, and it sits between the ported
     tertiary (.61) and disabled (.405) tiers, which is exactly the role it
     has. */
  --faint: rgba(var(--ink-c),.58);
  /* .40, re-solved on the new --elev: 3.63:1 there and 3.82:1 on --panel.
     Both halves of its contract still hold — above the 3:1 NON-text bar on
     every surface, and BELOW the 4.5 text bar on --panel, which is what stops
     it quietly becoming a second --faint. */
  --faint-nt: rgba(var(--ink-c),.40);   /* genuine NON-text only */
  /* PORT §5.1's --text-disabled. A CONTROL state, never a readable ink — see
     the ink block above for why it is not --faint. */
  --ink-disabled: rgba(var(--ink-c),.405);
  --hair: rgba(var(--lift-c),.07);
  --hair2: rgba(var(--lift-c),.13);
  /* was rgba(255,158,122,.22) — a raw coral literal. Achromatic now, per
     invariant 2; the accent no longer tints structure. */
  --hair-accent: rgba(var(--lift-c),.22);

  /* ══ NEUTRAL LIFT LADDER — §37 ═══════════════════════════════════
     Named by JOB, not by value.
       lift-1 panel/phead wash          lift-5 control active · neutral badge
       lift-2 table row hover           lift-6 selected / on
       lift-3 strike spine · hover      lift-7 strong hover
       lift-4 control hover
       rule-1 faint divider  rule-2 dashed  rule-3 dotted  rule-4 dragged */
  --lift-1: rgba(var(--lift-c),.022);
  /* .032, NOT --lift-2's .035, and the difference is load-bearing: the ITM
     wash sits UNDER .chip.bid, and snapping it to .035 dropped that chip's
     rose label from 4.51:1 to 4.40:1. Where a measured contrast depends on the
     exact value, the exact value gets its own token. */
  --lift-itm: rgba(var(--lift-c),.032);
  --lift-2: rgba(var(--lift-c),.035);
  --lift-3: rgba(var(--lift-c),.05);
  --lift-4: rgba(var(--lift-c),.07);
  --lift-5: rgba(var(--lift-c),.10);
  --lift-6: rgba(var(--lift-c),.13);
  --lift-7: rgba(var(--lift-c),.18);
  --rule-1: rgba(var(--lift-c),.045);
  --rule-2: rgba(var(--lift-c),.22);
  --rule-3: rgba(var(--lift-c),.30);
  --rule-4: rgba(var(--lift-c),.55);

  /* ── THE SEAM — a SEPARATOR, which is not an EDGE ──────────────── §24c
     §10 draws the distinction and the code never honoured it: "a hairline
     divides content INSIDE a panel and must stay under the numbers; the
     EDGE divides one WIDGET from the next and has to be seen across a
     six-panel workspace." Measured on the shipped sheet, 205 of 257 1px
     borders resolve through `--border`, and `--border: var(--edge)` — so
     every row separator, every section underline and every header rule was
     drawn at WIDGET-EDGE strength, rgba(255,255,255,.16). One token doing
     two jobs, and the louder job won 205 times.

     That is what reads as "borderlines everywhere": not one line too many,
     one line too STRONG, repeated. Splitting the role is the fix, and it is
     a smaller change than re-tuning 205 sites.

     TRANSPARENT on dark, because §24b's ladder now separates by lightness
     and a separator on top of a working elevation step is the belt beside
     the braces. NOT transparent on paper: light's ladder is already spent
     on the 1.30:1 card step (§37), and a table on paper without cell rules
     loses its rows — so the light ground keeps a real, quiet hairline.
     Genuine affordances (inputs, focus, selected, table cells at density)
     do NOT point here; they keep --field-border / --edge / --hair. */
  --seam: transparent;

  /* ── ELEVATION AS A SHADOW STACK, RING INCLUDED ────────────────── §24d
     A 1px ring belongs IN the shadow, not in the box model: `0 0 0 1px`
     costs no layout, cannot shift a grid, composites with the layers above
     it, and disappears cleanly when the surface is flush. Every floating
     surface takes one of these two and nothing else. Three layers because
     one shadow is a sticker and real light casts a contact, a body and an
     ambient — the alphas climb as the blur widens. */
  --ring-1: 0 0 0 1px rgba(var(--lift-c),.045);
  --ring-2: 0 0 0 1px rgba(var(--lift-c),.07);
  --elev-float:
    var(--ring-1),
    0 1px 2px -1px rgba(var(--drop-c),.7),
    0 8px 20px -8px rgba(var(--drop-c),.75);
  --elev-over:
    var(--ring-2),
    0 2px 4px -2px rgba(var(--drop-c),.8),
    0 12px 28px -10px rgba(var(--drop-c),.85),
    0 28px 56px -24px rgba(var(--drop-c),.75);

  /* ── elevation ─────────────────────────────────────────────────────
     On DARK, depth is a lightness step (--panel → --panel2 → --elev) and the
     shadows are supporting. On PAPER lightness only carries 1.08:1, so the
     light block's shadows do the real work — see the note there. */
  --shadow-card: 0 1px 6px rgba(var(--drop-c),.4);
  --shadow-pop: 0 24px 60px -24px rgba(var(--drop-c),1), 0 2px 10px -4px rgba(var(--drop-c),1);
  /* The accent bloom. Now a WHITE glow, because the accent is achromatic —
     PORT §10.1 gives .btn-primary exactly this treatment, an outer
     `0 4px 16px -2px rgba(255,255,255,0.12)` on top of a white-alpha fill, and
     calls it "loud on purpose". The alpha drops .4 -> .14: coral at .4 read as
     a warm halo, white at .4 reads as a blown highlight on a near-black
     ground. */
  --shadow-accent: 0 4px 18px -4px rgba(var(--accent-base-rgb),.14);
  /* Tags that sit ON the plot get a TIGHT shadow, not the 60px popover one.
     Only the ink flips: on paper .7 black under a 10px spread is a bruise. */
  /* ── PORT §9.3's OVERLAY LADDER ───────────────────────────────────────
     NEW. §24d gave this system two governed shadows (--elev-float,
     --elev-over) and they are unchanged; what it had no name for was the
     step ABOVE a popover. PORT §9.3 ships three, each paired with the
     matching --edge-top-N catch-light — "shadow alone reads as a drop
     shadow; shadow + catch-light reads as glass". P4's G3/G4 tiers read
     these, and referenced them before they were declared: `lint:tokens`
     caught it, which is the whole reason that gate exists. */
  --shadow-panel:   0 16px 40px -8px rgba(var(--drop-c),.45);
  --shadow-overlay: 0 8px 24px -6px rgba(var(--drop-c),.60), 0 2px 6px -2px rgba(var(--drop-c),.48);
  --shadow-modal:   0 32px 80px -16px rgba(var(--drop-c),.70), 0 4px 12px -4px rgba(var(--drop-c),.50);
  --shadow-tag: 0 2px 10px -3px rgba(var(--drop-c),.7);
  --shadow-tag-drag: 0 4px 14px -3px rgba(var(--drop-c),1);
  /* Bottom-dock elevation (PaperDock) — a shadow cast UPWARD. */
  --shadow-panel-up: 0 -4px 18px rgba(var(--drop-c),.35);

  /* Scrim behind a modal-ish overlay: BLACK in both grounds. Dimming is
     dimming; a white scrim on a light page does not read as "behind".
     .55 -> .70, PORT §6.2's `--surface-scrim` rgba(4,4,6,0.70). The ported
     modal tier is a 40px-blur glass surface, and a lighter scrim under it left
     the workspace legible THROUGH the dialog. --overlay-blur below is already
     the 4px the ported scrim pairs with. */
  --scrim: rgba(var(--drop-c),.70);
  /* The chip that sits ON the plot (order-line ×). This one does NOT follow
     --drop-c: it carries --ink-on-scrim, so it has to contrast with the INK,
     not with the page. */
  --chip-on-plot: rgba(0,0,0,.5);

  /* ── canvas grid ────────────────────────────────────────── §3 */
  --dot: rgba(255,255,255,.22);   /* WHITE, never accent-tinted */
  --dot-pitch: 52px;
  /* pointer-reveal grid — the disc that opens under the cursor on an empty
     workspace. Brighter than --dot because it is the layer that says "this is
     a placement surface"; it is masked, so it is never all on at once. */
  --dot-lit: rgba(255,255,255,.62);
  --canvas-lit: #060607;

  /* ── the chart's own reads ─────────────────────────────────────────
     [ThinqProfit] Not upstream: their chart.tsx hands lightweight-charts a JS
     literal built from --grid-c. Ours resolves these two off :root in
     `themes.ts` and pushes them to the WebGL controller, so the canvas and the
     chrome cannot drift — and so a gridline colour is a TOKEN rather than a
     constant in a helper. §11's rule applies: no fallback. A `var()` naming
     nothing renders nothing and looks merely "a bit off" — which is exactly
     how --hair drew every gridline cool white for months. */
  --chart-grid: rgba(var(--grid-c),.045);
  --chart-crosshair: rgba(var(--grid-c),.38);

  /* ── type ───────────────────────────────────────────────── §5
     IBM Plex, self-hosted. `Variable` is the family name the variable cut
     registers under — NOT an adjective; drop the word and this silently falls
     through to system-ui with no error, which is how upstream's own --sans has
     been rendering on every one of its 44 spec screenshots.

     ══ PORT §7.1 — INTER, SELF-HOSTED ═══════════════════════════════════
     The ported UI face. Still self-hosted (@fontsource-variable/inter, imported
     in main.tsx before index.css) — PORT §7.1 says the prototype pulls Inter
     from fonts.googleapis.com via a <link> while its own tokens.css comment
     claims "no external DNS on the critical path", calls those two facts a
     contradiction, and directs the production port to self-host. That is what
     this is; the Google <link> is never introduced.

     THE MONO FACE IS DELIBERATELY NOT PORTED. PORT §7.1 sets --font-mono to
     "Inter" — not a monospace face at all. That works in the prototype because
     it never renders code: its numeric alignment comes from `tnum` tabular
     figures, which is the ported system's actual answer to column alignment
     (PORT §8.1) and which this system now adopts. But ScriptEditor is a real
     code editor and ThinqScript is indentation-significant; giving it a
     proportional face would be a functional regression dressed as a design
     change. IBM Plex Mono stays, for code and nothing else. */
  --sans: 'Inter Variable', 'IBM Plex Sans Variable', system-ui, sans-serif;
  --mono: 'Inter Variable', 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  /* PORT §3 — tabular figures plus Inter's disambiguated 1 / l / I. Applied on
     <html> by the type layer so every digit in the product is 0.6309em wide.
     PORT §1.1 invariant 4: without this, Inter's `1` advances 0.4067em and its
     `0` advances 0.6309em — a 55% difference, which is the horizontal jitter
     traders call "the price dancing". */
  --font-features: "tnum" 1, "cv05" 1, "cv08" 1;
  /* SEVEN ROLES. §45: the terminal reads small because the scale is BYPASSED,
     not because the sizes are wrong — six of upstream's elements inherited
     14px from an unruled <body>. The seventh role (--t-micro) is not a
     rounding of --t-meta: the build used 8/9/9.5px in 47 places (side tags,
     DTE badges, the ▲▼ glyph, column eyebrows) and forcing those onto --t-meta
     is a 31% jump on type whose whole job is to sit beside a number without
     competing.

     ══ PORT §7.2 — FIVE OF SEVEN ROLES ADOPTED, TWO HELD ════════════════
     PORT §7.2 ships SIX tiers (28 / 18 / 14 / 13 / 12 / 11); §45 measured SEVEN
     here. The overlap is adopted verbatim and the divergence is deliberate:

       --t-display  19 -> 28  PORT display. "One big number per screen" — a
                              single 28px value is what makes the P&L read as
                              the answer to "how am I doing"; a second one makes
                              both of them furniture.
       --t-heading  15 -> 18  PORT heading
       --t-data   12.5 -> 14  PORT title — "panel titles, strike price, LTP",
                              which is exactly the role §45 gave --t-data
       --t-body     13 = 13   already the ported value
       --t-label    12 = 12   already the ported value
       --t-meta   10.5 -> 11  PORT micro — "axis labels, timestamps, hints"

     --t-micro (9.5px) IS HELD, and this is the one place the ported scale is
     not followed. It has no tier below 11px, so adopting it means raising 47
     measured sites (side tags, DTE badges, the direction glyph, column
     eyebrows) by 16%. Those sit in FIXED-HEIGHT boxes — --row-h-eyebrow is
     24px — so it is a layout change, not a type change, and P1's contract is
     that the existing UI re-renders on the new scale with NO component changes.
     Raising it is a P3 decision with the control layer, taken against a
     rendered page rather than inside a token block.

     WEIGHTS: --t-display-w drops 700 -> 600. PORT §7.2 puts display, heading,
     title and label all at 600 and keeps 700 out of the system entirely; the
     other six weights already match. */
  --t-display: 28px;   --t-display-w: 600;
  --t-heading: 18px;   --t-heading-w: 600;
  --t-body: 13px;      --t-body-w: 400;
  --t-label: 12px;     --t-label-w: 600;
  --t-data: 14px;      --t-data-w: 500;
  --t-meta: 11px;      --t-meta-w: 500;
  --t-micro: 9.5px;    --t-micro-w: 600;

  /* ── tracking + leading ─────────────────────────── PORT §7.2
     NEW. The scale had sizes and weights but no letter-spacing or line-height
     tokens, so every tier inherited whatever the cascade gave it. PORT §3
     records why that cannot stand for this face: Plus Jakarta Sans (and Inter
     to a lesser degree) has an unusually tall default line box, and "line-height
     must be set explicitly on every tier or rows inherit ~1.26em and the density
     scale silently stops holding". */
  --track-tight: -0.015em;  --track-normal: 0em;  --track-caps: 0.025em;
  --leading-tight: 1.15;    --leading-snug: 1.30;  --leading-normal: 1.45;

  /* ── geometry ──────────────────────────────────────────────────────
     MINIMUM WIDGET SIZES — a developer contract, not a style. Each is the
     width at which that widget's smallest tier still fits WITHOUT TRUNCATING A
     NUMBER; below it there is nothing left to remove and the widget stops
     being able to tell the truth. §51 makes these decide whether a DROP is
     refused, so they are an arbiter and not a hint.
       chain 400 = 4 chips at (54+18) + a 90px strike spine + padding
       ticket 360 = label column + the 176px control column + padding      */
  --w-min-chain: 400px;  --w-min-positions: 320px;
  --w-min-chart: 340px;  --w-min-oi: 280px;  --w-min-ticket: 360px;
  --h-min-widget: 180px;

  /* ── RADIUS ─────────────────────────────────── §42 · PORT §9.2
     The three-steps-plus-a-pill ramp is UNCHANGED. PORT §9.2 ships 3/5/8 where
     this system has 4/8/12, and the ported values are not adopted: §42's
     argument is that the corner scale is measured against a reference frame by
     frame, and swapping it would move 16 distinct radii across 11.8k lines to
     buy nothing the eye can resolve at these sizes.

     WHAT IS ADOPTED IS THE ROLE SPLIT, and it is the part that carries the
     look. PORT §9.2: "Radius is split BY ROLE — this is what stops everything
     looking like the same rounded blob." A capsule chip beside a 6px field
     beside a 10px card reads as three different KINDS of thing; one radius on
     all three reads as a theme. The roles point at the existing ramp, so this
     adds vocabulary without moving a pixel until a rule opts in (P3). */
  --r-sm: 4px;  --r-md: 8px;  --r-lg: 12px;  --r-pill: 999px;
  --radius-control: var(--r-pill); /* chip, tab, segmented, pill button */
  --radius-field:   var(--r-md);   /* input, select, stepper, menu item */
  --radius-surface: var(--r-lg);   /* panel, card, popover, toast       */
  --radius-modal:   14px;          /* dialog, drawer — PORT §9.2        */
  --radius-dot:     50%;           /* status dots, avatars              */

  --row-h: 32px;  --bar-h: 48px;  --phead-h: 34px;
  /* The price axis, in CSS, so overlays can stop at the plot boundary.
     `panels.css` reads it for `.orderline` and `.posline` and its header claimed
     "chart.tsx publishes it at runtime". Nothing published it — grep for
     `axis-w` outside panels.css returns nothing — so both rules had been taking
     the `var(--axis-w, 56px)` fallback since the sheet was vendored, while the
     engine's PRICE_AXIS_WIDTH is 72. Every order line and position line was
     drawn 16px too wide, ending underneath the axis price tags rather than at
     the plot edge. `tests/axis-width-contract.test.ts` pins the two together. */
  --axis-w: 72px;
  /* THE VERTICAL HALF OF THE SPACING SYSTEM. These are HEIGHTS, not paddings,
     and that is not an accident of authorship: a table row is a fixed box, so
     its vertical space cannot be written as cell padding — which is why
     --sp-cell-y does not exist (see the roles block). They hold the ordering
       data row > band > label row > eyebrow
       32        30     26          24    comfortable
       26        24     22          20    compact
     With --row-h at 26 and an expiry row still hard-coded at 30, the
     NAVIGATION row became taller than the data it navigates. Visible in a
     screenshot, invisible in the diff. */
  --band-h: 30px;
  /* The watchlist's A− / A+ control multiplies the type roles by this. Declared
     HERE so every read can be bare: `var(--panel-scale)` was the only form in
     the file, and a fallback is what a token with no declaration looks like. */
  --panel-scale: 1;
  --row-h-label: 26px;
  --row-h-eyebrow: 24px;
  --row-h-oi: 30px;

  /* ── motion ─────────────────────────────────────────────── §6
     Four duration bands and two easings. Live data gets NO transition —
     easing a price is a lie about when it changed.

     ══ PORT §9.4 — THE BANDS RE-SOLVED, THE FOUR-BAND SHAPE KEPT ════════
     PORT §9.4 ships four durations (60 / 120 / 400 / 180) against this
     system's four (50 / 120 / 150 / 260). --d-fast is already 120ms — the
     most-used duration in the app does not move. The other three are
     re-pointed at the ported bands, and one is new:
       --d-micro  50 ->  60ms  PORT instant: hover, colour change
       --d-menu  150 -> 120ms  PORT fast: press, overlay materialise
       --d-panel 260 -> 180ms  PORT panel: drawer, progress
       --d-flash        400ms  NEW — the tick-flash decay (PORT §8.5). There
                               was no band for it; the existing flash animation
                               hardcodes .6s, which P5 replaces along with the
                               hot-rate gate.
     --d-panel is the biggest move (260 -> 180) and it is deliberate: 260ms on a
     drawer is perceptible as lag on a terminal whose whole point is immediacy.

     EASINGS: --e-smooth IS PORT's --ease-out, byte-identical
     (cubic-bezier(.16,1,.3,1)) — the two systems arrived at the same curve
     independently. --e-snap is kept as-is and the two ported springs are added,
     because a menu settling and a flicked drawer are different motions and
     §6's two curves cannot express both. */
  --d-micro: 60ms;  --d-fast: 120ms;  --d-menu: 120ms;  --d-panel: 180ms;
  --d-flash: 400ms;
  --e-snap: cubic-bezier(.2,0,0,1);
  --e-smooth: cubic-bezier(.16,1,.3,1);
  --e-inout: cubic-bezier(.65,0,.35,1);          /* PORT: loops            */
  --spring-ui: cubic-bezier(.32,.72,0,1);        /* PORT: menus, sheets    */
  --spring-momentum: cubic-bezier(.16,1,.3,1);   /* PORT: flicked drawer   */
}

/* ══════════════════════════════════════════════════════════════════════════
   REDUCED MOTION — PORT §9.4
   "prefers-reduced-motion is fully wired and must stay wired. All four
   duration tokens go to 0ms." Doing it at the TOKEN is what makes it total:
   every transition and every animation in 11.8k lines reads one of these five,
   so none of them can forget. Individual @media blocks that also disable
   specific animations stay — this is the floor, not a replacement.
   ══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  :root {
    --d-micro: 0ms;  --d-fast: 0ms;  --d-menu: 0ms;  --d-panel: 0ms;
    --d-flash: 0ms;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE AMBIENT GLOW — PORT §6.1                                        [NEW]

   The signature of the ported system, and the thing that replaces coral as
   brand presence. ONE variable drives five gradient stops across the canvas;
   because the panels are translucent it bleeds through every surface, so the
   product is tinted without any single surface owning a hue.

   --glow-rgb is USER-SELECTABLE at runtime (P6 ships the picker and the
   persistence). It is written as an rgb TRIPLE, not a colour, because every
   consumer needs it at a different alpha — PORT §6.1's gradients use eight
   distinct alphas between .02 and .42 off this one value.

   P1 DECLARES IT AND WIRES THE SENTIMENT WASH TO IT. The gradients themselves
   are P2, because they are geometry on <body>, not values.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  --glow-rgb: 8, 45, 54;     /* deep teal #082d36 — PORT §6.1's default */

  /* ── GLOW STRENGTH, PER GROUND ──────────────────────────────────────────
     PORT §6.1 fixes eight alphas between .02 and .42 across the canvas
     gradients. Those are DARK-GROUND numbers: the default glow is a deep teal
     (Y .012), and a dark colour laid over near-black GLOWS while the same
     colour over Y .63 paper is a STAIN. Rather than fork the gradients per
     ground — two copies of one decision — every alpha is multiplied by this
     one number. Same shape, same stops, one knob.
     .28 on light is the same proportion §38 already spends there: the ambient
     wash drops --amb-core-a from .16 to .07 for exactly this reason. */
  --glow-k: 1;

  /* THE CANVAS ITSELF — PORT §6.1's three stacked gradients.
     The third layer is the ground's own vertical ramp and is the only part that
     is not glow; it is re-solved per ground below. Layers one and two are the
     glow and are shared, scaled by --glow-k. */
  --canvas-gradient: none;
  --canvas-ramp: linear-gradient(180deg, #121017 0%, #0E0C12 40%, #060508 100%);

  /* The top-of-page aura — PORT §6.1's `body::before`. Covers the top 45vh and
     is what makes the chrome sit IN a light rather than on a flat field. */
  --canvas-aura: none;

  /* ── GLASS TIERS — PORT §6.2 ──────────────────────────────────────────
     Pick by WHAT THE SURFACE DOES, not by how it should look. The fill/blur
     pairs and the inset catch-light ladder together ARE the elevation model:
     "shadow alone reads as a drop shadow; shadow + catch-light reads as
     glass." P2 applies these to real surfaces; P1 only declares them, so
     nothing renders differently from this block alone. */
  --glass-chrome:  blur(20px) saturate(170%);   /* G1 topbar, ticker, toolbars */
  --glass-panel:   blur(20px) saturate(170%);   /* G2 panels, cards, widgets   */
  --glass-overlay: blur(32px) saturate(190%);   /* G3 popovers, menus          */
  --glass-modal:   blur(40px) saturate(200%);   /* G4 modals, drawers          */
  --scrim-blur:    blur(4px);

  /* Written against --panel-rgb rather than as the ported literals, so ONE set
     of tiers serves both grounds: on dark --panel-rgb is 18,16,21 and these
     reproduce PORT §6.2's rgba(18,16,22,…) to within a byte; on paper they
     become a light translucent fill instead of a dark veil over white. */
  /* ══ THE FROST IS THE SURFACE ═════════════════════════════════════════
     These shipped at .22/.18 — measured on the running workspace, that left
     82% OF THE CANVAS READING THROUGH A PANEL. The brief asks for 30–40%, and
     the gap is why the desk looked flat rather than glassy: at .18 there is
     almost nothing there to catch light, so a "glass" panel was really a blur
     with no material on top of it. Apple's own materials are far more opaque
     than the word "translucent" suggests — the frost IS the surface, and what
     you see through it is a hint of ground, not the ground itself.

     Re-solved so the composited panel lands at ~35% canvas on BOTH grounds.
     They were failing in opposite directions: dark far too transparent (82%),
     light slightly too opaque (22%).

     A SECOND, QUIETER GAIN: the ink ladder was solved against --panel as a
     SOLID (#121015). At .18 the composite sat much nearer the canvas than that,
     so every measured contrast was being read off a surface the solve never
     saw. Raising the fill moves the real surface back toward the colour the
     ladder was built on. */
  --surface-g1: rgba(0, 0, 0, 0.25);   /* chrome — over the brightest glow */
  --surface-g2: rgba(0, 0, 0, 0.28);   /* panels — 28% translucent frosted glass */
  --surface-g3: rgba(16, 16, 16, 0.92);   /* menus, popovers — 92% dark glass */
  --surface-g4: rgba(14, 14, 14, 0.96);   /* modals — 96% dark glass          */
  /* PORT §10.3's topbar fill — the one chrome surface that is not a plain tier
     (.32 rather than G1's .22: the top bar carries the brand lockup and the
     market clock over the brightest part of the aura, and .22 let the gradient
     read straight through the wordmark). */
  --surface-chrome-fill: rgba(var(--panel-rgb),.62);
  /* PORT §1.1's frame hairline. The invariant is "no opaque coloured borders",
     NOT "no borders": the system ships 12 white-alpha hairlines at .03–.08,
     and 8 of them are the frame layer this token serves. */
  --hairline: rgba(var(--lift-c),.06);

  /* THE CATCH-LIGHT LADDER. A 1px inset white top edge — this is what PORT
     §1.1 invariant 1 means by "depth comes from surface value + blur + a 1px
     inset white catch-light", and it is why the system can ship with all three
     border tokens transparent. Paired with the matching --shadow-* below it. */
  --edge-top-1: inset 0 1px 0 0 rgba(var(--lift-c),.08);
  --edge-top-2: inset 0 1px 0 0 rgba(var(--lift-c),.12);
  --edge-top-3: inset 0 1px 0 0 rgba(var(--lift-c),.16);

  /* ── THE CHROMATIC SWEEP — PORT §6.3 ──────────────────────────────────
     A raking light across each panel. Two details from PORT §6.3 are the
     reason it reads as one material rather than a tiled texture, and both are
     easy to lose:
       · --sweep-reach is in PIXELS, not a percentage. A percentage gradient
         stretches its falloff to its own box, so the same light vanishes on a
         1100px chart panel and blazes on a short Positions panel — five panels
         lit by five different suns. A fixed reach gives every surface the same
         physical rake length from its own bottom-left corner.
       · --sweep-angle is PER PANEL. Five identical sweeps read as a tiled
         texture, which is this effect's failure mode; rotating the rake per
         panel makes each one a facet of the same stone. EVERY NEW WIDGET MUST
         SET ITS OWN ANGLE — P2 assigns them. */
  --sweep-angle: 38deg;
  --sweep-reach: 620px;
  --sweep: linear-gradient(var(--sweep-angle),
    rgba(255,255,255,.040)  0%,
    rgba(235,235,240,.025) 10%,
    rgba(245,240,235,.015) 20%,
    rgba(230,225,240,.008) 34%,
    rgba(255,255,255,0)    50%);
  --diamond-shine-border: linear-gradient(135deg,
    rgba(255,255,255,.22)  0%, rgba(255,255,255,.06) 25%,
    rgba(255,255,255,.02) 60%, rgba(255,255,255,.05) 100%);

  /* ── MICRO-INTERACTION STATES — PORT §2 ───────────────────────────────
     ALIASES, NOT NEW VALUES. PORT §2 specifies .05 / .10 / .14 and this
     system's lift ladder already sits at .05 / .10 / .13 — the two arrived at
     the same places independently. The one-hundredth of an alpha between
     lift-6 and the ported `selected` is below the threshold of vision, and
     re-tuning it would move a value §37 measured for no gain the eye can
     collect. The ported NAMES are what the control layer reads in P3. */
  --state-hover:    var(--lift-3);   /* .05 */
  --state-active:   var(--lift-5);   /* .10 */
  --state-selected: var(--lift-6);   /* .13 — PORT .14 */
}

/* ══════════════════════════════════════════════════════════════════════════
   LAYER SCALE — §41
   The low band is INSIDE a panel; the high band is above the page. The gap
   between 6 and 20 is deliberate — it is the boundary between "part of a
   widget" and "over the workspace", and leaving room there means a new
   in-panel layer never has to be squeezed against a scrim.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  --z-amb:       0;   /* the sentiment wash, behind everything (§38) */
  --z-grid:      1;   /* dot grid substrate */
  --z-grid-lit:  2;   /* the grid while a widget is being placed */
  --z-content:   3;   /* panels, bar, stage, empty state — the page itself */
  --z-plot:      4;   /* order + position lines: over the series, in the panel */
  --z-edge:      5;   /* the panel's own lit border (§10, §13) */
  --z-amb-over:  6;   /* the wash's over-layer — ABOVE content, which is why
                         its alpha ceiling had to be re-measured (§38) */

  /* ══ [ThinqProfit] THREE DOCKED TIERS, INSIDE THE DELIBERATE GAP ═══
     §41 leaves 6→20 empty on purpose — "a new in-panel layer never has to be
     squeezed against a scrim" — and this app has exactly the case it was left
     for. Upstream's product has no docked chrome at all; ours has three tiers
     that must stay strictly ordered, and they all sit UNDER --z-scrim so a
     modal still covers every one of them.

     Derived from the order that shipped, not chosen: watchlist 20 < picker
     panels 60 < tool dock 70 < popovers 200. Collapsing the middle two into
     --z-popover is what the first cut of this migration did, and a screenshot
     found it in one look — the first-run welcome card, a true popover, was
     rendering UNDERNEATH the watchlist and clipped at its edge. A layer scale
     that renumbers has to preserve the ORDER it renumbers, and only rendering
     it proves that. */
  /* The grid-layout splitter, in the gap this block exists to keep open. It was
     a bare `zIndex: 10` in App.tsx — a number with no name, which is why
     `lint:tokens` flagged it as a layer violation. 10 is not arbitrary and is
     kept: the drag handle must sit ABOVE the wash's over-layer (6) or the
     ambient gradient swallows a 6px-wide target, and BELOW the rail (14) so the
     watchlist still opens over the chart it is beside. */
  --z-splitter: 10;
  --z-rail:     14;   /* the watchlist column — a header dropdown opens OVER it */
  --z-panel:    15;   /* an anchored page panel: pickers, depth, object tree   */
  --z-drawer:   16;   /* the sliding tool dock / AI dock / studio panel        */

  /* ── above the page ── */
  --z-scrim:    20;
  --z-popover:  21;   /* anchored to a widget */
  --z-menu:     30;   /* context and dropdown menus */
  --z-tooltip:  40;   /* must clear a menu: tooltips fire on menu items */
  --z-modal:    60;
  --z-modal-2:  61;   /* [ThinqProfit] nested dialogs. Upstream's product has
                         one modal depth; ours confirms from inside a modal. */
  --z-modal-3:  62;
  --z-drag:     70;   /* a dragged widget outranks what it is dragged over */
  /* [ThinqProfit] DIVERGENCE FROM §41, DELIBERATE. Upstream puts --z-toast at
     50, i.e. UNDER its modal. Our toasts carry order state (submitted →
     filled), and PRODUCT.md's two-stage confirmation is the surface a trader
     reads to learn their order filled. A dialog open over it must not swallow
     that, so toast sits above every modal tier and below the spark. */
  --z-toast:    80;
  --z-spark:    90;   /* click spark. pointer-events:none, so it never
                         intercepts; it was 9999, which is not a layer, it is
                         an admission that there was no scale. */
}

/* ══════════════════════════════════════════════════════════════════════════
   SPACING — the §42 ramp is the VOCABULARY, the §53 roles are the API.
   Nothing outside this file may name a step directly.

   The ramp keeps 1px granularity BELOW 12px, where a terminal genuinely needs
   it, and coarsens above it where nothing does. THE CRITERION IS FREQUENCY,
   NOT ROUNDNESS: a value stays if the build uses it more than twice. Forcing
   everything onto a 4px grid would shift row heights that were matched against
   a reference frame by frame — trading a real measurement for a tidy one.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  --sp-0: 0;      --sp-1: 1px;    --sp-2: 2px;    --sp-3: 3px;   --sp-4: 4px;
  --sp-5: 5px;    --sp-6: 6px;    --sp-7: 7px;    --sp-8: 8px;   --sp-9: 9px;
  --sp-10: 10px;  --sp-11: 11px;  --sp-12: 12px;  --sp-14: 14px; --sp-16: 16px;
  --sp-20: 20px;  --sp-24: 24px;  --sp-32: 32px;  --sp-40: 40px; --sp-48: 48px;
  --sp-64: 64px;
}
/* SPACING ROLES — §53. A token called --sp-9 cannot carry a density mode:
   compact shifts what a CELL takes, and "9" is not a cell. Grouped by WHAT
   THEY SPACE rather than by what they measure.
     --sp-hair    1  a seam that must not read as a gap
     --sp-tight   2  inside ONE unit — two stacked lines of one record
     --sp-bond    5  a value and the word that belongs to it (▲ 0.42% · 2 lots)
     --sp-inline  7  two glyph runs that read as ONE thing; a small inline box
     --sp-band-y  8  the vertical padding of a band that is NOT height-driven
     --sp-dock    8  the gutter between docked widgets — a docked workspace is
                     not a document, so it is 8 and not the page's 20
     --sp-item    9  two independent items in one row
     --sp-cell-x  9  the horizontal padding of a dense table cell
     --sp-inset  11  a box holding one line of content
     --sp-group  12  two clusters of controls in one band
     --sp-pad    14  a decision surface's gutter
     --sp-block  20  two stacked blocks
     --sp-page   32 · --sp-section 40 · --sp-page-end 64
   TWO ROLES DELIBERATELY ABSENT: --sp-cell-y and --sp-ctl-y. Five of six table
   cell y-paddings in a terminal are literally 0 and every control sets
   `height`, so the vertical size of a row is carried by --row-h and of a
   control by its own height. That is why density owns --row-h below. */
:root {
  --sp-hair:      var(--sp-1);
  --sp-tight:     var(--sp-2);
  --sp-bond:      var(--sp-5);
  --sp-inline:    var(--sp-7);
  --sp-band-y:    var(--sp-8);
  --sp-dock:      var(--sp-8);
  --sp-item:      var(--sp-9);
  --sp-cell-x:    var(--sp-9);
  --sp-inset:     var(--sp-11);
  --sp-group:     var(--sp-12);
  --sp-pad:       var(--sp-14);
  --sp-block:     var(--sp-20);
  --sp-page:      var(--sp-32);
  --sp-section:   var(--sp-40);
  --sp-page-end:  var(--sp-64);
}

/* ══════════════════════════════════════════════════════════════════════════
   PORTFOLIO SENTIMENT — the ambient wash's HUE follows the P&L   §38
   --amb-core-a  .amb at --z-amb, BEHIND every widget; binding ink is --ink3.
   --amb-over-a  .amb-over at --z-amb-over, ON TOP of the widgets, so it passes
                 over live prices. Binding ink is --faint, and this alpha is a
                 CEILING measured per hue at 90% of the measured maximum, not a
                 taste call.
   ══════════════════════════════════════════════════════════════════════════ */
:root, [data-sentiment="flat"] { --amb-rgb: var(--glow-rgb); --amb-over-a: .060; }
[data-sentiment="positive"]    { --amb-rgb: var(--up-rgb);    --amb-over-a: .060; }
[data-sentiment="negative"]    { --amb-rgb: var(--down-rgb);  --amb-over-a: .035; }
:root { --amb-core-a: .16; }

/* ══════════════════════════════════════════════════════════════════════════
   LIGHT — a token swap, not a second design system                     §37

   DERIVATION RULE, applied to every chromatic token below: a light value keeps
   the dark value's HUE (same HSL metric §4/§20/§22 use, to within 0.5°) and
   its Lab CHROMA (clamped to 92% of the sRGB gamut maximum at the new
   lightness), and re-solves ONLY its LIGHTNESS against a measured contrast
   target. Hue is INHERITED, not re-picked, so every hue separation already
   measured holds without being re-argued.

   [ThinqProfit] The axis is `[data-theme="light"]` on <html>, matching
   design-suite verbatim so every rule ports without translation. It replaced
   `html.light`, which was ours.
   ══════════════════════════════════════════════════════════════════════════ */
[data-theme="light"] {
  color-scheme: light;

  /* Four attempts were rejected before this one. The first three chased
     contrast ratios and turned the brand brown; the fourth moved the ground to
     slate, fixed coral, and came out pastel. The diagnosis that held: ON PAPER
     RICHNESS IS NOT A PALETTE PROPERTY. --panel sits 1.08:1 above --canvas —
     lightness alone cannot separate two light surfaces, which is why the dark
     theme's elevation model produces a flat page here. On a light ground depth
     comes from SHADOW, and the earlier attempts had none. */
  /* ══ PORTED — THE STOCK TURNED COOL, BECAUSE THE BRAND LEFT IT ═════════
     §37's warm print stock was correct FOR A CORAL PRODUCT: "a neutral rule on
     warm paper reads as a printing fault", and the whole ground was tuned to
     sit under a warm accent. With the accent achromatic and the dark ground
     re-solved at hue 252, a warm paper would be the only warm surface left in
     the product — the light theme quietly becoming a second design system,
     which is the one thing §37 exists to prevent.

     THE DERIVATION RULE IS UNCHANGED AND IS WHAT MADE THIS CHEAP: every value
     below keeps its dark counterpart's HUE (within 0.5°) and re-solves only
     LIGHTNESS against a measured target. The Y TARGETS ARE §37's OWN, not new
     ones — stock .632, canvas .699, card .920 — so the argument that produced
     them ("a card at .92 over a canvas at .70 is a 1.30:1 step; the rejected
     luminous ground managed 1.08:1 and read flat") carries over untouched.
     Measured on the ported ladder: 1.295:1. Nothing about the light theme's
     ARCHITECTURE is re-argued; only its hue moved.

     THE INK ALPHAS DID NOT MOVE. .95/.83/.70/.64/.52 were solved against the
     darkest surface text can land on, and the cool stock sits at the same Y as
     the warm one did, so they re-measure clean — the tightest is --faint at
     4.73:1 on the stock (§37 measured 4.67 on the warm one). */
  --ink-c: 17,18,24;
  --lift-c: 0,0,0;
  --grid-c: 0,0,0;

  /* THE GLOW, DIMMED FOR PAPER — see --glow-k in the dark block. The default
     glow is a deep teal at Y .012: over near-black that reads as light, over
     Y .63 stock it reads as a stain. .28 is the proportion §38 already spends
     here (--amb-core-a .16 -> .07). The gradients themselves are shared. */
  --glow-k: .28;
  /* the ground's own vertical ramp, light. Same clean shape as dark. */
  --canvas-ramp: none;
  /* Chrome and panels need crisp contrast on paper. */
  --surface-g1: rgba(var(--panel-rgb),.98);
  --surface-g2: rgba(var(--panel-rgb),.98);
  --surface-g3: rgba(var(--panel-rgb),.98);
  --surface-g4: rgba(var(--elev-rgb),.98);
  --surface-chrome-fill: #FFFFFF;

  --bg: #F0F3F8;       --bg-rgb: 240,243,248;
  --canvas: #FFFFFF;   --canvas-lit: #FFFFFF;
  --canvas-gradient: none;
  --panel: #FFFFFF;    --panel-rgb: 255,255,255;
  --panel2: #FFFFFF;   --panel2-rgb: 255,255,255;
  --elev: #FFFFFF;     --elev-rgb: 255,255,255;

  --surface:        #FFFFFF;
  --surface-2:      #FFFFFF;
  --surface-0:      #FFFFFF;
  --surface-rgb:    255,255,255;
  --surface-2-rgb:  255,255,255;
  --surface-chrome: #FFFFFF;
  --surface-panel:  #FFFFFF;
  --surface-panel-solid: #FFFFFF;
  --surface-overlay: #FFFFFF;
  --surface-modal:   #FFFFFF;
  --edge-focus: rgba(28, 32, 44, 0.28);
  --hairline: rgba(28, 32, 44, 0.08);

  /* Ink α RE-SOLVED against the DARKEST surface any text can land on — the
     stock, not the card. Two surfaces two-thirds of a step apart cannot share
     one ink ladder: solving on the card left 179 nodes short, every one a
     label sitting on the workspace rather than inside a widget. The same
     --faint reads 4.57:1 on the card and 4.04:1 on the stock. */
  --ink1: rgba(var(--ink-c),.95);     /* 12.16:1 on stock · 14.9 on a card */
  --ink2: rgba(var(--ink-c),.83);     /*  7.62 · 9.9 */
  --ink3: rgba(var(--ink-c),.70);     /*  5.41 · 6.9 */
  --faint: rgba(var(--ink-c),.64);    /*  4.67 · 5.9 — the tightest */
  --faint-nt: rgba(var(--ink-c),.52); /*  3.55, non-text only */

  --hair: rgba(var(--lift-c),.065);
  --hair2: rgba(var(--lift-c),.14);
  /* achromatic on both grounds now — see the dark block. These were a warm
     rgba(162,100,77,…), i.e. brand in the structure, which invariant 2 ends. */
  --hair-w: rgba(var(--lift-c),.10);
  --hair-w2: rgba(var(--lift-c),.18);
  /* §24c — paper KEEPS its separator. The dark ground can drop the seam
     because §24b gave it a lightness ladder to separate with; paper already
     spends its ladder on the 1.30:1 card step, and a dense table on white
     with no cell rule loses which row a number belongs to.
     The WARMTH is what changed, not the reasoning: §24c's rule was "a neutral
     rule on warm paper reads as a printing fault", and the paper is no longer
     warm. On the cool stock the fault is the other way round, so the seam and
     every cast below it are re-solved to the ground's own cool slate
     (28,32,44) at the alphas §24c/§37 already measured. */
  --seam: rgba(28,32,44,.10);
  /* the ring inverts too — on paper a WHITE ring is invisible, so the
     elevation ring is a dark hairline and the casts are tinted, not black. */
  --ring-1: 0 0 0 1px rgba(28,32,44,.07);
  --ring-2: 0 0 0 1px rgba(28,32,44,.10);
  --elev-float:
    var(--ring-1),
    0 1px 2px rgba(28,32,44,.07),
    0 6px 16px -8px rgba(28,32,44,.13);
  --elev-over:
    var(--ring-2),
    0 2px 8px -3px rgba(28,32,44,.14),
    0 18px 42px -18px rgba(28,32,44,.26);
  --dot: rgba(var(--lift-c),.26);
  --dot-lit: rgba(var(--lift-c),.62);

  /* DEPTH IS SHADOW HERE — the one thing the flat attempts were missing.
     Two-part, as the craft floor requires: an offset with a soft blur, never a
     zero-offset halo. */
  --shadow-card:   0 1px 2px rgba(28,32,44,.07), 0 6px 16px -8px rgba(28,32,44,.13);
  --shadow-pop:    0 18px 42px -18px rgba(28,32,44,.26), 0 2px 8px -3px rgba(28,32,44,.14);
  --shadow-tag:    0 2px 8px -2px rgba(28,32,44,.20);
  --shadow-tag-drag: 0 4px 14px -3px rgba(28,32,44,.34);
  --shadow-panel-up: 0 -4px 18px rgba(28,32,44,.12);
  /* the accent BLOOM. On dark this is a white glow; on paper a white glow is
     invisible, so the bloom inverts to the accent's own dark neutral — the
     same inversion the elevation ring makes four lines up, for the same
     reason. The 1px ring carries it; the 18px cast is the halo. */
  --shadow-accent: 0 0 0 1px rgba(72,75,81,.55), 0 4px 18px -4px rgba(72,75,81,.30);

  /* THE BRAND CROSSES BY HUE, NOT BY HEX — the rule the first five attempts
     all broke. --up and --down are EMISSIVE colours, picked to glow at Y .55
     and .18 against a near-black ground; carried onto paper unchanged they
     have nowhere to glow. Darkening them just enough to pass AA converges the
     pair and kills the hue — that is the mud.
     The full PIGMENT redefinition is PARKED, not adopted: redefining --up at
     the source moves its hue to 157.0° against the canonical 163.5° and closes
     ΔL(up,down) to 0.087, which the conformance gate caught on the first run
     after the change. Adopting it means amending §4 and §22 deliberately, not
     as a side effect. So the FILLS stay canonical and only the INK tokens
     carry the darker values.
       pigment up   #2A7357  Y .1345  L* 43.4  5.26:1 as ink on the card
       pigment down #7A1030  Y .0471  L* 25.9  9.99:1     ΔL* 17.5

     ══ PORT — RE-SOLVED AT THE NEW HUES, AND THE PARK IS NO LONGER NEEDED ══
     The park existed because the pigment values drifted the hue 6.5° off
     canonical and closed ΔL(up,down) to 0.087. Both problems were artefacts of
     solving by eye. Every value below is now solved by HOLDING HSL hue AND
     saturation from its dark counterpart and moving ONLY lightness, which is
     §37's stated rule executed arithmetically — so the drift is 0.03–0.43°
     across the whole set, inside the 0.5° tolerance, and no separation §20 or
     §22 measured is disturbed.
       --up-ink   #0A6157  173.10° (dark 173.41°)  4.78:1 stock · 6.78 card
       --down-ink #AE0A26  349.76° (dark 349.72°)  4.77 · 6.76
       --up-lift  #09574F  173.85°                 5.50 · 7.80
       --down-lift#9C0922  349.80°                 5.53 · 7.83
     --up-dim / --down-dim take the ink values, as they did before: a dimmed
     reference readout on paper is already at the ink's strength. */
  --up-lift: #396e04;   --down-lift: #c70000;   /* same solve — both are TEXT roles on paper */
  --up-dim: #0A6157;    --down-dim: #AE0A26;
  /* RE-SOLVED for the new direction pair. These were #0A6157 / #AE0A26 — hue 173°
     and 349°, the OLD teal-and-rose — while `--up` / `--down` moved to hue 90° and
     0°. §37 requires the light twin to keep the dark HUE and re-solve only
     LIGHTNESS, so an 83° drift is the rule broken outright, and
     `tokens-contrast` failed on exactly that. Re-solved at the source hue against
     the stock: 5.54:1 (up) and 5.51:1 (down), 6.17 / 6.13 on a card. */
  --up-ink: #396e04;    --down-ink: #c70000;
  /* THE FILLS ARE NOT REDECLARED HERE, and that is load-bearing rather than an
     omission. Solving light --up and --down to the same 3:1 target against the
     stock gives them the SAME luminance — measured, both land at 3.02:1 — which
     collapses ΔL(up,down) to ~0 and fails §22's 0.05 collision line outright.
     §22 is what stops a red-green-deficient trader losing the pair entirely, so
     the fills stay canonical and only the INK tokens carry paper-readable
     values. This is the same conclusion §37 reached when it PARKED the pigment
     redefinition, arrived at from the opposite direction.
     The fills are canonical, so their ink is DARK exactly as on the dark
     ground. Light ink belongs with a pigment slab and nothing else. */
  --ink-on-up: #08120D;  --ink-on-down: #1A0509;

  /* ── the ACHROMATIC accent, solved for paper ──────────────────────────
     The dark accent is a BRIGHT silver (#F3F4F6) carrying dark ink. That
     inverts here for the same reason the elevation ring does: a near-white
     fill on near-white paper is not a fill. So the light accent is a dark
     neutral carrying near-white ink.
     THE HUE STILL HAS TO HOLD. An achromatic colour is not hue-LESS — #F3F4F6
     resolves to 220.0° in HSL, and §37's fidelity check reads --coral on both
     grounds. Every value here is built on the same r : r+d/3 : r+d ratio, so
     all three measure 220.00° exactly rather than approximately. */
  /* Channel spread 6, not 9: `audit:conformance` measures "achromatic" as a
     channel spread (the same measure the panel edge uses, same bound of 8), and
     the first solve sat at exactly 9 — a neutral by eye, a chromatic by the
     gate. Hue still resolves to 220.00 deg, so §37's cross-ground fidelity
     check is unaffected. 5.79:1 on the stock, 8.20:1 on the card. */
  --accent-base: #484A4E;  --accent-base-rgb: 72,74,78; /* the FILL */
  --accent-hi: #3A3D43;    /* hover is DARKER on paper — still lightness only */
  --coral-ring: #606369;  --coral-ring-rgb: 96,99,105; /* 3.93:1 on the stock — the fill's edge */
  --coral-lt: #3A3D43;    /* 7.10:1 on the STOCK (10.06 on a card) — solved
                             where inline code and links actually sit, which is
                             the page ground, not inside a widget */
  --ink-on-accent: #F7F8FA;                            /* on the #484B51 fill */
  /* The silver wordmark solved for paper — the METAL_LIGHT *white* ramp's mid
     stop, neutral like its dark counterpart. ~5:1 on the stock. */
  --silver: #6B6B70;  --silver-rgb: 107,107,112;
  --silver-grad: linear-gradient(104deg,#B0B0B4,#313131 26%,#414144 46%,#929296 66%,#333334 88%,#6B6B70);  /* lint-tokens-ok: METAL_LIGHT white ramp */
  --brand-q: var(--silver);
  /* --live follows --success through the :root declaration; the light
     --success below is what it resolves to. */
  --success: #219675;  --success-rgb: 33,150,117;
  --danger:  #C64F53;  --danger-rgb: 198,79,83;

  /* ── THE CANDLE CHANNEL, RE-SOLVED FOR PAPER ── PORT §5.3, §14.4
     The dark pair carried onto light UNCHANGED measures 2.69:1 for
     --bull-chart on the light canvas — under the 3:1 a candle needs as a
     graphical object. So it is re-solved, and the solve is constrained on TWO
     axes rather than one:
       · each clears 3:1 against --canvas (4.54 and 7.14 measured);
       · ΔY between them is 0.0599, against the dark pair's 0.0612 and §22's
         0.05 collision line.
     The second constraint is the one that is easy to miss and was caught here
     by measuring: solving both to the SAME contrast target lands them on the
     same luminance, and a bull/bear pair separated by hue alone is exactly what
     a red-green deficiency cannot read. §22 asserts this for --up/--down; the
     candle pair is the same claim on the surface where it matters most.
     Hue is inherited to within 0.25°, per §37. */
  --bull-chart: #046D4C;  --bull-chart-rgb: 4,109,76;
  --bear-chart: #841515;  --bear-chart-rgb: 132,21,21;
  /* every coloured ink solved to 4.5:1 by darkening along its OWN hue — the
     minimum each needs, not a blanket push. Attempt one pushed them all past
     that and turned the palette to mud.
     RE-SOLVED for the port: dark --warn moved 35.26° -> 39.88°, so the light
     value has to move with it or §37's fidelity check fails. 39.84° here. */
  --warn: #C87844;  --warn-rgb: 200,120,68;
  /* [ThinqProfit] --warn-ink is NOT --warn here, and upstream's is.
     Measured: --warn #9A6B28 is solved to 4.55:1 on --elev and reads 4.30:1 on
     --panel — which is the surface a warning label actually lands on, --elev
     being a popover. That is the identical defect --info-ink was given its own
     value for eleven lines below, and §37's ink ladder was re-solved for on the
     pass before that; the third instance was simply never checked, because
     upstream never declared a light --warn-ink at all.
     Re-solved on --panel along its OWN hue: 35.33° against 35.26°, inside
     §37's 0.5° tolerance, so every separation §20 measured carries over.
     4.735:1 rather than 4.51 — the same ~5% margin §38 spends on its ceilings
     and the scrim spends on its floor, because a value correct to two decimal
     places is one subpixel from being wrong.
     RE-SOLVED at the ported hue: 39.57° against --warn's 39.84°. */
  --warn-ink: #A65D2C;
  /* --info moved 208.3° -> 209.76° in the port; both values follow it. */
  --info: #5966D6;  --info-rgb: 89,102,214;
  /* the status ink, one step darker along the SAME hue — 209.80° against
     --info's 209.70°, inside §37's 0.5° tolerance. Solved on --panel with the
     row's lift under it: 6.81:1 bare, where --info itself manages 4.26:1. */
  --info-ink: #4652BF;
  /* P&L section color pair (both Light & Dark theme follow the exact same P&L pair) */
  --pnl-up: #67D574;    --pnl-up-rgb: 103,213,116;
  --pnl-down: #FF6B90;  --pnl-down-rgb: 255,107,144;
  --pnl-up-bed:   rgba(var(--pnl-up-rgb),.12);
  --pnl-down-bed: rgba(var(--pnl-down-rgb),.12);
  /* Re-solved on the CARD (Y .920), where a chart legend actually sits. Solved
     on --elev they all read 4.34:1 — a popover is not where an indicator value
     ever appears.
     ══ [ThinqProfit] RE-DERIVED AT THE CANONICAL HUE ═══════════════════
     §37's derivation rule is that a light value keeps the dark value's HUE to
     within 0.5° and re-solves only its LIGHTNESS. Measured against upstream's
     own light values, ALL FOUR broke it:
       ind-1 255.73 → 256.92  (+1.19°)     ind-2 281.05 → 285.22  (+4.16°)
       ind-3 305.65 → 311.38  (+5.73°)     ind-4 222.00 → 220.00  (−2.00°)
     Not cosmetic. §20 reserves ≥30° from every semantic hue, and ind-3's 5.73°
     of drift is precisely what put it 28.98° from --down — a live violation of
     the rule it was placed by. Upstream's own comment here claims the opposite
     ("against the carmine pigment it is 31.6° apart"), and that is true only of
     the PIGMENT redefinition, which the block above PARKS. A fix credited to a
     change that was never adopted.
     Re-derived at each dark hue with S and L untouched, so nothing about their
     legibility is re-argued — all four still clear 4.5:1 on --panel, and the
     hue-fidelity assertion in `tests/tokens-contrast.test.ts` is what stops the
     rule going unenforced a second time. */
  --ind-1: #7460AE;  --ind-1-rgb: 116,96,174;  /* 255.38° · 4.81:1 · 33.4° clear */
  --ind-2: #86579C;  --ind-2-rgb: 134,87,156;  /* 280.87° · 5.03    · 58.9°       */
  --ind-3: #92588D;  --ind-3-rgb: 146,88,141;  /* 305.17° · 4.84    · 35.2°       */
  --ind-4: #656B7A;  --ind-4-rgb: 101,107,122; /* 222.86° · 4.92 — near-neutral,
                                                  the hue rule does not apply    */

  --edge: rgba(var(--lift-c),.13);
  --edge-focus: rgba(255,158,122,.85);

  /* SCRIMS DO NOT FLIP — see the dark block. The ink stays near-white on both
     grounds, AND THE ALPHA HAD TO BE RE-SOLVED for the same reason §38's
     ceiling did: .5 of black is a different colour depending on what is under
     it. Over the tag's own --elev it composites to a mid-grey and put the
     order-line × at 3.79:1 wearing near-white ink.
       α .50 3.79 ✗   .55 4.50 (the bar itself)   .62 5.80 ✓   .70 7.86
     .62 rather than .55, because a value correct to two decimal places is one
     subpixel from being wrong; .62 is .55/0.9, the same 10% margin §38 spends.
     Invisible until a proof sheet rendered the × — audit:contrast walks the
     RESTING page, the same blind spot as a closed menu. */
  --ink-on-scrim: rgba(255,255,255,.94);
  --chip-on-plot: rgba(0,0,0,.62);

  /* [ThinqProfit] Both re-solved on paper. .045 of BLACK on a Y .70 canvas is
     a gridline you cannot find — the dark ground gets .045 of WHITE against a
     Y .006 canvas, which is a 7x relative step. These are the values the light
     canvas already shipped with, kept because they were measured against real
     candles rather than derived from the dark alpha. */
  --chart-grid: rgba(var(--grid-c),.07);
  --chart-crosshair: rgba(var(--grid-c),.55);   /* .38 of black is faint on paper */
}

/* the sentiment wash, re-measured on the warm ground. The three HUES are the
   same on both (§37: the fills do not change); only the ceilings differ,
   because --faint starts at 4.88:1 here against 4.66:1 on dark and so has less
   headroom to spend. The ORDER differs too — on dark the down-rose is tightest
   because it is the brightest of the three against a near-black panel; here it
   is the loosest, because on paper a darker wash LIFTS the ink's own contrast
   instead of eating it. */
[data-theme="light"] { --amb-core-a: .07; }
[data-theme="light"], [data-theme="light"] [data-sentiment="flat"] { --amb-over-a: .028; }
[data-theme="light"] [data-sentiment="positive"] { --amb-over-a: .028; }
[data-theme="light"] [data-sentiment="negative"] { --amb-over-a: .038; }

/* ══════════════════════════════════════════════════════════════════════════
   DENSITY — §53. `comfortable` is :root the same way `dark` is; `compact` is
   an ATTRIBUTE on <html>, not a media query — a media query would make density
   a property of the WINDOW, and this is a property of the TRADER. The same
   terminal is compact on a 13" laptop in a taxi and comfortable on a 32" desk.

   EVERY COMPACT VALUE IS A STEP ON THE §42 RAMP — compact is not a freehand
   shrink, it is a re-pointing of each role at a smaller MEASURED step.

   TYPE DOES NOT MOVE. A trader learns the size of a number and changing it
   under them is disorienting; the container queries shed COLUMNS rather than
   resize type (§8), so there is no size at which the type has to give.

   The payoff, measured on an 800px-tall chain panel: chrome 142.5 → 119.5, row
   32 → 26, ROWS THAT FIT 20 → 26. Six more strikes, 30% more of the chain,
   with every number the same size.
   ══════════════════════════════════════════════════════════════════════════ */
html[data-density="compact"] {
  --sp-hair:      var(--sp-1);   /* a seam cannot be thinner than a pixel */
  --sp-tight:     var(--sp-1);
  --sp-bond:      var(--sp-4);
  --sp-inline:    var(--sp-5);
  --sp-band-y:    var(--sp-5);
  --sp-dock:      var(--sp-4);
  --sp-item:      var(--sp-7);
  --sp-cell-x:    var(--sp-6);
  --sp-inset:     var(--sp-8);
  --sp-group:     var(--sp-9);
  --sp-pad:       var(--sp-10);
  --sp-block:     var(--sp-14);
  --sp-page:      var(--sp-24);
  --sp-section:   var(--sp-32);
  --sp-page-end:  var(--sp-48);

  /* THE VERTICAL HALF, and the whole payoff. The four below --row-h are not
     optional — they hold data > band > label > eyebrow. */
  --row-h:         26px;
  --phead-h:       28px;
  --band-h:        24px;
  --row-h-label:   22px;
  --row-h-eyebrow: 20px;
  --row-h-oi:      24px;

  /* [ThinqProfit] app chrome, which upstream's spec page has none of. --btn-h
     is NOT restated: it is var(--row-h) in the alias block, so it follows the
     row it sits in automatically — 32 → 26. The other three are their own
     decisions and stay explicit. */
  --btn-h-sm: 24px;  --btn-h-lg: 32px;
  --bar-h:    46px;

  /* THE MISSING 88%. Everything above retokenises the NAMED tokens (--sp-bond,
     --row-h, …), which have 136 call sites. The app's actual spacing vocabulary is
     the numbered alias scale --space-1..10 — 970 call sites — and it resolves to the
     raw pixel primitives (--space-3 → --sp-6 → 6px), which are constants by
     definition and so could never respond to density. Compact therefore changed
     control heights and almost nothing else, which is exactly how it was reported:
     "scope is control-heights-only".
     Restating the alias scale one step down the SAME primitive ladder fixes every one
     of those 970 sites at once — no per-surface `[data-density]` rules, and no new
     spacing values. ~75-80% of comfortable, and --space-1 holds at 2px because the
     step below it is the 1px hairline. */
  --space-1:  var(--sp-2);   --space-2:  var(--sp-3);   --space-3:  var(--sp-4);
  --space-4:  var(--sp-6);   --space-5:  var(--sp-8);   --space-6:  var(--sp-9);
  --space-7:  var(--sp-11);  --space-8:  var(--sp-12);  --space-10: var(--sp-16);
}

/* ══════════════════════════════════════════════════════════════════════════
   COMFORTABLE — the third tier                          §53 · PORT §9.1  [NEW]

   PORT §9.1 ships THREE density tiers (compact / normal / comfortable) where
   §53 shipped two. The third is added rather than the ported numbers adopted
   wholesale, and the distinction matters:

     · §53's two tiers are MEASURED — "the payoff, measured on an 800px-tall
       chain panel: chrome 142.5 -> 119.5, row 32 -> 26, ROWS THAT FIT 20 -> 26".
       Re-pointing default from 32 to PORT's 34 would move every one of those
       measurements to buy nothing; the ported `normal` and this system's
       default are the same tier under two names.
     · What was genuinely MISSING is the tier above. PORT §9.1 argues density is
       a property of the TRADER, not the window — §53 says the same thing in the
       same words — and a trader on a 32" desk had no setting that used the
       room. Every value here is a step on §42's ramp, exactly as compact is.

   TYPE STILL DOES NOT MOVE, for §53's reason: a trader learns the size of a
   number and changing it under them is disorienting.

   ⚠ THE SELECTOR IS `roomy`, NOT `comfortable`, AND THAT IS DELIBERATE.
   PORT §9.1 calls this tier `comfortable`, but THIS STORE ALREADY SPENDS THAT
   WORD ON ITS DEFAULT: `store.ts` types density as `'comfortable' | 'compact'`
   and `App.tsx:139` REMOVES the attribute for `'comfortable'`, so that value
   means ":root defaults, --row-h 32px" here — which is PORT's `normal`, not
   PORT's `comfortable`.
   Naming this block `comfortable` therefore plants a trap rather than adding a
   tier: it is inert today (nothing emits that attribute value), and the moment
   anyone "fixed" the applier to emit it, every user on the default would jump
   from a 32px row to a 40px one without asking. Measured on the live DOM
   before renaming: `data-density` is null and --row-h is 32px, i.e. the block
   was unreachable — which is exactly why the collision would have gone
   unnoticed until it fired.
   REACHING THIS TIER NEEDS A STORE CHANGE (a third density value plus a third
   control in ThemePanel), which is feature work rather than a re-skin. The
   tokens are ready for it; nothing is wired to them yet.
   ══════════════════════════════════════════════════════════════════════════ */
html[data-density="roomy"] {
  --sp-tight:     var(--sp-3);
  --sp-bond:      var(--sp-6);
  --sp-inline:    var(--sp-9);
  --sp-band-y:    var(--sp-10);
  --sp-dock:      var(--sp-12);
  --sp-item:      var(--sp-11);
  --sp-cell-x:    var(--sp-12);
  --sp-inset:     var(--sp-14);
  --sp-group:     var(--sp-16);
  --sp-pad:       var(--sp-20);
  --sp-block:     var(--sp-24);
  --sp-page:      var(--sp-40);
  --sp-section:   var(--sp-48);
  --sp-page-end:  var(--sp-64);

  --row-h:         40px;   /* PORT §9.1's comfortable row */
  --phead-h:       40px;
  --band-h:        36px;
  --row-h-label:   30px;
  --row-h-eyebrow: 26px;
  --row-h-oi:      36px;

  --btn-h-sm: 28px;  --btn-h-lg: 36px;
  --bar-h:    52px;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE ALIAS LAYER — our 83 legacy names, pointed at the substrate above.

   TEMPORARY BY CONSTRUCTION. Every entry here is a name Phases 5–6 delete from
   its call sites; the block shrinks as they do. It exists because 5,183 lines
   of CSS below and 244 inline style objects across 51 files name these, and a
   simultaneous rewrite of all of them is how a migration becomes a rewrite.

   An ALIAS is a second NAME for one decision — survivable. A second VALUE for
   one decision is the mirror the record forbids, and it is why every line here
   is `var(--…)` and not a hex.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  /* surfaces + ink */
  --surface:        var(--panel);
  --surface-2:      var(--panel2);
  --surface-0:      var(--canvas);
  --surface-rgb:    var(--panel-rgb);
  --surface-2-rgb:  var(--panel2-rgb);
  --border:         var(--edge);
  --text:           var(--ink1);
  --text-bright:    var(--ink1);
  --text-secondary: var(--ink2);
  --text-muted:     var(--ink3);
  --text-dim:       var(--faint);

  /* semantics. --green/--red are the FILL tokens; anything using them as TEXT
     wants --up-ink/--down-ink, which is what §37's role split is for. */
  --green:     var(--up);
  --red:       var(--down);
  --green-rgb: var(--up-rgb);
  --red-rgb:   var(--down-rgb);
  --on-green:  var(--ink-on-up);
  --on-red:    var(--ink-on-down);
  --buy:       var(--up);
  --sell:      var(--down);
  /* Ink ON an order-side fill. origin/main-dev computes these per preset with
     onAccentInk(); this branch reads its ground from CSS and has no preset to
     compute from, but --buy/--sell alias --up/--down here, so the measured ink
     for those exact fills IS the answer (10.48:1 and 5.55:1). Declared because
     the rules that read them shipped from main-dev, and an undeclared token
     renders nothing rather than failing. */
  --on-buy:    var(--ink-on-up);
  --on-sell:   var(--ink-on-down);

  /* accent — points at the achromatic substrate through the legacy coral names,
     so the 110 --accent call sites keep resolving while the coral spelling is
     retired in P3 with its own call sites. */
  --accent:       var(--coral);
  --accent-rgb:   var(--coral-rgb);
  --accent-ink:   var(--coral-lt);   /* §37: the accent used as INK or a BOUNDARY */
  --accent-hover: var(--accent-hi);  /* PORT §5.2 — lightness only, never a hue */
  --on-accent:    var(--ink-on-accent);
  --accent-soft:  rgba(var(--coral-rgb),.16);
  --accent-bg:    rgba(var(--coral-rgb),.08);
  /* PORT §5.2's own spelling of --accent-soft. */
  --accent-muted: var(--accent-soft);

  /* ── DIRECTION, under the ported names ── PORT §5.3
     --bull / --bear ARE --up / --down. One decision, two spellings: the ported
     control and data layers (P3, P5) are written against bull/bear, and 5,183
     lines of existing CSS are written against up/down. Aliasing is what lets
     both be true without a second value to keep in sync. */
  --bull: var(--up);   --bull-rgb: var(--up-rgb);
  --bear: var(--down); --bear-rgb: var(--down-rgb);
  /* PORT §5.1's text ramp, under its own names. Same four decisions as the ink
     ladder — see the ink block for why the ALPHAS are ours and the VALUES are
     the ported ramp's. */
  --text-primary:  var(--ink1);
  --text-tertiary: var(--ink3);
  --text-disabled: var(--ink-disabled);
  --text-on-accent: var(--ink-on-accent);

  /* Categorical hues — the calendar's event-type chips. NOT semantic and not
     the accent, so they take the two families designed for exactly that: the
     widget-link groups and the indicator arc. --cat-gold is --warn because a
     "gold" chip is a warning-weight amber and there is no second one. */
  --cat-blue:   var(--group-2);
  --cat-violet: var(--ind-1);
  --cat-purple: var(--group-1);
  --cat-cyan:   var(--ind-3);
  --cat-gold:   var(--warn-ink);   /* a chip LABEL — the ink role, not the fill */

  /* fields. --field-border is the one place a raw hex survives the port: §10's
     --edge is a PANEL boundary at 1.53:1 and WCAG 1.4.11 wants 3:1 for a
     control, so an input needs its own value. Measured on the new --panel2
     (#110F0F) rather than the old #232323 — a darker field only raises the
     ratio, and `tests/tokens-contrast.test.ts` asserts it. */
  --field-bg:     var(--panel2);
  --field-border: #70707a;

  /* ONE FOCUS RING for the whole system — §40, at --coral-lt. The 2px --bg
     spacer stays: it is what keeps the ring legible on an accent-FILLED
     control, where ring-on-fill would otherwise vanish. Was
     `rgba(var(--accent-rgb),.55)`, which composited to 2.35:1 against --bg,
     i.e. the one control state a keyboard user could not reliably see. */
  --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--coral-lt);

  /* overlay + tooltip */
  --overlay-bg:   var(--scrim);
  --overlay-blur: blur(4px);
  --tooltip-bg:   #000;
  --tooltip-fg:   #fff;

  /* border as a triple, for the rgba() call sites that predate --hair2 */
  --border-rgb: 128,128,128;

  /* spacing — an exact remap onto the §42 ramp, so nothing moves a pixel */
  --space-1:  var(--sp-2);   --space-2:  var(--sp-4);   --space-3:  var(--sp-6);
  --space-4:  var(--sp-8);   --space-5:  var(--sp-10);  --space-6:  var(--sp-12);
  --space-7:  var(--sp-14);  --space-8:  var(--sp-16);  --space-10: var(--sp-20);

  /* radii. --radius-md/lg/xl move 6→8 / 8→12 / 10→12: §42's argument for
     spacing is that frequency beats roundness, and the corner scale is the one
     place the record is unambiguous that there are exactly three steps plus a
     pill. --radius-xs stays 2px — it is a 1px hairline's inside corner, below
     the scale's floor rather than off it. */
  --radius-xs:   2px;
  --radius-sm:   var(--r-sm);
  --radius-md:   var(--r-md);
  --radius-lg:   var(--r-lg);
  --radius-xl:   var(--r-lg);
  --radius-pill: var(--r-pill);

  /* elevation ladder → the two governed shadows */
  --shadow-sm: var(--shadow-card);
  --shadow-md: var(--shadow-card);
  --shadow-lg: var(--shadow-pop);
  --shadow-xl: var(--shadow-pop);

  /* motion → the §6 bands. --t-normal was .12s and --d-fast IS 120ms, so the
     most-used duration in the app is unchanged. */
  --t-fast:   var(--d-micro);
  --t-normal: var(--d-fast);
  --t-slow:   var(--d-menu);
  --ease:        var(--e-snap);
  --ease-out:    var(--e-smooth);
  --ease-in-out: var(--e-smooth);

  /* ── TWO OVERSHOOT CURVES, AND THE LINE THEY MAY NOT CROSS ───── §24e
     §6's bands and its two easings stay authoritative: --e-snap for state,
     --e-smooth for entrances, 300ms ceiling, and LIVE DATA GETS NO
     TRANSITION — easing a price is a lie about when it changed.

     What §6 has no token for is OVERSHOOT. Both curves below end above 1
     and settle back, which is what separates a surface that "pops" from one
     that merely fades. Used only where an element ENTERS from nothing —
     tooltips, popovers, menus, toasts. Never on a value, never on a state
     the user drives dozens of times an hour (a timeframe button that
     springs is a timeframe button that feels slow by the tenth press).
       --e-pop    overshoots ~13%, for small fast surfaces (tooltips)
       --e-spring overshoots ~9%,  for larger ones (menus, dialogs)
     Neither is a physics spring: a closed curve is interruptible by the
     transition engine and costs nothing per frame, which matters beside a
     WebGL canvas that owns the frame budget. */
  --e-pop:    cubic-bezier(.2, 1.5, .4, 1);
  --e-spring: cubic-bezier(.34, 1.56, .64, 1);
  /* PRESS. §6 had no press token and the app used translateY(.5px) in eight
     places — sub-pixel on a 1x display, which is to say invisible. A scale
     is felt rather than seen and it scales the label with the box, so the
     whole control acknowledges the finger. .97 at 100ms; the release is the
     same curve, so a fast double-press never fights a pending animation. */
  --press: scale(.97);
  --d-press: 100ms;

  /* TYPE ROLES, by JOB rather than by number — §45's whole finding is that a
     terminal reads small because the scale is BYPASSED. Sizes move where the
     role says they should:
       2xs micro-labels/badges 9  → 9.5   xs meta/captions   10 → 10.5
       sm  secondary labels   11  → 12    md control text    12 → 12
       base body              13  → 13    lg panel titles    14 → 15
       xl  modal titles       15  → 15    2xl stat readouts  17 → 19
     --text-data is new: every NUMERAL is --t-data in --font-mono with tabular
     figures, so digits do not reflow as prices tick. */
  --text-2xs:  var(--t-micro);
  --text-xs:   var(--t-meta);
  --text-sm:   var(--t-label);
  --text-md:   var(--t-label);
  --text-base: var(--t-body);
  --text-lg:   var(--t-heading);
  --text-xl:   var(--t-heading);
  --text-2xl:  var(--t-display);
  --text-data: var(--t-data);
  --font-mono: var(--mono);

  /* weight roles → the §5 role weights */
  --fw-med:   var(--t-data-w);      /* 500 */
  --fw-semi:  var(--t-label-w);     /* 600 */
  --fw-bold:  var(--t-display-w);   /* 700 */
  --fw-black: 800;                  /* hero readouts — above the role table */

  /* ICON glyph sizes. Deliberately NOT part of the type scale: a close glyph
     is a picture, and forcing it onto a text step made 16/18/26/30px read as
     four more type sizes in the census when they were never type. */
  --icon-md: 16px;  --icon-lg: 18px;  --icon-xl: 28px;

  /* control + chrome heights. --btn-h matches --row-h by construction: a
     control in a data row is the height of the row it sits in. */
  --btn-h-sm: 28px;  --btn-h: var(--row-h);  --btn-h-lg: 36px;
  --header-h: var(--bar-h);
  /* Total chrome above the body. The header is now all of it — the ticker
     strip was removed, and leaving its 30px in this sum would push every panel
     anchored under the header down against a strip that is not rendered. */
  --chrome-h: var(--header-h);

  /* Explicit transition property LISTS, replacing `transition: all`.
     `all` was on 33 selectors — including the canonical `.btn` — and resolved
     on 422 live elements. Two real costs, not style points:
       1. it animates the FOCUS RING. --focus-ring is a box-shadow, so `all`
          faded the keyboard indicator in over 100ms instead of asserting it.
       2. it animates any LAYOUT property a class swap happens to change, which
          is both a jank source and how a hover can silently reflow a row.
     box-shadow is deliberately ABSENT — that is what fixes (1). */
  --tx-fast:
    background-color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease), opacity var(--t-fast) var(--ease),
    transform var(--t-fast) var(--ease);
  --tx-normal:
    background-color var(--t-normal) var(--ease), border-color var(--t-normal) var(--ease),
    color var(--t-normal) var(--ease), opacity var(--t-normal) var(--ease),
    transform var(--t-normal) var(--ease);
}

/* light-ground overrides for the alias layer — only what the substrate above
   cannot already reach. */
[data-theme="light"] {
  --field-bg:     var(--elev);
  /* 3.21:1 on --elev. --edge is 1.4:1 here, and an input with no visible
     boundary does not read as editable (WCAG 1.4.11). */
  --field-border: #8A8075;
  --border-rgb:   197,189,180;
  /* Accent tints at dark-mode alphas are near-invisible over paper — active
     chips and selections looked washed out. Deeper tints keep them saturated. */
  --accent-soft: rgba(var(--coral-rgb),.22);
  --accent-bg:   rgba(var(--coral-rgb),.12);
  /* Pure black tooltips look severed on light chrome. */
  --tooltip-bg: #26201B;
}

/* Native form controls follow the theme in BOTH grounds (§129): dark
   previously rendered UA-white selects and blue checkboxes because
   color-scheme was never set. Element-level rules, so any class-styled field
   still wins on specificity. */
input, select, textarea { accent-color: var(--accent); }
select {
  background: var(--field-bg); color: var(--text);
  border: 1px solid var(--field-border); border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3); font: inherit;
}
/* These overlays invert direction between grounds (light-tint on dark,
   dark-tint on light), so they stay explicit — but they are the LIFT ladder,
   which is exactly what --lift-c exists to flip. */
.ind-legend-row { background: var(--lift-2); }
/* Scrollbars: a --panel2 thumb vanishes on paper — the lift ladder flips it. */
* { scrollbar-color: var(--rule-2) transparent; }
::-webkit-scrollbar-thumb { background: var(--rule-2); }
::-webkit-scrollbar-thumb:hover { background: var(--rule-3); }
/* Focused-pane ring: 2px solid accent overpowers a white canvas — thin + fade.
   §10's step between resting and focused is the same decision, one layer up. */
[data-theme="light"] .chart-wrap.focused::after,
[data-theme="light"] .chart-pane.focused::after {
  border-width: 1px; border-color: var(--edge-focus);
}
/* Side drawers cast a shadow tuned for dark — --shadow-pop's ink flips. */
.tool-dock { box-shadow: -10px 0 30px rgba(var(--drop-c),.14); }
.depth-dom,
.objtree { box-shadow: -8px 0 24px rgba(var(--drop-c),.12); }
/* Favourite star. #ffd700 gold measured 1.14:1 on light chrome — invisible;
   --warn is solved on both grounds (5.06:1 on paper) and is the same amber. */
.ind-fav-btn:hover,
.ind-fav-btn.is-fav { color: var(--warn-ink); }

/* ══════════════════════════════════════════════════════════════════════════
   CVD — THE ONE PALETTE THAT GIVES THE ACCENT A HUE          PORT §3, §5.2

   Invariant 2 makes the accent achromatic so that green and red mean one thing
   only. PORT §3 records the single documented exception, and it is an
   ACCESSIBILITY one rather than a taste one:

     "Colour-vision-deficiency palette. The ONE case where the accent takes a
      hue: with blue/orange direction colours a neutral silver accent stops
      being distinguishable from the bull colour."

   With CVD active the bull is a sky blue (#38BDF8) — light, low-saturation,
   and close enough to an achromatic #F3F4F6 in both lightness and chroma that
   "you can act on this" and "the market went up" stop being separable. A user
   who selected the colourblind-safe palette is the last user who should have
   to tell those apart by hue.

   OVERRIDES `--accent-base`, NOT `--accent`. The accent flows
   --accent-base → --coral → --accent through the alias layer; writing --accent
   here would be beaten by the alias block below it, which is the defect this
   block was found by (the attribute was being set by `applySectionPalette`
   and nothing in this stylesheet consumed it).

   The focus ring reads --coral-lt rather than a --focus-ring colour: in this
   system --focus-ring is a two-layer BOX-SHADOW, not a hue.
   ══════════════════════════════════════════════════════════════════════════ */
:root[data-direction-palette="cvd"] {
  --accent-base: #C4A5FB;  --accent-base-rgb: 196,165,251;
  --accent-hi:   #D3BBFC;
}
[data-theme="light"][data-direction-palette="cvd"] {
  /* re-solved for paper by §37's rule — the violet holds its hue and drops
     lightness until it clears the boundary bar on the stock. */
  --accent-base: #5B3F91;  --accent-base-rgb: 91,63,145;
  --accent-hi:   #4A3277;
}

/* ══════════════════════════════════════════════════════════════════════════
   FONT STACKS — PORT §7.1                                              [NEW]

   Three swappable stacks on `<html data-font>`. The ported system ships
   default / `geist` / `legacy`; the first two are taken as specified and the
   third is ADAPTED, because "legacy" only means something relative to a
   product's own history:

     (default)  Inter        — the ported UI face
     geist      Geist        — as specified, self-hosted
     legacy     IBM Plex     — THIS product's previous face, which is what the
                               slot is for. PORT's legacy is Plus Jakarta Sans,
                               which was never this product's face; shipping it
                               would add an 18KB subset to offer a stack no
                               user here has ever seen. Plex is already loaded
                               as Inter's metric fallback, so this costs nothing.

   FEATURE SETTINGS MOVE WITH THE FACE, and that is the part that is easy to
   miss: `cv05`/`cv08` are INTER's character-disambiguation features (the
   1 / l / I problem) and mean nothing to Geist, which spells the same intent
   `zero` + `lnum`. A stack switch that changed the family and left the feature
   string behind would silently drop digit disambiguation — on a screen of
   instrument codes and quantities, that is a misread order.
   `tnum` is in all three: invariant 4 is not negotiable per-face.

   WEIGHTS also move. PORT §7.1 gives the legacy stack 500/600/700 against the
   other two at 400/500/600, because Plus Jakarta runs light; Plex does not, so
   it keeps the standard ramp rather than inheriting a number solved for a
   different typeface.
   ══════════════════════════════════════════════════════════════════════════ */
:root[data-font="geist"] {
  --sans: 'Geist Variable', 'Inter Variable', system-ui, sans-serif;
  --mono: 'Geist Mono Variable', 'IBM Plex Mono', ui-monospace, monospace;
  --font-features: "tnum" 1, "zero" 1, "lnum" 1;
}
:root[data-font="legacy"] {
  --sans: 'IBM Plex Sans Variable', system-ui, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  /* Plex has no cv05/cv08; its zero is already slashed. tnum stays. */
  --font-features: "tnum" 1;
}
