/* THE single declaration site for the whole product (VIS-01 / D-01). /login links it
 * at RUNTIME through the /static mount; the React SPA @imports it at BUILD TIME from
 * frontend/src/index.css. A value declared anywhere else is drift.
 * Two layers (D-02): PRIMITIVES = the palette, the ONLY place a hex/rgba literal may
 * appear; SEMANTICS = the intent, var() references only. Components read the semantic
 * layer. Fonts are SELF-HOSTED from /static/fonts — no third-party CDN at runtime. */

/* --- Self-hosted fonts (drop the woff2 files into /static/fonts) --------------- */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/Inter-Variable.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/JetBrainsMono-Variable.woff2") format("woff2");
}

:root {
  /* === PRIMITIVES — the ONLY place a hex/rgba literal may appear === */

  /* --- The four-step elevation ladder (VIS-02 / D-07). Depth is TINT, not shadow. §4.1
   * re-values all four onto the Voltrex ramp; luminance still rises strictly
   * (0.00377 < 0.00770 < 0.01196 < 0.01882), which is C-2. --- */
  --canvas: #0D0A1A;        /* step 1 — the canvas (60%) */
  --panel: #16122A;         /* step 2 — cards / tables / tiles (30%) */
  --elev-overlay: #1E1836;  /* step 3 — sheets / popovers / tooltips / login card */
  --elev-float: #272044;    /* step 4 — dialogs + palette + active nav row */

  /* --- Hairline ramp, one per step. A single hairline is OUT OF CONTRACT: the raised
   * hairline on the float surface computes to 1.000 — invisible. §4.1 re-values all
   * three and the ratios RISE (1.23/1.32/1.39): §6.2 takes corners to 20px, and an edge
   * that reads at 6px vanishes around 20px. --- */
  --border: #2A2247;        /* on raised, 1.23 — name kept (the SPA maps --color-border
                             * onto it); no longer shares a hex with --disarmed-chip */
  --hairline-2: #342B57;    /* on overlay, 1.32 */
  --hairline-3: #3F3567;    /* on float, 1.39 */

  /* --- Text + accent family (10%). §4.1: lavender-white text, violet accent. §4.4 cuts
   * --accent to two meanings: the :focus-visible ring and the KB/citation affordance —
   * never a heading, a link, a hover, or a border. --- */
  --text: #F2F0FF;          /* text primary (lavender-white) */
  --muted: #A9A2C8;         /* labels, captions >= 14px, timestamps, axis ticks */
  --accent: #8B7CFF;        /* focus ring + the KB/grounding hue (§4.4) */
  --accent-fill: #5B4BD6;   /* fill-with-white-text: white on --accent is 3.27 (fails
                             * AA at 14px/600); this gives 6.14 */
  --accent-lifted: #A79BFF; /* accent TEXT on overlay/float (--accent is 5.18 / 4.67) */
  --on-accent: #FFFFFF;     /* foreground on an accent fill */

  /* --- P&L / direction (functional, never decorative) --- */
  --profit: #16C784;        /* profit / long / up */
  --loss: #EA3943;          /* loss / short / down */
  --loss-lifted: #FF6B72;   /* loss TEXT on overlay/float (--loss is 4.17 / 3.75) */
  --neutral: #958DBB;       /* flat / zero. Owner ruling 32-17: --muted's twin (both were
                             * #8A93A3) till §4.1 moved --muted and left this on slate. Now
                             * --muted's hue 251.1deg/sat .257 at the OLD luminance (.2903 vs
                             * .2892) — contrast held (panel 5.90 was 5.88, float 4.94, AA). */
  --warning: #F0A92C;       /* drift / halted / cooldown / stale */

  /* --- Safety-state colours (SAFETY-CRITICAL — CTRL-04 / CTRL-05 / D-03). FROZEN, and
   * guarded through var() chains by test_safety_tokens_resolve_to_exact_hex. Phase 32
   * moves the whole palette AROUND these eight and changes not one of them (C-1).
   * DO NOT "CLEAN UP" THE UNPAINTED THREE: §4.5/§4.7 turn the LIVE band, the MAINNET
   * chip and the ARMED/DISARMED chips into quiet wash pills, so --live-strip,
   * --armed-chip and --disarmed-chip paint NOWHERE yet stay declared and frozen
   * deliberately — deleting them as "dead" is a C-1 failure. --- */
  --demo-strip: #1E3A2F;    --demo-text: #16C784;
  --live-strip: #EA3943;    --live-text: #FFFFFF;
  --armed-chip: #F0A92C;    --armed-text: #0B0E14;
  --disarmed-chip: #232A36; --disarmed-text: #8A93A3;
  --kill-engaged: #7A0C12;  --kill-engaged-text: #FFFFFF;

  /* --- Depth literals. The --elev- prefix is MANDATORY: Tailwind v4's shadow theme
   * namespace is --shadow-* too, so a primitive sitting there would make the SPA's
   * @theme inline mapping self-referential — emitted SILENTLY, invalid at
   * computed-value time, and the D-07 depth cue would simply never render. --- */
  --scrim: rgba(6, 4, 14, .72);                   /* re-tinted to the new canvas hue */
  --lit-edge: 0 1px 0 rgba(255, 255, 255, .06);   /* inset — the lit top edge; nudged up
                                                   * because a 20px corner spreads it */
  --well-edge: 0 1px 0 rgba(0, 0, 0, .35);        /* inset — recessed wells */
  /* §4.2/§6.3 — card depth for the RAISED step. The --elev- prefix is mandatory for the
   * reason above: index.css maps `--shadow-raised: var(--elev-shadow-raised)`, and
   * naming it --shadow-raised would make that self-referential and paint no shadow. */
  --elev-shadow-raised: 0 2px 8px -2px rgba(0, 0, 0, .45);
  --elev-shadow-overlay: 0 8px 24px -6px rgba(0, 0, 0, .55), 0 2px 6px -2px rgba(0, 0, 0, .40);
  --elev-shadow-float: 0 24px 56px -12px rgba(0, 0, 0, .70), 0 8px 18px -6px rgba(0, 0, 0, .50);

  /* --- §4.2 — data palette + pill washes + the bloom. DATA hues are chart series and
   * their delta chips (§4.3); --wash-danger is the ONLY red wash (§4.5). --- */
  --gold: #F0C070;          /* data A — up candles, cost line, ARMED tint */
  --magenta: #E7609B;       /* data B — down candles, drawdown area, negative deltas */
  --wash-gold: rgba(240, 192, 112, .14);      /* gold pill fill */
  --wash-magenta: rgba(231, 96, 155, .14);    /* magenta pill fill */
  --wash-positive: rgba(22, 199, 132, .14);   /* mint pill fill (positive badge, LIVE) */
  --wash-danger: rgba(234, 57, 67, .16);      /* danger pill fill — KILL/DANGER ONLY */
  --wash-accent: rgba(139, 124, 255, .14);    /* citation pill fill / KB surfaces */
  --wash-neutral: rgba(242, 240, 255, .06);   /* neutral pill fill, table row hover */
  /* The card bloom (§6.4) — a GRADIENT, not a colour: it ships as a background-image
   * and must never enter Tailwind's --color-* namespace (C-14 rationale below). */
  --panel-glow: radial-gradient(120% 100% at 100% 0%, rgba(139, 124, 255, .10) 0%, rgba(139, 124, 255, 0) 62%);
  /* The white "this one is selected" pill (§4.4) — mirrors --text, declared separately
   * so it can be tuned without moving every line of body text. */
  --pill-active-bg: #F2F0FF;
  --pill-active-fg: #0D0A1A;

  /* --- Spacing scale (multiples of 4). This is the COMFORTABLE density; the compact
   * block below re-values md and up. --space-row is the table/list row minimum height
   * — the "row height" D-11 compresses. --- */
  --space-xs: 4px;          --space-sm: 8px;          --space-md: 16px;
  --space-lg: 24px;         --space-xl: 32px;         --space-2xl: 48px;
  --space-3xl: 64px;        --space-row: 36px;        --space-cell: 8px;

  /* --- Type: SIX sizes, TWO weights (VIS-03 / D-09). The mono face is
   * --font-mono-face, not the bare Tailwind font-namespace name — same trap.
   * The role->weight pairing is FIXED: caption/data/body are --weight-regular (400);
   * heading/display/hero are --weight-semibold (600). A caption at 600 or a heading at
   * 400 is OUT OF CONTRACT. No 500 and no 700 token exists, here or anywhere.
   * SAFETY (binds Phases 18 + 24): white --live-text on --live-strip computes to
   * contrast 4.07 — below AA for normal text, legal ONLY because the safety strip
   * renders at --size-display (22px) weight 600, which qualifies as WCAG large text.
   * The safety-strip text must NEVER drop below 18.66px at weight 600.
   * D-10: the caption size NEVER uses --muted. Captions render in --text-primary
   * (#E6EAF0, contrast >= 11.94 on every ladder step); --muted stays restricted to
   * >= 14px, and no --muted-strong variant is introduced.
   * --size-hero is RESERVED for the Phase 20 / COCK-01 equity headline — never
   * decorative; Phase 20 is its first legal consumer. --- */
  --font-ui: "Inter", -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono-face: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --size-caption: 11px;     /* labels, units, timestamps, axis ticks */
  --size-data: 13px;        /* table body / dense rows / citation text */
  --size-body: 14px;        /* default UI text / labels */
  --size-heading: 18px;     /* panel titles */
  --size-display: 22px;     /* banner text / headline metric / kill label */
  --size-hero: 44px;        /* equity headline ONLY (Phase 20 / COCK-01); §5 moves it
                             * 36 -> 44: 36 is not an oversized numeral beside a 20px
                             * card radius (§19 item 1) */
  --weight-regular: 400;    --weight-semibold: 600;

  /* Leading + tracking are TOKENS so they are assertable and so density cannot reach
   * them (D-11 freezes type across densities). Deliberate delta from Phase 11: data
   * leading drops 1.5 -> 1.40, recovering ~1.5 rows per screen without touching a
   * single size — with type frozen, line-height is where row economy comes from. */
  --leading-caption: 1.45;  --leading-data: 1.40;     --leading-body: 1.50;
  --leading-heading: 1.20;  --leading-display: 1.20;  --leading-hero: 1.10;
  --tracking-caption: 0.02em;  --tracking-data: 0;    --tracking-body: 0;
  --tracking-heading: 0;    --tracking-display: 0.02em;  --tracking-hero: -0.01em;

  /* --- Radii, per step. §6.2 — cards read as OBJECTS, not outlined boxes. --- */
  --radius: 12px;           /* drives the shadcn primitive radii (sm 10 / md 12 / lg 12) */
  --radius-chip: 999px;     /* every pill: status chips, lane badges, tab segments */
  --radius-panel: 20px;     /* every card, tile, chart shell, table card */
  --radius-overlay: 16px;   /* popovers, tooltips, sheets, the drawer */
  --radius-float: 20px;     /* dialogs, the ⌘K palette */
  --radius-login: 24px;     /* the login card */

  /* --- Density-immune structural tokens (D-11). Deliberately OUTSIDE the --space-*
   * namespace so the compact block CANNOT reach them — the guard asserts exactly that.
   * Phase 17 DECLARES them as the forward contract Phases 18/20/23 bind to when those
   * surfaces move. The existing arbitrary literals min-h-[44px] (KillSwitch,
   * VenueSwitch) and h-[420px] / max-h-[420px] (WaveChart, ReasoningFeed) are NOT
   * rewritten in this phase: they are already immune to density, and rewriting them on
   * a live real-money kill switch is unnecessary risk. --- */
  --banner-height: 40px;    /* SAFE-01 / CTRL-04 — the safety strip stays unmissable */
  --hit-target-min: 44px;   /* SAFE-02 — kill-switch hit target, every breakpoint */
  /* The SECOND platform threshold, and the same species as the 44px above: a number the
   * operating system enforces, not a value this product chose. Safari on iOS auto-zooms
   * the PAGE when any input under 16px takes focus, and the zoom is a page-level state
   * with open reports that it does NOT reset on blur — so one focused field can leave the
   * whole cockpit zoomed (research §M4). Deliberately declared OUTSIDE the --size-*
   * namespace: those six roles are asserted by SET equality (D-09 / VIS-03), so a seventh
   * entry there is a contract breach by construction, and a platform floor is not a type
   * role — the research's suggested name for this token would have broken that guard.
   * The fix is to RAISE the font, never to disable zoom: pinning the viewport's maximum
   * scale is a WCAG 1.4.4 violation, and no viewport attribute doing so exists here.
   * Consumers: the ⌘K palette input below 768px (frontend/src/components/ui/command.tsx)
   * and .field-input below 768px (the sole @media block). */
  --input-font-min: 16px;
  --chart-height-wave: 420px; /* D-11 — shrinking the analyst surface defeats it */
  --chart-height-analytics: 240px; /* §6.1 — was a hard-coded h-[220px]; 220 is a root
                             * cause of the §13.2 axis collision, so it stays out of
                             * --space-* where compact density cannot shrink it back */

  /* --- Shell structural dimensions (D-11, added by Phase 18). Same mechanism and the
   * same reason as the three above: the compact block can only re-value the names it
   * declares, so a structural dimension parked OUTSIDE --space-* is unreachable by a
   * cosmetic preference. --alarm-outline-w and --nav-rail-w deliberately share the
   * value 3px and deliberately do NOT share a name — two independent contracts must
   * never become one token, or tuning the nav rail would silently thin the alarm
   * frame. --icon-* and --focus-ring-* are the only non-multiples of 4 in the product:
   * both are optical, not structural, which is the second reason they are not
   * --space-* (p-icon-md must never be a legal utility). --- */
  --sidebar-width: 280px;   /* D-01/D-03 — nav column; raised 240->280px, owner round 2
                             * ("still too lean"); kill confirm-label wrap follows it */
  --drawer-width: 280px;    /* D-07 — the off-canvas drawer below 768px; wider than the
                             * desktop rail because every target in it is >= 44px */
  --content-max-w: 1600px;  /* content-pane cap so tables do not stretch an ultrawide */
  --alarm-outline-w: 3px;   /* SAFE-03 / D-10 — the KILL-ENGAGED viewport frame; a
                             * compressed alarm signal is a safety regression */
  --nav-rail-w: 3px;        /* the active-route rail — the non-colour "active" signal */
  --focus-ring-w: 2px;      /* every :focus-visible outline, /login and SPA alike */
  --focus-ring-offset: 1px; /* its offset — keyboard reachability is an a11y contract */
  --icon-sm: 14px;          /* chip icons, the WS connection indicator */
  --icon-md: 18px;          /* route icons, the safety-strip state icon */
  --icon-lg: 20px;          /* the kill octagon */

  /* --- Phase 33 motion vocabulary (D-01 "alive data, calm chrome"). Durations and
   * easings ONLY — no colour and no spacing, so C-7 (no hex after the marker) and the
   * D-11 compact-density contract are both untouched either way. Two speeds: chrome is
   * quick and subtle, data is expressive. D-04 grants danger surfaces NO duration at
   * all — they snap; a motion token applied to a kill/alarm surface is a safety
   * regression, not a styling choice. --- */
  --motion-press: 90ms;     /* press-down scale on tappable cards/buttons (D-03) */
  --motion-chrome: 180ms;   /* route x-fade, tab indicator, drawer (D-01's 150-250ms band) */
  --motion-data: 400ms;     /* number tick, sparkline wipe, chart series fade-in (D-02) */
  --motion-flash: 600ms;    /* value-change flash hold, fresh-data hairline pulse (D-02a/b) */
  --motion-shimmer: 1400ms; /* skeleton highlight sweep (D-03) */
  --motion-breathe: 2400ms; /* connection-dot loop — DELIBERATELY unlike the kill pulse (§6.5) */
  /* Decelerate; NEVER a spring/bounce. An equity figure that overshoots its true value,
   * even for 80ms, has displayed a number the account never held. */
  --motion-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --motion-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);  /* the breathing dot's loop */

  /* STRUCTURAL, and deliberately NOT a --space-* value: the compact block can only
   * re-value the names it declares, so parking the bottom tab bar's height outside
   * --space-* is what makes it unreachable. If compact density could reach it the tab
   * bar would shrink below --hit-target-min and D-10's thumb-reachable promise would
   * break silently — every tab would stop being a legal touch target with no error.
   * 56px = 18px icon + 4px gap + 11px caption + 2x11.5px padding. */
  --tabbar-h: calc(56px + env(safe-area-inset-bottom, 0px));

  /* --- Safety z-layer (D-08). --z-safety is the dedicated top layer for the LIVE/DEMO
   * strip, the KILL-ENGAGED full-bleed latch, and the 3px chrome outline. It MUST
   * numerically exceed every z-<n> literal in frontend/src/components/ui/*.tsx (all
   * seven are z-50 today); no panel, sheet, dialog, popover, tooltip, or command
   * palette may EVER render above it. Deliberate split: Phase 17 declares and exports
   * the token, Phase 18 APPLIES it when the safety strip moves into the routed chrome
   * and becomes positioned — z-index has no effect on an unpositioned element, and the
   * banner is non-sticky by owner decision. D-08 is NOT complete at the end of Phase
   * 17: a sheet opened today still covers the strip. --- */
  --z-base: 0;              --z-sticky: 30;
  --z-overlay: 50;          --z-safety: 900;
  /* NAV-02 (Phase 19): the non-modal ⌘K command palette. The ORDERING is the contract —
   * strictly 50 (--z-overlay) < 100 < 900 (--z-safety) — so it paints above the content
   * pane, tiles, and any overlay/scrim, yet below the safety band + kill zone. The exact
   * number is discretion; single declaration site, never a component or arbitrary value. */
  --z-palette: 100;
  /* === END PRIMITIVES === */

  /* === SEMANTICS — intent names; var() references only, never a literal ===
   * Three binding rules, all load-bearing:
   *  - Elevation is HIERARCHY, never STATE: depth says where a surface sits in the
   *    stack, never armed/disarmed, live/demo, stale, or P&L.
   *  - --text-loss-elevated / --text-accent-elevated are scoped to TEXT on the overlay
   *    and float steps ONLY — never a fill, and never the LIVE strip.
   *  - --muted sits at 4.66 on float, only 0.16 above AA, so the float primitive is an
   *    UPPER BOUND on lightness; re-check --muted before ever lightening it. */

  /* Surfaces — the four-step ladder plus the recessed well */
  --surface-base: var(--canvas);
  --surface-raised: var(--panel);
  --surface-overlay: var(--elev-overlay);
  --surface-float: var(--elev-float);
  --surface-inset: var(--canvas);

  /* Hairlines — one per elevation step */
  --hairline-raised: var(--border);
  --hairline-overlay: var(--hairline-2);
  --hairline-float: var(--hairline-3);

  /* Text intents */
  --text-primary: var(--text);
  --text-secondary: var(--muted);
  --text-loss-elevated: var(--loss-lifted);
  --text-accent-elevated: var(--accent-lifted);

  /* Action / focus */
  --action-primary-bg: var(--accent-fill);
  --action-primary-fg: var(--on-accent);
  --focus-ring: var(--accent);

  /* Chart data series (§4.2/§4.3), named by ROLE not hue so §13.3 can re-point a series
   * without every consumer learning the word "gold". */
  --data-primary: var(--gold);
  --data-secondary: var(--magenta);
  /* Selection (§4.4) — the white active pill: tabs, active route row, ⌘K selected item.
   * White-as-fill means "this one is selected" and nothing else. */
  --select-bg: var(--pill-active-bg);
  --select-fg: var(--pill-active-fg);

  /* The two ambient blooms (§6.4): --surface-glow per card, --app-backdrop page-level.
   * BOTH are background-IMAGE values — C-14 — because a filter/backdrop-filter/opacity
   * delivery would create a stacking context and trap --z-safety (900) below a z-50
   * sheet. color-mix() keeps --app-backdrop hex-free below the marker (C-7). */
  --surface-glow: var(--panel-glow);
  --app-backdrop: radial-gradient(1200px 720px at 12% -12%, color-mix(in oklab, var(--accent) 16%, transparent), transparent 60%), radial-gradient(900px 620px at 100% 0%, color-mix(in oklab, var(--magenta) 10%, transparent), transparent 55%);

  /* Shell chrome (Phase 18). --alarm-frame is KEYWORD-FREE, exactly like --lit-edge
   * and --well-edge: the `inset` keyword is supplied by the SPA's @theme inline
   * mapping (--inset-shadow-alarm), so the frame geometry and the frame colour keep
   * exactly one declaration site. An OUTER shadow on a full-viewport fixed box paints
   * entirely off-screen, so the keyword is not optional at the call site — it is
   * simply owned by the mapping instead of by this file.
   * --safety-band-h is the height of the WHOLE safety band; it drives the shell
   * grid's first row and the drawer's top offset from one place. */
  --alarm-frame: 0 0 0 var(--alarm-outline-w) var(--kill-engaged);
  --safety-band-h: var(--banner-height);
}

/* Compact density (COCK-04 / D-11 / D-12) — the CSS half; the toggle UI is plan 17-06.
 * MECHANISM: a data-density attribute on <html> re-values the spacing primitives.
 * `@theme inline` already inlined var(--space-md) into .p-md, so every existing
 * p-md / gap-lg / px-xl in the SPA cascades with ZERO component edits. Specificity
 * (0,1,1) beats :root (0,1,0), so source order is irrelevant.
 * D-11: compact compresses spacing, padding, row height, and grid gaps ONLY. Type
 * sizes, leading, tracking, chart heights, the 40px banner height, the 44px hit
 * target, colour, elevation, radii, and z-index are IDENTICAL in both densities —
 * which is why the three structural tokens live outside --space-*. xs (4px) and sm
 * (8px) are deliberately absent here: 4px is already the minimum legible icon gap and
 * 8px the minimum legible cell gutter, so compressing them would cost legibility for
 * no row economy.
 * D-12 (persistence): localStorage key "wavebot.density", values "comfortable" |
 * "compact"; absent or unrecognised => comfortable. Client-only, because the kill
 * switch is the ONLY mutating action in the entire SPA and a preferences POST would
 * breach that read-only invariant for a cosmetic setting.
 * SECURITY (threat T-17-03): the stored value is read inside try/catch and compared
 * against a LITERAL allow-list (d === "compact"). It is NEVER interpolated into a
 * selector, a class string, or innerHTML — the selector below is static. Plan 17-06
 * implements the reader; this comment is the contract it must satisfy. */
:root[data-density="compact"] {
  --space-md: 12px;         --space-lg: 16px;         --space-xl: 24px;
  --space-2xl: 32px;        --space-3xl: 48px;        --space-row: 28px;
  /* --space-cell is the vertical cell padding that ACTUALLY sets row height. --space-row
   * alone cannot: `min-height` is undefined on a table-row box (CSS 2.1 §10.7) and every
   * engine ignores it, so the row floor is applied as `height` and the cell padding must
   * shrink with it or the cell keeps the row tall regardless (CR-01). */
  --space-cell: 4px;
}

/* The shell breakpoint, and the ONLY media query in this file. A media CONDITION cannot
 * read a custom property (CSS spec), so 767.98px is the one place this literal is allowed
 * to exist. Its mirror in the SPA is Tailwind's default `md:` variant (48rem = 768px) —
 * change one and you must change both. D-19 (plan 33-07) moved both, plus AppShell's
 * DESKTOP_QUERY, in ONE commit: one breakpoint stated three ways, and moving any one
 * alone leaves a band with no sidebar AND no mobile chrome — no navigation on a tablet.
 * WHY THE VALUE DROPPED from calc(40px + 44px): D-06 moves the kill control out of the
 * band's action row into the drawer (plan 33-10), so the phone band is a SINGLE 44px row.
 * env(safe-area-inset-top) is added because §3.3 ships viewport-fit=cover — without it
 * the notch eats the strip. The 40px state row never shrinks: --banner-height is density-
 * AND breakpoint-immune, and at and above this breakpoint the band IS that 40px row.
 *
 * ⚠️ TWO-PART CONTRACT (33-20) — THIS TOKEN IS ONLY PART ONE. It supplies TRACK HEIGHT,
 * and a track is a container: growing it moves NOTHING inside it. Part two is CONTENT
 * OFFSET and has exactly two REQUIRED consumers, both of which re-state the same env():
 * TopBar.tsx's ROW_BASE (no explicit height, so the padding is additive: inset + 44px =
 * this token) and SafetyBanner.tsx's kill-engaged branch (the only branch without
 * max-md:hidden; it also pins max-md:h-[var(--safety-band-h)] because BANNER_BASE has an
 * explicit height and border-box takes padding OUT of one). Inset in ONE place only is
 * the defect this closes: the owner's 2026-08-14 device pass had the iOS clock painted
 * over the drawer trigger and the route title, the unused inset rendering as an empty
 * dark band with a stranded hairline — a SAFE-01 breach shipped as a layout detail. Do
 * not delete either consumer believing this token alone suffices. */
@media (max-width: 767.98px) {
  :root { --safety-band-h: calc(var(--hit-target-min) + env(safe-area-inset-top, 0px)); }
  /* The three /login fields carry --size-body (14px), which is the same sub-16px iOS
   * auto-zoom trigger as the ⌘K palette input — on the ONE page every session starts at,
   * and the only one of the two an operator can actually reach on a phone. Raised only
   * BELOW the hand-off: at >= 768px there is no iOS auto-zoom and 14px is the contracted
   * body size, so the desktop /login card renders byte-identically. It lives inside the
   * file's sole @media block by necessity, not preference — a second block would be a
   * second breakpoint literal in the single declaration site, which
   * test_safety_band_is_one_hit_target_below_the_shell_breakpoint forbids (D-19). */
  .field-input { font-size: var(--input-font-min); }
}

/* The product's SINGLE default border colour (contract A21). Tailwind preflight resets
 * every element to `border: 0 solid`, leaving border-color at its CSS-initial
 * currentColor — Tailwind ships NO theme-driven default. About ten SPA components use
 * the BARE `border` utility (alert.tsx, ReasoningFeed.tsx, the dashboard tiles,
 * charts/ChartShell.tsx, WaveChart.tsx) and NOT ONE is edited by any Phase 17 task, so
 * without this line every one of those hairlines would render in near-white
 * --text-primary. It previously lived at frontend/src/index.css:184 and moves here
 * because this file is the single declaration site (VIS-01) and /login needs it too.
 * Guarded by test_bare_border_utility_resolves_to_a_hairline_not_currentcolor — do not
 * change box-sizing, do not add a property, do not delete it. */
* { box-sizing: border-box; border-color: var(--hairline-raised); }

/* §6.4 — the ambient purple bloom over the flat canvas. background-IMAGE is deliberate
 * and is C-14: a blurred pseudo-element, a backdrop-blur utility or an opacity wrapper
 * each creates a stacking context, trapping --z-safety (900) beneath a z-50 shadcn
 * sheet. It must stay AFTER the `background` shorthand, which resets background-image. */
html, body {
  margin: 0; padding: 0;
  background: var(--surface-base); background-image: var(--app-backdrop);
  color: var(--text-primary);
  font-family: var(--font-ui); font-size: var(--size-body);
  font-weight: var(--weight-regular); line-height: var(--leading-body);
}

/* Tabular numerals on the mono face for every numeric surface (prices, P&L, sizes,
 * equity, timestamps, hashes) — decimal-fidelity display. The two /login field
 * selectors used to sit here too; they moved into the login block below so the
 * password field is not declared twice with divergent values (it now also carries
 * letter-spacing, which does not belong on .mono/.num/code). */
.mono, .num, code {
  font-family: var(--font-mono-face);
  font-variant-numeric: tabular-nums;
}

/* --- Login card (D-06 restyle) — the ONLY component CSS left in this file --------
 * /login is the one server-rendered page (D-01/D-10 — everything else is the read-only
 * SPA). The ~340 lines of dead Jinja-dashboard CSS that used to follow are DELETED
 * (D-04); git history is the archive and there is no _legacy.css. These rules
 * reference the SEMANTIC layer only. They also ship inside the SPA bundle because the
 * SPA @imports this whole file; splitting it would mean changing the href in
 * login.html, which D-01 forbids (17-UI-SPEC Open Question 5).
 * CSS-ONLY: src/wavebot/control/templates/login.html is BYTE-IDENTICAL through this
 * restyle (contract A6, pinned by test_login_template_is_byte_identical). Every
 * selector below matches the EXISTING DOM — including the two attribute selectors,
 * which is why the TOTP field gets the mono treatment with zero template change.
 * HARD RESTYLE GUARDRAIL (threat T-omu-02): this restyle must NOT introduce a
 * per-branch error message, a per-field error class, a per-field validation state, or
 * ANY visual difference between reject causes. There is exactly ONE .login-error rule
 * carrying exactly ONE copy string — locked / missing user / disabled / wrong password
 * / wrong-or-expired TOTP / replayed TOTP / missing-or-wrong Fernet key all render it
 * identically via the single _login_failed (app.py:306-319). Never add a
 * `.login-error--password`, an `.is-invalid`, or a per-field :invalid style: the
 * styling is the last place the leak could reappear after app.py closed it. --- */
/* The two min-heights are ONE declaration with a fallback, not a duplicate: an engine
 * without `dvh` drops the second and keeps 100vh. This was the last 100vh in the repo —
 * the SPA shell has sized to `dvh` since 18-03 — and on iOS 100vh is the LARGEST
 * viewport, i.e. the one that assumes the browser chrome has scrolled away, so /login
 * overshot the visible area by the height of that chrome (plan 33-20). */
/* HYGIENE, NOT THE GAP-P1 FIX — see 33-P1-DIAGNOSIS.md. Research §1.0 FALSIFIED the
 * "text-size-adjust is set nowhere" hypothesis for the SPA: Tailwind Preflight ships
 * `html, :host { -webkit-text-size-adjust: 100% }`, it survives into the compiled bundle,
 * and the `html, body` rule above deliberately does not shadow it. /login is the ONE
 * surface where the missing-guard claim is literally true — it links this file directly
 * and loads NO Preflight — so the declaration is scoped to `.login-body`, which only
 * templates/login.html renders. Scoping it here rather than on `html, body` is the point:
 * on `html, body` it would become a SECOND opinion about the SPA's already-working
 * Preflight value. Unprefixed FIRST (research §M2, single-source MEDIUM confidence):
 * written after the prefixed one it can override the working -webkit- value in Safari
 * versions that do not support the unprefixed property. */
.login-body {
  text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
  display: flex; align-items: center; justify-content: center;
  min-height: 100vh; min-height: 100dvh;
  background-color: var(--surface-base);
  background-image: radial-gradient(1100px 620px at 50% -10%,
    color-mix(in oklab, var(--surface-raised) 70%, var(--surface-base)) 0%,
    var(--surface-base) 65%);
}
/* OVERLAY elevation — the only object on an empty canvas earns real occlusion. The
 * lit edge and the drop shadow compose into ONE box-shadow (no ::before, no
 * border-image), so the card stays a single painted box. */
.login-card {
  width: 360px; padding: var(--space-2xl) var(--space-xl);
  border-radius: var(--radius-login);
  background: var(--surface-overlay); border: 1px solid var(--hairline-overlay);
  box-shadow: inset var(--lit-edge), var(--elev-shadow-overlay);
}
.login-title {
  margin: 0 0 var(--space-lg); text-align: center; color: var(--text-primary);
  font-size: var(--size-display); font-weight: var(--weight-semibold);
  line-height: var(--leading-display); letter-spacing: var(--tracking-display);
}
.login-form { display: flex; flex-direction: column; gap: var(--space-md); }
/* Uppercase is a CSS text-transform, NOT a copy change — the DOM text and the
 * accessible name stay "Username" / "Password" / "Authenticator code". D-10: the
 * caption size never uses --muted, so these render in --text-primary. */
.field-label {
  font-size: var(--size-caption); font-weight: var(--weight-regular);
  line-height: var(--leading-caption); color: var(--text-primary);
  letter-spacing: 0.08em; text-transform: uppercase;
}
/* INSET elevation — a recessed well sitting below the card. */
.field-input {
  min-height: 40px; padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-panel); box-shadow: inset var(--well-edge);
  background: var(--surface-inset); border: 1px solid var(--hairline-overlay);
  color: var(--text-primary); font-size: var(--size-body);
}
.field-input:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
.field-input[type="password"], .field-input[inputmode="numeric"] {
  font-family: var(--font-mono-face); font-variant-numeric: tabular-nums;
  letter-spacing: 0.22em;
}
.btn-primary {
  margin-top: var(--space-sm); min-height: var(--hit-target-min);
  padding: var(--space-sm) var(--space-md);
  border: none; border-radius: var(--radius-panel); cursor: pointer;
  background: var(--action-primary-bg); color: var(--action-primary-fg);
  font-size: var(--size-body); font-weight: var(--weight-semibold);
  letter-spacing: 0.01em;
}
.btn-primary:hover { filter: brightness(1.08); }
.btn-primary:active { transform: translateY(1px); }
/* The ring is --text-primary, not --accent: an accent ring on an accent fill is
 * invisible. This is the one sanctioned exception to the global focus-ring rule. */
.btn-primary:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 2px; }
.login-error {
  margin: 0 0 var(--space-md); padding-left: var(--space-sm);
  border-left: 2px solid var(--loss);
  color: var(--text-loss-elevated); font-size: var(--size-body);
}
