/* ===== FONTS ===== */
/* Self-hosted Inter (variable, latin subset) — no CDN, CSP/offline-safe. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/inter-var-latin.woff2') format('woff2');
}
/* ★ THE TYPEFACE PICKER'S FIVE OTHERS (Appearance → Typeface, core/appearance.js FONTS) — every one
   SELF-HOSTED (public/fonts/<family>/, with its OFL licence text beside it), never Google Fonts in
   production. Saira and Archivo are true variable fonts over BOTH weight and width (the display use
   reads the width axis via `font-stretch`, see [data-font=...] below); Manrope, Sora and IBM Plex
   Sans resolve to one file across their whole weight range too — Google serves each as a single
   file whatever discrete weight is asked for, so one @font-face with a weight RANGE is correct and
   is all a page ever downloads. Barlow and Barlow Semi Condensed are genuinely static per weight
   (different files), so each gets its own rule. No unicode-range, same as Inter above — a complex
   script (Telugu, Hindi, Arabic, …) keeps its system/Noto fallback; these cover Latin only. */
@font-face {
  font-family: 'Saira'; font-style: normal; font-weight: 400 800; font-stretch: 75% 125%;
  font-display: swap; src: url('/fonts/saira/saira-var-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Archivo'; font-style: normal; font-weight: 400 800; font-stretch: 75% 125%;
  font-display: swap; src: url('/fonts/archivo/archivo-var-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Manrope'; font-style: normal; font-weight: 400 800;
  font-display: swap; src: url('/fonts/manrope/manrope-var-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400 700;
  font-display: swap; src: url('/fonts/ibm-plex-sans/ibm-plex-sans-var-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Sora'; font-style: normal; font-weight: 400 800;
  font-display: swap; src: url('/fonts/sora/sora-var-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/fonts/barlow/barlow-400-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow'; font-style: normal; font-weight: 500;
  font-display: swap; src: url('/fonts/barlow/barlow-500-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('/fonts/barlow/barlow-600-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow'; font-style: normal; font-weight: 700;
  font-display: swap; src: url('/fonts/barlow/barlow-700-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow Semi Condensed'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('/fonts/barlow-semi-condensed/barlowsc-600-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow Semi Condensed'; font-style: normal; font-weight: 700;
  font-display: swap; src: url('/fonts/barlow-semi-condensed/barlowsc-700-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow Semi Condensed'; font-style: normal; font-weight: 800;
  font-display: swap; src: url('/fonts/barlow-semi-condensed/barlowsc-800-latin.woff2') format('woff2');
}

/* ===== RESET & BASE ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size:15px; scroll-behavior: smooth; }
body {
  font-family: var(--font-ui);
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* Headings, card/page titles and big figures read the DISPLAY face; everything else (body copy,
   forms, tables) reads --font-ui. Both tokens default to the same Inter stack (declared in :root
   below), so this changes nothing for anyone who has not picked a typeface. The selector list is
   the same one [data-theme="warm"]'s own heading override already targets, plus the KPI/summary/
   metric figures (the tabular-nums rule a few lines up keys digits to a grid; this one is about
   the TYPEFACE, a different question). */
.content-area > h1, .content-area > h2, .page-title, .ov-title, .panel-head h3, .card-header h3,
.stat .v, .kpi-value, .summary-value, .metric-value {
  font-family: var(--font-display);
  font-stretch: var(--font-stretch-display);
}

/* Tabular (monospaced) figures so columns of numbers align on digit width —
   essential for a numbers-first reporting product. */
.kpi-value, .summary-value, .metric-value,
.form-table td, .form-table td input, .form-table td select,
.data-table tbody td, .totals-row td, .grand-total td,
td.num, .num, .tabular {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* ===== ACCESSIBILITY UTILITIES ===== */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: absolute; left: 8px; top: -60px; z-index: 2000;
  background: var(--primary); color: #fff; padding: 8px 16px;
  border-radius: var(--radius-sm); font-weight: 600; font-size:14px;
  transition: top 0.15s ease;
}
.skip-link:focus { top: 8px; outline: none; }
/* Visible keyboard focus ring (mouse clicks are unaffected via :focus-visible). */
a:focus-visible, button:focus-visible, [role="button"]:focus-visible,
.nav-item:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px;
}

/* ===== MODAL / CONFIRM DIALOG ===== */
/* ★ A CLOSED LAYER TAKES NO CLICK. The layer covers the whole screen and `.show` only fades it in, so
   a page that dropped `.show` and never hid the element left an invisible sheet over everything:
   no hover, no click, a page that looked alive and was dead (Accounting, 08-10 - every popup, after
   Cancel AND after a save; the hidden dialog's own buttons still took the clicks). Not shown now
   means the pointer goes straight through, so a forgotten close can never freeze a page again.
   Pointer only, NOT visibility: several pages focus a field in the same tick they open a dialog and
   add `.show` a frame later - a hidden layer would refuse that focus (Recruitment's link box). */
.modal-overlay {
  position: fixed; inset: 0; z-index: 1500; padding: 20px;
  background: rgba(15, 23, 42, 0.5);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transition: opacity 0.15s ease;
}
.modal-overlay.show { opacity: 1; pointer-events: auto; }
.modal-dialog {
  background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); width: 100%; max-width: 460px;
  max-height: calc((100vh - 40px) / var(--ui-zoom, 1)); overflow: auto;
  transform: translateY(8px) scale(0.98); transition: transform 0.15s ease;
}
.modal-overlay.show .modal-dialog { transform: none; }
.modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border); font-weight: var(--fw-bold); font-size: var(--text-lg); }
.modal-body { padding: 20px; font-size: var(--text-base); color: var(--text); line-height: 1.55;
  /* The body is set with textContent — deliberately, so a server string can never inject markup. That
     means a line break has to survive as a newline character, which it did not: a two-paragraph
     confirmation ran together into one wall of text. */
  white-space: pre-wrap; }
.modal-footer { padding: 16px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: var(--space-2); }
a { color: #3b82f6; text-decoration: none; }
button { cursor: pointer; font-family: inherit; }
input, select, textarea { font-family: inherit; font-size: inherit; }
table { border-collapse: collapse; width: 100%; }

/* ===== CSS VARIABLES ===== */
:root {
  --primary: #3b82f6;
  --primary-dark: #1d4ed8;
  --primary-light: #dbeafe;
  --sidebar-bg: #0f172a;
  --sidebar-hover: #1e293b;
  --sidebar-text: #94a3b8;
  --sidebar-active: #e2e8f0;
  --success: #10b981;
  --success-light: #d1fae5;
  --warning: #f59e0b;
  --warning-light: #fef3c7;
  --danger: #ef4444;
  --danger-light: #fee2e2;
  /* ★ LEVEL COLOURS (14-10, Vijay: "tank levels why its following theme colors it should follow standard level colors like
     tank levels designs"). A fuel tank's or a bunker's level is drawn in the plant's STANDARD colours on every skin - green
     in range, amber at the reorder (or high) mark, red past the minimum (or full) - never the skin's accent: on PlantX a
     full tank was lime, on Liquid teal. Declared here ONLY; no theme may redefine them (_e2e_appearance_axes.js). */
  --level-ok: #10b981; --level-warn: #f59e0b; --level-alarm: #ef4444;
  /* ★ ALARM PRIORITY COLOURS (18-10, PlantX Live, ISA-18.2 / ISA-101 - docs/PLANTX-LIVE.md §19). An alarm's priority is the plant's
     STANDARD colour on every skin, like a level - critical red, high orange, medium yellow, low violet - always beside its WORD, never
     colour alone. Each is a solid fill with its own ink, measured: white on #dc2626 4.8:1, slate on #f97316 5.4:1, slate on #facc15
     11:1, white on #7c3aed 5.7:1. Declared here ONLY; no theme may redefine them. */
  --alarm-critical: #dc2626; --alarm-critical-ink: #ffffff; --alarm-high: #f97316; --alarm-high-ink: #1e293b;
  --alarm-medium: #facc15; --alarm-medium-ink: #1e293b; --alarm-low: #7c3aed; --alarm-low-ink: #ffffff;
  /* ★ WHAT A PLANT DRAWING SHOWS (18-10, PlantX Live Phase 4 - public/js/live/renew.js, environment.js, docs/PLANTX-LIVE.md §32-§33).
     Water is drawn as water, a solar panel as a panel and the sun as the sun on every skin - like a level, a thing's own colour, never the
     skin's accent (on PlantX the accent is charcoal, and an effluent plant's tanks drawn in it read as tar). Declared here ONLY. */
  --matter-water: #0ea5e9; --matter-pv: #3f5888; --matter-pv-frame: #2c3f66; --matter-sun: #f59e0b;
  /* ★ PROCESS COLOUR (19-10, PlantX Live - public/js/live/hmi.js look 'process', docs/PLANTX-LIVE.md §13a). Vijay: "UI screens with
     colored equipments, line, pipes, wires, tanks, fans, motors, blowers, levels... top notch". A screen drawn in Process Colour paints
     each machine in its own colour on a control-room ground - a pump in machine blue, a motor in motor green, a conveyor's frame in
     machinery yellow, a furnace's refractory glowing when it runs - and every pipe in the colour of what it carries, after IS 2379
     (water sea green, steam aluminium, air sky blue, gas canary yellow, oils brown, acids and alkalis violet, fire red). Each is ONE
     base colour; hmi.js shades it (color-mix) into the dark, middle and light of a cylinder, a dome or a tube. A thing's own colour,
     like a level - declared here ONLY, never re-themed. */
  --proc-ground: #0c1524; --proc-ground-2: #16253d; --proc-grid: #1a2a43; --proc-box: #0f1c30; --proc-box-line: #2c4364;
  --proc-text: #e6edf6; --proc-muted: #93a4ba; --proc-edge: #04080e;
  --proc-run: #2ee66b; --proc-idle: #5f6f84; --proc-fault: #ff3b30; --proc-warn: #ffb020; --proc-glow: #ff7a1a; --proc-glow-2: #ffd27a;
  --proc-steel: #7d8ca0; --proc-tank: #a9b8c9; --proc-silo: #b0b9c4; --proc-mach: #2f78c4; --proc-mach2: #5c7a69; --proc-motor: #1f8a7a;
  --proc-fan: #4f7fa3; --proc-frame: #e0a516; --proc-rubber: #2a3039; --proc-hot: #7a2e17; --proc-shell: #8f5634; --proc-boiler: #aab3be;
  --proc-concrete: #9ca1a8; --proc-panel: #c4cacf; --proc-trafo: #5f7a90; --proc-copper: #c47a3d; --proc-gear: #4b525c; --proc-turbine: #47627d;
  --proc-hyd: #4c56b8; --proc-instr: #dbe2ea; --proc-white: #e3e9ef; --proc-pv: #3f5888; --proc-open: #1fa25a; --proc-shut: #c8382a;
  --proc-m-water: #2e9b6a; --proc-m-cooling: #2fb3c9; --proc-m-hotwater: #e3743a; --proc-m-steam: #c6ccd6; --proc-m-condensate: #8ec3e8;
  --proc-m-air: #7ecbf0; --proc-m-compressed: #4fa8e8; --proc-m-gas: #f5d21f; --proc-m-natgas: #e9b81f; --proc-m-oxygen: #dbe9ff;
  --proc-m-nitrogen: #8f9cb0; --proc-m-oil: #a87a43; --proc-m-fueloil: #6e4520; --proc-m-hydraulic: #7a63d6; --proc-m-acid: #6b3fa0;
  --proc-m-alkali: #8d4fb8; --proc-m-slurry: #8c7356; --proc-m-flue: #6f747d; --proc-m-dust: #a39684; --proc-m-coal: #4a4a54;
  --proc-m-ore: #8e3b2a; --proc-m-lime: #e4dfd2; --proc-m-hotmetal: #ff7a1a; --proc-m-slag: #c0591f; --proc-m-fire: #e02424;
  --proc-m-chemical: #4a4a52; --proc-m-product: #5aa9e6; --proc-m-power: #f0b429; --proc-m-hotair: #cf4a2e;
  /* the art's own matter (public/js/live/hmi-process.js): molten metal and its crust, an arc, a flame, glass, a hearth, cold steel */
  --proc-molten-1: #fffbe0; --proc-molten-2: #ffd84a; --proc-molten-3: #ff8c1a; --proc-molten-4: #c9400c; --proc-crust-1: #5c2f22; --proc-crust-2: #22120d;
  --proc-slag: #8a4a22; --proc-arc: #eef7ff; --proc-flame-1: #fff4c8; --proc-flame-2: #ffc23a; --proc-flame-3: #ff5f1a;
  --proc-window-1: #050a12; --proc-window-2: #132033; --proc-hearth-1: #160d0a; --proc-hearth-2: #3a1a12; --proc-earth: #2b2520;
  --proc-brick: #6b2c1d; --proc-mortar: #3a1911; --proc-cold-1: #727e8c; --proc-cold-2: #363e48; --proc-zinc: #d4dce6;
  --proc-chrome-1: #f2f5f8; --proc-chrome-2: #8e9aa7; --proc-alu: #d0d7df; --proc-steam: #e9eef4; --proc-smoke: #8b9099;
  --proc-cell: #1b2d5e; --proc-cell-line: #6a88d0; --proc-porcelain: #8c5b3b; --proc-carton: #c99a5b; --proc-cherenkov: #3fb6ff;
  --proc-wood: #a87650; --proc-pcb: #23733f; --proc-ink-c: #00a3dc; --proc-ink-m: #d6247b; --proc-ink-y: #f2cf00; --proc-ink-k: #2a2a2e; --proc-fabric: #5b6fd0;
  /* ★ THE DARK ENDS OF THE SEMANTIC FAMILY. These were WRITTEN AS IF THEY EXISTED — 19 views say
     `var(--success-dark, #059669)` and 3 say `var(--warning-dark, #b45309)` — and they were declared
     nowhere. Nothing looked broken, because every one carried a fallback; but a fallback for a token
     that does not exist is not a fallback, it is the value. So all 22 were pinned to a literal,
     silently exempt from the theme picker, and they had already begun to disagree with each other:
     admin/plants.ejs reached for #047857 while the other 18 used #059669.
     Declared here, they are one colour again and they follow the theme like everything else.
     Used for TEXT and BORDERS on a light tint — the pairing --success-light + --success-dark is
     what a "done" pill is made of. */
  /* ★ 3.77:1 AT #059669, MEASURED AS INK ON A WHITE CARD — under the 4.5 text floor on every page
     that prints a green figure ("APPROVED 0", the DOA's Approve verb, Tech Directory's source
     chips, the PM due-in dates). #047857 is 5.48:1, and console-day already declared exactly that
     value for the same reason. --success stays #10b981: it is the DOT and the BAR, not the ink. */
  --success-dark: #047857;
  /* ★ THE ACCENT AS INK, WHICH IS NOT THE ACCENT. --primary (#3b82f6) measured 3.68:1 on a white
     card — under the 4.5 text floor — wherever a page printed a link or a figure in the accent.
     --primary-dark is the right ink on every theme that darkens its accent, and every theme that
     INVERTS (black, console, glass-dark) already declares --primary-dark as its bright value, so
     one indirection is correct in both halves. The exception is Liquid Glass, whose --primary-dark
     is an indigo GRADIENT END (#3730a3) on a teal page: pointing its links at that would have
     recoloured them. Its own --primary is already 5.36:1, so it uses that. */
  --primary-ink: var(--primary-dark);
  /* ★ AND THE DANGER EQUIVALENT. --danger-dark is an INK token: every dark theme flips it to a
     light red (#f87171, #ffa0a0) so it can be read ON a dark pane. Using it as the FILL under
     white text therefore inverts — measured on the notification bell with 75 unread: white on
     #ffa0a0 is 1.95:1. --danger-solid is the other thing: a fill that carries white ink, and it
     does NOT flip, because the pair is white-on-red in both halves. 4.83:1 with white, and 5.25:1
     against the darkest ground so the chip still separates from it. */
  --danger-solid: #dc2626;
  /* ★ AND THE SAME PROBLEM UPSIDE DOWN: a FLAT --primary fill carrying WHITE ink is also 3.68:1,
     so #3b82f6 fails as ink AND as a surface — it is a border, a dot and a bar, never either side
     of a text pair. Measured on the weighbridge tab strip, the calendar's today chip, the group
     help CTA and the DPR print button. Same indirection as --primary-ink, same reasoning about
     themes that invert; the Glass pair pins it because glass-dark's --primary-dark IS #3b82f6. */
  --accent-solid: var(--primary-dark);
  /* the primary-button gradient's start — --primary on the default look; a theme whose --primary is
     too pale under white text points it at --accent-solid (see the Forest/Black line further down) */
  --btn-grad-from: var(--primary);
  /* channels the glass themes answer (a lip highlight, a pane fill, the popover surface); declared here
     as the plain-theme values so an unscoped rule reading them never reads nothing */
  --gl-lip: transparent;
  --gl-f1: transparent;
  --menu-bg: var(--card);
  --warning-dark: #b45309;
  --danger-dark: #dc2626;
  --bg: #f8fafc;
  --card: #ffffff;
  --text: #292929;
  --text-muted: #737373;
  --border: #e7e7e7;
  /* Referenced by .ds-opt-hint (and now the document-number treatments) but never defined, so every
     use fell through to its inline fallback. A document number is read character by character, which
     is the one place a page is allowed to leave Inter. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* ★ THE TYPEFACE, AS TWO TOKENS — a per-user choice (Appearance → Typeface, core/appearance.js
     FONTS), orthogonal to theme. --font-ui is EVERYTHING: body copy, forms, tables. --font-display
     (+ its width axis, --font-stretch-display) is read only by the handful of selectors just above
     this file's RESET & BASE section — headings, card/page titles, big figures. Both default to the
     exact stack `body` used literally before this existed, so nobody's screen changes until they
     pick a face; [data-font="…"] blocks below (grouped with the @font-face rules at the top of this
     file) override them. */
  --font-ui: 'Inter', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: var(--font-ui);
  --font-stretch-display: 100%;
  /* ★ THE PRIMARY BUTTON'S INK. #fff on every theme today — a token only because the PlantX skin's
     gold gradient needs a dark ink under it (the same reasoning Black/Console already apply to the
     FILL via --accent-solid; this is the mirror case, the TEXT). Read by .btn-primary/.ox-hero/
     .ds-btn; every existing theme leaves this at the default and is unaffected. */
  --on-primary: #fff;
  /* Status TINT pairs (bg + readable fg) — used by .badge-* and every inline status pill.
     They FLIP in [data-theme="black"] so a pill stays legible on a dark page. Always use the
     token pair, never a raw #fef3c7/#065f46 literal, or the pill freezes light on dark. */
  --tint-warn-bg: #fef3c7;  --tint-warn-fg: #92400e;
  /* A FOURTH STEP. Risk and priority both run Low → Medium → High → Critical, and with only warn and
     danger the middle two had to share amber — two different decisions painted identically. `hot` is
     the orange between them, so a four-step ramp can actually be shown as four steps. */
  --tint-hot-bg: #ffedd5;   --tint-hot-fg: #9a3412;   --tint-hot-dot: #f97316;
  --tint-danger-bg: #fef2f2; --tint-danger-fg: #991b1b;
  --tint-ok-bg: #d1fae5;    --tint-ok-fg: #065f46;
  --tint-info-bg: #eff6ff;  --tint-info-fg: #1d4ed8;
  --tint-neutral-bg: #f4f4f4; --tint-neutral-fg: #475569;
  --th-bg: #f4f4f4;         /* table header strip */
  /* THE PROJECT HUB (V2 part H1, public/js/plan/project-hub.js): whose turn a package is - ours or the party's - a clock
     past its date, our own notes beside a party's, a sealed figure. Named by what they mean, each the token it stands on,
     so every theme reaches them. */
  --hub-ball-owner: var(--primary); --hub-ball-party: var(--success); --hub-clock-red: var(--danger);
  --hub-ours: var(--warning-light); --hub-sealed: var(--text-hint);
  /* ★ ELEVATION IS FOUR STEPS. Measured before this pass: 202 box-shadow declarations carrying
     97 DISTINCT outer-shadow values — not a system, but 97 surfaces each tuned in isolation,
     which is what the eye reads as "assembled" rather than "designed". The references put beside
     this app use three shadows on a whole screen.
     Four steps, named by what a surface IS:
       el-1 resting · a card on the page      el-3 raised  · a dropdown, above the page
       el-2 lifted  · hover, a chip           el-4 overlay · a modal, above everything
     ★ NOT COLLAPSED, ON PURPOSE: focus rings (`0 0 0 Npx` carries a semantic colour and is an
     accessibility signal), accent glows (a blue bloom under a primary button MEANS something),
     inset lips (an edge, not a cast), and anything inside @keyframes or an @supports condition —
     a shadow in a keyframe is motion, and a shadow in a feature query is a CONDITION. Ignoring
     that last one corrupted the stylesheet from line 2878 on the first attempt at this pass. */
  --el-1: 0 1px 2px rgba(15,23,42,.05), 0 1px 3px rgba(15,23,42,.09);
  --el-2: 0 2px 4px rgba(15,23,42,.06), 0 4px 10px rgba(15,23,42,.08);
  --el-3: 0 4px 10px rgba(15,23,42,.07), 0 12px 26px rgba(15,23,42,.12);
  --el-4: 0 8px 18px rgba(15,23,42,.09), 0 32px 64px rgba(15,23,42,.20);
  --shadow: var(--el-1);
  --shadow-md: var(--el-3);
  --shadow-lg: var(--el-4);
  /* Read unscoped by .nav-item.active, and declared by all twelve themes but never by the base.
     That worked only because every theme happened to remember it; the thirteenth would have had a
     nav with no active state and no error anywhere. The base owns the default, themes override. */
  --nav-active-bg: #eff6ff;
  /* ★ FOUR CORNERS. Measured first: 256 border-radius declarations across 36 DISTINCT values —
     3,4,6,7,8,9,10,11,12,14,16,18,20,24px all in use. That is not one decision repeated, it is
     36 taken separately, and a reader does not see a system in it, only near-misses.
       radius-sm  · chip, input, small control     radius-lg   · dialog, board, large surface
       radius     · card, panel (THE theme's corner) radius-pill · a pill
     ★ THE BOUNDARY MATTERS AND I GOT IT WRONG FIRST TIME. Putting 10px in the same bucket as
     12px gave chips a card's corner, so on glass (radius 16) a chip became as round as the card
     holding it and the size hierarchy went. 10px belongs with the small controls.
     ★ 50% IS NOT ON THE SCALE — a circle is geometry, not styling; an avatar or status dot must
     stay round whatever the theme's corner is. `0` and `inherit` likewise. */
  --radius: 12px;
  --radius-lg: 18px;
  --radius-pill: 999px;
  --radius-sm: 8px;
  --sidebar-width: 260px;
  --topbar-height: 60px;
  --transition: 0.2s ease;

  /* ============================================================================================
     ★ THE SCALES. Declared here and read by name — never a raw px in a rule.
     This block was already half-present and almost entirely UNREAD: the seven --space-* below have
     existed for months and are used ELEVEN times in 5,600 lines, while the stylesheet uses THIRTY-TWO
     distinct spacing values (every integer from 1px to 20px, then 22 · 24 · 26 · 28 · 32 · 34 · 36 ·
     40 · 44 · 48 · 60 · 64) and EIGHTY-ONE distinct font sizes. A scale nothing obeys is not a scale,
     and that is precisely how a theme ended up with a 10.5px button beside a 12.5px nav item.
     `theme-raw-px` in db/design-audit.js now fails a raw px size inside any [data-theme] block, so a
     new theme cannot repeat it. The 140 existing views are NOT migrated and do not have to be.
     ============================================================================================ */

  /* Spacing scale (4px base) — VALUES UNCHANGED, eleven rules already read them. */
  --space-05: 2px;          /* the half-step: a hairline gap inside a pill */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;          /* completes the grid: 4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 */

  /* ★ TYPE, BY ROLE AND IN PIXELS — the canonical scale.
     By ROLE because a name you pick from ("this is a micro-label") is a scale, while a number you
     pick ("13.5 looks about right") is 81 numbers. In PIXELS because rem is measured against
     `html { font-size:15px }` at the top of this file, so any theme that moves the basis silently
     re-scales every rem written against the old one — which is exactly what produced buttons smaller
     than the nav items beside them. data-size still scales all of it, because that axis zooms.
     Nine values. If a new size seems necessary, the role is missing, not the number. */
  --fs-label:  9px;          /* small-caps micro-label: BED TEMPERATURE, a column head */
  --fs-meta:  11px;          /* mono: a reference, a timestamp, an age */
  --fs-aux:   12px;          /* a chip, a secondary line, a hint */
  --fs-body:  13px;          /* a list row, a field value, table text */
  --fs-ui:    14px;          /* the base: prose, inputs, buttons */
  --fs-head:  15px;          /* a card or section heading */
  --fs-lede:  17px;          /* a standfirst under a page title */
  --fs-title: 21px;          /* the page title */
  --fs-value: 25px;          /* a reading, a summary figure */

  /* Legacy type scale — SUPERSEDED by --fs-* above, kept because eight rules still read it and
     changing a rem value here would move existing pages by a pixel or two for no benefit. Do not
     add readers; use --fs-* in anything new. */
  --text-xs: 0.72rem;
  --text-sm: 0.82rem;
  --text-base: 0.9rem;
  --text-md: 1rem;
  --text-lg: 1.15rem;
  --text-xl: 1.3rem;
  --text-2xl: 1.6rem;

  /* ★ THE DOA VERB COLOURS — recommend / confirm / inform.
     These were declared on `:root` inside views/admin/plants.ejs, which is three defects in one
     line: a GLOBAL token defined by a single page, so `public/js/core/select.js` fell back to its
     own hardcoded hex on every other page that draws a verb pill; light-only values, so they stayed
     pale lilac and pale cyan on the dark themes; and a view writing into :root at all, which is the
     shared-component rule pointing the other way. Values are unchanged, so nothing on the light
     themes moves — they simply live where every theme can reach them now. */
  /* ★ THE DARKER PAIR, NOT THE LIGHTER ONE. Moving these out of the page :root was right, but I
     carried plants.ejs's values — and select.js had been rendering its own darker fallbacks
     everywhere else. Measured: recommend went 4.83:1 → 3.59, confirm 4.79 → 3.29. A pass turned into
     a fail on a 363-cell grid whose entire job is telling three verbs apart. These are select.js's. */
  --verb-rec: #7c3aed;  --verb-rec-bg: #f3e8ff;
  --verb-con: #0e7490;  --verb-con-bg: #cffafe;
  --verb-inf: #be123c;  --verb-inf-bg: #ffe4ec;

  /* ★ READ ON ALL TWELVE THEMES, DECLARED ON FOUR. `--border-strong` was set only by white, soft,
     console and console-day; on the other eight every reader fell through to a frozen #cbd5e1 —
     measured at 12.37:1 against the black theme's card, a near-white line on near-black. This is the
     --success-dark defect verbatim, and it survived because the audit rule asks "declared anywhere",
     not "resolvable on every theme". Declared here so the eight themes inherit it. */
  --border-strong: #d4d4d4;
  /* Same shape, smaller blast radius: seven bare reads, undefined on eight themes, and it collapses
     to --text — so the secondary tier silently stops existing rather than looking wrong. */
  --text-secondary: #404040;

  /* Font weights */
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;
  --fw-extrabold: 800;
}

/* [data-font="…"] — sets --font-ui / --font-display / --font-stretch-display (declared in :root
   above, with their @font-face rules at the very top of this file). Placed AFTER :root on purpose:
   `:root` and `[data-font="x"]` carry the SAME specificity (0,1,0), so on the tie the later rule in
   the file wins — exactly how every `[data-theme="x"]` block below already overrides :root's
   tokens. Every stack keeps Inter's own fallback tail, so a complex script (Telugu, Hindi, Arabic —
   none of these faces carry those glyphs) still falls through to the system/Noto font exactly as it
   does today; only the Latin glyphs change face. 'inter' is written out explicitly, byte for byte
   the same as the :root default, so picking it back is always a no-op. */
[data-font="inter"] {
  --font-ui: 'Inter', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: var(--font-ui); --font-stretch-display: 100%;
}
/* Saira — wide and engineered, echoes the PLANTX wordmark's own lettering. A true variable font
   over weight AND width, so the display face is the SAME family at a wider instance via
   font-stretch, not a second font file. */
[data-font="saira"] {
  --font-ui: 'Saira', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: var(--font-ui); --font-stretch-display: 112%;
}
/* Archivo — industrial grotesque, built for dense tables. Same variable-width trick as Saira. */
[data-font="archivo"] {
  --font-ui: 'Archivo', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: var(--font-ui); --font-stretch-display: 118%;
}
/* IBM Plex Sans — engineering-drawing feel, one family for both roles. */
[data-font="plex"] {
  --font-ui: 'IBM Plex Sans', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: var(--font-ui); --font-stretch-display: 100%;
}
/* Sora + Manrope — a geometric display face over a softer, rounded body text; two families, no
   width axis on either, so the stretch stays 100%. */
[data-font="sora"] {
  --font-ui: 'Manrope', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: 'Sora', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-stretch-display: 100%;
}
/* Barlow + Barlow Semi Condensed — from road signage, compact figures; the display face is the
   condensed sibling, a separate family rather than a stretch axis (Barlow carries none). */
[data-font="barlow"] {
  --font-ui: 'Barlow', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: 'Barlow Semi Condensed', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-stretch-display: 100%;
}

/* ===== LOGIN PAGE ===== */
#login-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh/var(--ui-zoom,1));
  background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
  position: relative;
  overflow: hidden;
}
#login-page::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background:
    repeating-linear-gradient(90deg, transparent, transparent 100px, rgba(59,130,246,0.03) 100px, rgba(59,130,246,0.03) 101px),
    repeating-linear-gradient(0deg, transparent, transparent 100px, rgba(59,130,246,0.03) 100px, rgba(59,130,246,0.03) 101px);
  pointer-events: none;
}
#login-page::after {
  content: '';
  position: absolute;
  bottom: -50px;
  left: 0; right: 0;
  height: 200px;
  background:
    linear-gradient(90deg,
      transparent 10%, rgba(59,130,246,0.05) 10%, rgba(59,130,246,0.05) 10.5%,
      transparent 10.5%, transparent 20%, rgba(59,130,246,0.08) 20%, rgba(59,130,246,0.08) 22%,
      transparent 22%, transparent 35%, rgba(59,130,246,0.06) 35%, rgba(59,130,246,0.06) 36%,
      transparent 36%, transparent 50%, rgba(59,130,246,0.1) 50%, rgba(59,130,246,0.1) 53%,
      transparent 53%, transparent 65%, rgba(59,130,246,0.07) 65%, rgba(59,130,246,0.07) 66%,
      transparent 66%, transparent 80%, rgba(59,130,246,0.05) 80%, rgba(59,130,246,0.05) 81%,
      transparent 81%
    );
  pointer-events: none;
}
.login-card {
  background: var(--card);
  border-radius: var(--radius-lg);
  padding: 48px 40px;
  width: 420px;
  max-width: 90vw;
  box-shadow: var(--el-4);
  position: relative;
  z-index: 1;
}
.login-brand {
  text-align: center;
  margin-bottom: 32px;
}
.login-brand .logo-icon {
  width: 64px; height: 64px;
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  border-radius: var(--radius-lg);
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto 16px;
  color: #fff;
  font-size:25px;
  font-weight:600;
}
.login-brand h1 { font-size:25px; font-weight:600; color: var(--text); }
.login-brand p { color: var(--text-muted); font-size:14px; margin-top: 4px; }
.login-error {
  background: var(--danger-light);
  color: var(--danger);
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  font-size:14px;
  margin-bottom: 16px;
  display: none;
}

/* ===== FORM INPUTS (Global) ===== */
.form-group { margin-bottom: 16px; }
.form-group label {
  display: block;
  font-weight: 600;
  font-size:14px;
  color: var(--text);
  margin-bottom: 6px;
}
/* Compact field label for dense admin forms — replaces repeated inline label styles. */
/* Compact variant for dense builder tables — same tokens/focus treatment, smaller metrics. */
.form-control.form-control-xs { padding: 4px 8px; font-size:13px; border-radius: var(--radius-sm); }
/* A field label is READ, not glanced at — 0.72rem put it at ~11.5px in muted grey, which is below
   what anyone should have to squint at on a plant floor. --text-sm keeps it compact without that. */
.form-label-sm {
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--fw-bold);
  color: var(--text-muted);
  margin-bottom: 4px;
}
/* THE HINT UNDER A FIELD. Seven views use .form-hint and nothing defined it, so it inherited the body
   size and rendered LARGER than the label it belongs to — the explanation shouting over the thing it
   explains. The scale under a field descends: label .82 (bold, muted) → value .9 → hint .76. */
.form-hint {
  font-size:12px;
  line-height: 1.45;
  color: var(--text-hint, var(--text-muted));
  margin-top: 5px;
  max-width: 62ch;
}
.field { margin-bottom: var(--space-3); }
.form-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-3); }
.form-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); }
@media (max-width: 640px) { .form-grid-2, .form-grid-3 { grid-template-columns: 1fr; } }
.form-control {
  width: 100%;
  padding: 8px 12px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  font-size:15px;
  color: var(--text);
  background: #fff;
  transition: border-color var(--transition), box-shadow var(--transition);
  outline: none;
}
.form-control:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}
.form-control::placeholder { color: #94a3b8; }
select.form-control { appearance: auto; }
textarea.form-control { resize: vertical; min-height: 80px; }
.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 12px;
}
.form-row-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.form-row-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.form-row-5 { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
.form-row-6 { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; }

/* ===== TABULAR FORM STYLES ===== */
.form-table-wrap { overflow-x: auto; margin-bottom: 16px; border-radius: var(--radius); border: 1px solid var(--border); }
.form-table {
  width: 100%;
  border-collapse: collapse;
  margin: 0;
}
.form-table th {
  background: var(--th-bg);
  padding: 8px 12px;
  text-align: left;
  font-weight: 600;
  /* --fs-body, which IS 13px here, so this is the same size it has always been on the default
     theme — but it now moves with a theme that sets its own scale, instead of pinning while the
     cells below it drift. See the note on `td`. */
  font-size: var(--fs-body);
  color: var(--text-muted);


  border: 1px solid var(--border);
  white-space: nowrap;
}
.form-table td {
  padding: 4px 6px;
  border: 1px solid var(--border);
  vertical-align: middle;
  /* ★ THE CELL HAD NO SIZE AT ALL, so it inherited the ROOT — 15px normally and 16px on Liquid
     Glass, which sets --fs-root:16px. The header beside it was pinned at 13px, so a table's
     headings were SMALLER than its own data, and a cell only matched its neighbours when its
     content happened to be wrapped in something that did set a size. Measured in one row of the
     Stock Ledger: the header 13px, the timestamp and the item code 16px, the store and the
     reference 13.12px, the sub-reference 12px — four sizes, which is what reads as "different
     fonts" even though every one of them is Inter. One token for the whole table. */
  font-size: var(--fs-body);
}
.form-table td input,
.form-table td select {
  width: 100%;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size:14px;
  background: transparent;
  transition: all 0.15s;
}
.form-table td input:focus,
.form-table td select:focus {
  border-color: var(--primary);
  background: white;
  outline: none;
  box-shadow: 0 0 0 2px var(--primary-light);
}
.form-table td input:hover,
.form-table td select:hover {
  background: var(--bg);
}
.form-table td input[readonly] {
  background: var(--tint-neutral-bg);
  color: var(--primary-dark);
  font-weight: 600;
}
.form-table td.row-label {
  background: var(--bg);
  font-weight: 600;
  font-size:14px;
  color: var(--text);
  padding: 8px 12px;
  white-space: nowrap;
  min-width: 140px;
}
.form-table tr:hover td {
  background: var(--bg);
}
.form-table tr:hover td.row-label {
  background: var(--th-bg);
}
.form-table tr.section-header:hover td {
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  color: white;
}
.form-table tr.sub-header:hover td {
  background: var(--border, #e2e8f0);
  color: var(--text);
}
.form-table tr.totals-row:hover td {
  background: var(--primary-light, #dbeafe);
}
.form-table .section-header td {
  background: linear-gradient(135deg, var(--accent-solid), var(--primary-dark));   /* white ink needs the solid, not the brand fill */
  color: white;
  font-weight:600;
  font-size:14px;
  padding: 8px 12px;


}
.form-table .sub-header td {
  background: var(--border, #e2e8f0);
  font-weight: 600;
  font-size:13px;
  color: var(--text);
  padding: 6px 12px;


}
.form-table .totals-row td {
  background: var(--primary-light, #dbeafe);
  font-weight:600;
  border-top: 2px solid var(--primary);
}
.form-table .totals-row input {
  font-weight:600;
  color: var(--primary-dark);
}

/* ===== BUTTONS ===== */
.btn {
  background: var(--card);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px 20px;
  border: none;
  border-radius: var(--radius-sm);
  font-weight: 600;
  font-size:14px;
  transition: all var(--transition);
}
/* ★ A COLOUR UNDER THE GRADIENT, so this button can never become white-on-white.
   `background:` with a gradient sets background-IMAGE and leaves background-COLOR transparent. Any
   theme that overrides it with a value the browser rejects — a var() that resolves to nothing, a
   token renamed out from under it — drops the image and leaves white text on whatever is behind,
   which on a white modal footer is an invisible button. It is not a hypothetical: the Save control
   on the Appearance dialog was reported missing, and "there but unpaintable" is indistinguishable
   from "absent" to the person looking at it.
   background-color first, gradient second: if the gradient survives it covers the colour, and if it
   does not the button is still a solid blue slab with legible white text. */
.btn-primary {
  background-color: var(--primary-dark, #1d4ed8);
  /* --btn-grad-from is --primary on the default look (the 135° #3b82f6→#1d4ed8 gradient CLAUDE.md
     specifies). A theme whose --primary is too pale to sit under white text — Forest's #10b981,
     Black's #60a5fa — points it at --accent-solid instead, and keeps its gradient on a solid it can read. */
  background-image: linear-gradient(135deg, var(--btn-grad-from, var(--primary)), var(--primary-dark));
  /* --on-primary: #fff on every theme today — a token only because PlantX's gold gradient needs a
     dark ink under it. Every other theme reads the fallback and is byte-for-byte unaffected. */
  color: var(--on-primary, #fff);
}
.btn-primary:hover { box-shadow: 0 4px 12px rgba(59,130,246,0.4); transform: translateY(-1px); }
.btn-secondary { background: var(--bg); color: var(--text); border: 1.5px solid var(--border); }
.btn-secondary:hover { background: var(--border); }
.btn-success { background: var(--success); color: #fff; }
.btn-success:hover { background: #059669; }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { background: #dc2626; }
.btn-warning { background: var(--warning); color: #fff; }
.btn-sm { padding: 6px 12px; font-size:13px; }
.btn-lg { padding: 12px 24px; font-size:15px; }
.btn-block { width: 100%; }
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; }

/* ===== APP SHELL ===== */
#app { display: none; }
.app-layout { display: flex; min-height: calc(100vh/var(--ui-zoom,1)); }

/* ===== SIDEBAR ===== */
.sidebar {
  width: var(--sidebar-width);
  background: #fff;
  color: #475569;
  border-right: 1px solid var(--border);
  position: fixed;
  top: 0; left: 0; bottom: 0;
  display: flex;
  flex-direction: column;
  z-index: 100;
  transition: width 0.25s ease, transform 0.3s ease;
  overflow-y: auto;
  overflow-x: hidden;
}
.sidebar-brand {
  /* Tighter at the BOTTOM than the top. The brand's 18px, the nav's own 10px, the first section's
     3px and the group button's 9px used to stack into 40px of empty rail between the company name
     and the first heading — invisible while the brand was three lines tall, obvious once it was one. */
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--border);
}
.sidebar-brand .brand-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
/* The company name WRAPS. It briefly did not: an ellipsis rule was added here while the collapse
   button was being tried inside this row, to stop a long name pushing it out. The button ended up on
   the sidebar's edge instead, and the leftover rule squeezed "Unified Reporting Suite" onto one line —
   which halved the row's height and left the 38px logo looking oversized beside it. */
.sidebar-brand .brand-icon {
  width: 38px; height: 38px;
  background: linear-gradient(135deg, var(--accent-solid), var(--primary-dark));
  border-radius: var(--radius-sm);
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight:600; font-size:15px;
  flex-shrink: 0;
  box-shadow: var(--el-2);
}
.sidebar-brand h2 {
  font-size:17px;
  font-weight:600;
  color: var(--text);
  line-height: 1.2;
}
.sidebar-brand .brand-sub {
  font-size:11px;
  color: var(--text-muted);
  margin-top: 2px;


}
.sidebar-nav { flex: 1; padding: 2px 0 8px; }   /* see .sidebar-brand — the top gap was doubled up */

/* ===== Sidebar account menu (top, below brand) + footer settings/avatar (bottom) ===== */
.side-usermenu { position: relative; display: flex; align-items: center; gap: 8px; margin: 12px 14px 4px; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--card); cursor: pointer; transition: background 0.13s ease; }
.side-usermenu:hover { background: var(--bg); }
.side-usermenu .sum-name { font-size:13px; font-weight: 600; color: var(--text); line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.side-usermenu .sum-sub { font-size:11px; color: var(--text-muted); margin-top: 1px; }
.side-usermenu .sum-caret { margin-left: auto; color: var(--text-muted); flex: none; }
.side-user-menu { position: absolute; left: 0; right: 0; top: calc(100% + 4px); background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); box-shadow: var(--shadow-lg); padding: 6px; display: none; z-index: 60; }
.side-user-menu.show { display: block; }

.side-foot { padding: 8px 12px 12px; border-top: 1px solid var(--border); }
.side-set { display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px 12px; border: none; background: none; border-radius: var(--radius-sm); color: #475569; font-size:14px; font-weight: 500; cursor: pointer; text-align: left; font-family: inherit; transition: background 0.13s ease; }
.side-set:hover { background: var(--bg); color: var(--text); }
.side-set svg { width: 17px; height: 17px; stroke: #94a3b8; fill: none; stroke-width: 1.8; flex: none; }
.side-set:hover svg { stroke: var(--text-muted); }
.side-avatar { display: flex; align-items: center; gap: 8px; padding: 8px 12px 2px; }
.side-avatar .sa-av { width: 30px; height: 30px; border-radius: 50%; background: var(--primary-light); color: var(--primary-dark); display: flex; align-items: center; justify-content: center; font-size:12px; font-weight:600; flex: none; }
.side-avatar .sa-name { font-size:13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.side-avatar .sa-role { font-size:11px; color: var(--text-muted); }

.app-layout.collapsed .side-usermenu { display: none; }
.app-layout.collapsed .side-set-label, .app-layout.collapsed .side-avatar-meta { display: none; }
.app-layout.collapsed .side-set { justify-content: center; padding: 8px 6px; }
.app-layout.collapsed .side-avatar { justify-content: center; padding: 8px 6px 2px; }
.nav-section {
  padding: 0 12px;
  margin-bottom: 4px;
}
/* GROUP HEADING — a quiet label with a chevron, not a filled band.
   It used to be a tinted bar built from --primary, edge to edge, once per group. With eleven groups
   the rail turned into eleven coloured stripes competing with the nav items themselves, and colour
   in the nav is exactly what the sidebar design says it does not want ("the nav carries NO accent
   colour, so colour lives in the content"). A heading's job is to be findable when scanned and
   invisible when not — muted, small, no fill. The chevron carries the new meaning: this opens. */
/* An OPEN group is one block: heading and its children share a tinted panel, so you can see at a
   glance where a group starts and stops. Closed groups are bare headings, which is what makes a
   collapsed rail read as a list of drawers. */
.nav-section { border-radius: var(--radius); margin: 3px 0; transition: background 0.15s ease; }
.nav-section.is-open { background: color-mix(in srgb, var(--text) 4.5%, transparent); padding-bottom: 4px; }
.nav-group {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 12px 6px;
  margin: 4px 0 1px;
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  font-size:12px;
  font-weight:600;


  /* 72%, not 55%: a 12px uppercase label at 55% of --text measured 3.4:1 on a white rail — below
     the 4.5 a small label needs. 72% keeps it visibly quieter than an item and reads. */
  color: color-mix(in srgb, var(--text) 72%, transparent);
  border-radius: var(--radius);
  transition: color 0.13s ease;
}
.nav-group:hover { color: var(--text); }
/* An OPEN group takes the theme colour, a closed one stays muted — so the accent marks which
   drawers are actually open and the heading reads as active rather than as a static label.
   ★ --primary-ink, not --primary: the brand FILL (#10b981 on Forest, #3b82f6 on Soft) measured
   2.0 and 2.6:1 as 12px ink on a light rail. The ink token is the same hue made readable. */
.nav-section.is-open > .nav-group { color: var(--primary-ink, var(--primary-dark)); }
.nav-section.is-open > .nav-group .nav-caret { opacity: 1; }
.nav-group-label { flex: 1 1 auto; text-align: left; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Points down when the group is open, right when it is closed — the standard disclosure direction. */
.nav-caret { flex: none; opacity: .8; transition: transform 0.18s ease; transform: rotate(90deg); }
.nav-group[aria-expanded="false"] .nav-caret { transform: rotate(0deg); }
.nav-section:first-child .nav-group { margin-top: 0; }
/* Children sit in from the heading, so the hierarchy is visible without reading the words.
   The open/close is ANIMATED, so everything below a group slides rather than jumping: the row
   collapses from 1fr to 0fr and the inner wrapper clips. This is the grid trick rather than a
   max-height guess, because it animates to the content's REAL height — with a guessed max-height
   a four-item group and a one-item group move at visibly different speeds, and anything taller
   than the guess snaps at the end. display:none cannot be transitioned at all, which is why the
   closed state is a class and `inert` (set in main.ejs) rather than the hidden attribute. */
.nav-group-items {
  padding-left: 12px;
  padding-right: 4px;
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 0.26s cubic-bezier(0.4, 0, 0.2, 1),
              opacity 0.18s ease 0.04s;
}
.nav-group-inner { overflow: hidden; min-height: 0; }
.nav-section:not(.is-open) > .nav-group-items {
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows 0.24s cubic-bezier(0.4, 0, 1, 1), opacity 0.12s ease;
}
@media (prefers-reduced-motion: reduce) {
  .nav-group-items, .nav-section:not(.is-open) > .nav-group-items { transition: none; }
}

/* Kept for any rail still rendering the old markup (the Units group builds its own header). */
.nav-section-title {
  font-size:11px;
  font-weight:600;


  color: color-mix(in srgb, var(--text) 55%, transparent);
  padding: 8px 12px 6px;
  margin: 6px 0 1px;
}
.nav-section:first-child .nav-section-title { margin-top: 0; }
.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  color: #475569;
  font-size:15px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.13s ease, color 0.13s ease;
  margin: var(--space-05) 0;
  white-space: nowrap;
}
.nav-item:hover { background: var(--tint-neutral-bg); color: var(--text); }
.nav-item.active, .nav-item.nav-parent.expanded { background: var(--nav-active-bg, #eff6ff); color: var(--primary-dark); font-weight: 600; box-shadow: inset 3px 0 0 var(--primary); }
.nav-item .nav-icon {
  width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  color: #94a3b8;
}
.nav-item:hover .nav-icon { color: #64748b; }
.nav-item.active .nav-icon, .nav-item.nav-parent.expanded .nav-icon { color: var(--primary); }
.nav-item .nav-icon svg { width: 19px; height: 19px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* READ-ONLY badge — a small eye pinned to the corner of the module's own icon. It sits OUTSIDE the
   row's flex flow, so it takes no width from the label: a badge in the row cost ~24px and truncated
   "Maintenance Requests", which is the opposite of what a qualifier should do. The disc carries the
   sidebar's own background so the eye stays legible over whatever the icon is.
   The icon hydrator turns the <i> into a span>svg, so sizing targets that span. */
.nav-item .nav-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item .nav-icon { position: relative; }
.nav-ro {
  position: absolute; right: -5px; bottom: -4px;
  width: 13px; height: 13px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--card); color: var(--text-muted);
  box-shadow: 0 0 0 1.5px var(--card);
}
.nav-item:hover .nav-ro, .nav-item.active .nav-ro { color: var(--text); }
.nav-ro > span, .nav-ro svg { width: 9px; height: 9px; stroke: currentColor; fill: none; stroke-width: 2.4; }

/* Unit group rows (admin sidebar): factory icon + truncating unit name + rotating chevron.
   Markup keeps .nav-item, so every theme's tokenized nav colors apply automatically —
   only the differences live here. Compound selectors so these beat the later
   single-class .nav-item theme overrides. */
.nav-item.nav-parent { font-weight: 500; cursor: pointer; user-select: none; }
.nav-item.nav-parent .nav-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* A unit row is a group INSIDE a group, so it carries the same chevron and turns the same accent
   when open as a top-level heading does — a nested group that looks different from its parent is
   exactly where the hierarchy stops being readable. Rotation matches: right closed, down open. */
.nav-item.nav-parent .nav-caret { margin-left: auto; opacity: .7; transform: rotate(0deg); }
.nav-item.nav-parent.expanded .nav-caret { opacity: 1; transform: rotate(90deg); }
/* The child group animates exactly like a top-level one — same grid technique, same curves — so
   opening a unit slides its pages down instead of snapping them in. It is never display:none, which
   is why the closed state is a class plus `inert` rather than the old inline style.display. */
.nav-sub {
  margin-left: 14px;
  padding-left: 6px;
  border-left: 1px solid var(--border);
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows 0.24s cubic-bezier(0.4, 0, 1, 1), opacity 0.12s ease;
}
.nav-sub.is-open {
  grid-template-rows: 1fr;
  opacity: 1;
  transition: grid-template-rows 0.26s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.18s ease 0.04s;
}
.nav-sub-inner { overflow: hidden; min-height: 0; }
@media (prefers-reduced-motion: reduce) { .nav-sub, .nav-sub.is-open { transition: none; } }
.nav-sub .nav-item { font-size:14px; padding: 6px 12px; }
.sidebar-footer {
  padding: 16px 20px;
  border-top: 1px solid var(--border);
}
.sidebar-user {
  display: flex;
  align-items: center;
  gap: 8px;
}
.sidebar-user .user-avatar {
  width: 34px; height: 34px;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-weight:600;
  font-size:14px;
  flex-shrink: 0;
}
.sidebar-user .user-info { flex: 1; min-width: 0; }
.sidebar-user .user-name { color: var(--text); font-weight: 600; font-size:14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar-user .user-role { color: var(--text-muted); font-size:11px; }
.btn-logout {
  margin-top: 10px;
  width: 100%;
  padding: 6px;
  background: rgba(239,68,68,0.1);
  color: #f87171;
  border: 1px solid rgba(239,68,68,0.2);
  border-radius: var(--radius-sm);
  font-size:13px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
  transition: all var(--transition);
}
.btn-logout:hover { background: rgba(239,68,68,0.2); }

/* Brand department line */
.brand-dept { font-size:11px; color: rgba(255,255,255,0.4); margin-top: 2px; }

/* Sidebar collapse handle — a round button straddling the sidebar's right border, level with the
   account card. It used to be a full-width strip inside the sidebar, which spent a whole row on one
   small button; out here it costs no row at all. Arrow icon (« collapse / » expand). */
.side-toggle {
  position: fixed; z-index: 101;
  left: calc(var(--sidebar-width) - 14px);
  bottom: 88px;   /* level with where the old toggle row sat, just above the account card's divider */
  width: 28px; height: 28px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--card);
  color: var(--text-muted); cursor: pointer; display: flex; align-items: center; justify-content: center;
  box-shadow: var(--el-2);
  transition: left .25s ease, background .15s, color .15s, box-shadow .15s;
}
.side-toggle:hover {
  background: var(--primary); color: #fff; border-color: var(--primary);
  box-shadow: 0 2px 8px rgba(59,130,246,.35);
}
.side-toggle svg { width: 14px; height: 14px; }

/* Collapsed = icon rail (DESKTOP only). Hovering an icon opens a floating POPUP MENU next to it,
   styled exactly like the avatar (.cacct-pop) menu — see .nav-fly below. */
@media (min-width: 769px) {
  .app-layout.collapsed .sidebar { width: 64px; }
  .app-layout.collapsed .main-content { margin-left: 64px; }
  .app-layout.collapsed .brand-row h2,
  .app-layout.collapsed .brand-row .brand-sub,
  .app-layout.collapsed .brand-row .brand-dept,
  .app-layout.collapsed .nav-section-title,
  .app-layout.collapsed .nav-group,
  .app-layout.collapsed .nav-label,
  .app-layout.collapsed .cnm-wrap,
  .app-layout.collapsed .cdl { display: none; }
  /* The handle rides the rail's border in, so it stays reachable to expand again. */
  .app-layout.collapsed .side-toggle { left: 50px; }
  .app-layout.collapsed .side-toggle svg { transform: scaleX(-1); }  /* arrow flips → points right = expand */
  .app-layout.collapsed .nav-item { padding: 12px 0; justify-content: center; }
  .app-layout.collapsed .nav-section, .app-layout.collapsed .nav-section.is-open { background: none; padding: 0; }
  .app-layout.collapsed .nav-group-items { padding-left: 0; padding-right: 0; }
  /* A CLOSED group keeps one row on the rail — its chevron — so its pages are still reachable:
     hovering that row opens the flyout listing them. Without it the group's icons would be clipped
     away with nothing left to hover, and those pages would be unreachable until the rail is
     expanded again. An OPEN group needs no heading; its icons are already on the rail. */
  .app-layout.collapsed .nav-group { display: none; }
  .app-layout.collapsed .nav-section:not(.is-open) > .nav-group {
    display: flex;
    justify-content: center;
    padding: 8px 0;
    margin: 2px 0;
  }
  .app-layout.collapsed .nav-group-label { display: none; }
  .app-layout.collapsed .nav-caret { opacity: .85; }
  .app-layout.collapsed .nav-item .nav-icon { margin: 0; }
  .app-layout.collapsed .nav-sub { display: none; }
  .app-layout.collapsed .cacct { justify-content: center; }
  /* Avatar popup must ESCAPE the 64px rail (sidebar has overflow-x:hidden) — pin it fixed beside
     the rail at full width so it isn't squeezed into a vertical sliver. */
  .app-layout.collapsed .cacct-pop {
    position: fixed; left: 72px; right: auto; bottom: 14px; top: auto;
    width: 250px; max-height: 82vh; overflow-y: auto;
  }
}

/* Hover popup menu (collapsed rail) — same look as the avatar popup (.cacct-pop). */
.nav-fly {
  position: fixed; z-index: 2000; min-width: 184px; max-width: 300px;
  background: var(--menu-bg, var(--card)); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 6px; display: none;
}
.nav-fly.show { display: block; }
.nav-fly .nav-fly-head { font-size:11px; font-weight: 600;
  color: color-mix(in srgb, var(--text) 40%, transparent); padding: 8px 12px 4px; }
.nav-fly .pit { display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px 12px; border: none; background: none;
  border-radius: var(--radius-sm); font: inherit; font-size:13px; font-weight: 450; box-sizing: border-box;
  color: color-mix(in srgb, var(--text) 82%, transparent); cursor: pointer; text-align: left; text-decoration: none; }
.nav-fly .pit:hover { background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text); }
[data-theme="black"] .nav-fly {
  background: linear-gradient(180deg, rgba(40,42,58,.72), rgba(18,19,28,.68));
  -webkit-backdrop-filter: blur(18px) saturate(150%); backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid rgba(140,170,255,.34);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.09), var(--el-4), 0 0 14px -3px rgba(96,165,250,.35), 0 0 44px -14px rgba(124,58,237,.30);
}
[data-theme="black"] .nav-fly .pit:hover { background: rgba(255,255,255,.08); }

/* ===== MAIN CONTENT ===== */
.main-content {
  flex: 1;
  min-width: 0; /* let wide tables scroll inside their wrappers instead of stretching the page */
  margin-left: var(--sidebar-width);
  min-height: calc(100vh/var(--ui-zoom,1));
  display: flex;
  flex-direction: column;
  transition: margin-left 0.25s ease;
}

/* ===== TOP BAR ===== */
.topbar {
  height: var(--topbar-height);
  background: var(--card);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  padding: 0 24px;
  position: sticky;
  top: 0;
  /* Z-INDEX BAND (whole app, so a new popover has somewhere obvious to sit):
       1-10    page content that must beat a later sibling (board toolbars)
       90      this top bar — chrome, and the parent of the notification panel
       99/100  mobile sidebar scrim / sidebar, which slide OVER the bar by design
       1500    modal overlays (.ovl)
       2000    body-level popovers (ds-pop, export menu, tooltips)
     It was 50, below the toolbars at 60, so the notification dropdown rendered
     BEHIND the Summary Cards row. Note the panel's own z-index cannot fix that:
     sticky + z-index (and backdrop-filter, once glass is on) make this element a
     stacking context, so everything inside it competes at the bar's level, not
     its own. Raise THIS number, never the panel's. */
  z-index: 90;
  gap: 16px;
}
.topbar-hamburger {
  display: none;
  background: none;
  border: none;
  padding: 8px;
  cursor: pointer;
  color: var(--text);
}
.topbar-hamburger svg { width: 24px; height: 24px; stroke: currentColor; fill: none; stroke-width: 2; }
.topbar-title { font-size:17px; font-weight:600; color: var(--text); flex: 1; }
.topbar-date {
  display: flex;
  align-items: center;
  gap: 8px;
}
.topbar-date label { font-size:13px; color: var(--text-muted); font-weight: 600; }
.topbar-date input[type="date"] {
  padding: 6px 12px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  font-size:14px;
  color: var(--text);
  background: var(--bg);
  outline: none;
  cursor: pointer;
}
.topbar-date input[type="date"]:focus { border-color: var(--primary); }
.topbar-actions { display: flex; align-items: center; gap: 12px; }

/* Floating menu button for chrome-less (Platform/Dev) consoles — mobile only. */
.mobile-menu-fab { display: none; }
@media (max-width: 768px) {
  .mobile-menu-fab { display: flex; align-items: center; justify-content: center; position: fixed; top: 12px; left: 12px; z-index: 60; width: 40px; height: 40px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); box-shadow: var(--shadow); color: var(--text); cursor: pointer; }
  .mobile-menu-fab svg { width: 22px; height: 22px; }
}
.notification-btn {
  position: relative;
  background: none;
  border: none;
  padding: 8px;
  cursor: pointer;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: background var(--transition);
}
.notification-btn:hover { background: var(--bg); }
/* The other top-bar icon buttons (full screen, scan, language) — same shape and hit area as the
   bell so the group reads as one control strip, and the same accent the nav icons take. */
.topbar-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 8px; cursor: pointer;
  color: var(--primary); border-radius: var(--radius-sm);
  transition: background var(--transition), color var(--transition);
}
.topbar-icon-btn:hover { background: color-mix(in srgb, var(--primary) 9%, transparent); color: var(--primary-dark); }
.topbar-icon-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.notification-btn svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 2; }
.notification-badge {
  position: absolute;
  top: 1px; right: 1px;
  min-width: 16px; height: 16px;
  padding: 0 4px;
  /* ★ --danger-SOLID, not --danger and not --danger-dark. --danger (#ef4444) under white ink is
     3.76:1, measured on 58 of the 63 routes a department persona can open. But --danger-dark was
     the wrong correction: it is an ink token, so the dark themes flip it light and white-on-
     #ffa0a0 measured 1.95:1 — worse than the defect. See --danger-solid in :root.
     9px was also under the 10.5px floor for the one number telling somebody they have work. */
  background: var(--danger-solid);
  color: #fff;
  border-radius: var(--radius-sm);
  font-size:10.5px;
  font-weight:600;
  line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.topbar-user {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--transition);
}
.topbar-user:hover { background: var(--bg); }
.topbar-user .avatar-sm {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-weight:600; font-size:13px;
}
.topbar-user .user-name-sm { font-size:14px; font-weight: 600; }

/* User dropdown menu */
.user-menu {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  background: var(--card, #fff);   /* was --bg-card: a token that exists nowhere, so this read
                                      its literal on all twelve themes. The glass pass below happens
                                      to repaint .user-menu, which is the only reason it never showed. */
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--el-4);
  min-width: 220px;
  z-index: 100;
  overflow: hidden;
}
.user-menu.show { display: block; }
.user-menu-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
}
.user-menu-sep {
  height: 1px;
  background: var(--border);
}
.user-menu-item {
  display: block;
  padding: 8px 16px;
  font-size:13px;
  color: var(--text);
  text-decoration: none;
  transition: background 0.15s;
}
.user-menu-item:hover { background: var(--bg, #f8fafc); }
.user-menu-logout { color: var(--danger, #ef4444); font-weight: 600; }
.user-menu-logout:hover { background: rgba(239,68,68,0.08); }

/* ===== CONTENT AREA ===== */
.content-area {
  flex: 1;
  padding: 24px;
  width: 100%;
  max-width: 1440px;
  margin-inline: auto;
}
/* Dense full-bleed screens (e.g. wide data-entry matrices) can opt out. */
.content-area.full-bleed { max-width: none; }
.page { display: none; }
.page.active { display: block; }

/* ===== CARDS ===== */
.card {
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  border: 1px solid var(--border);
  transition: box-shadow var(--transition), transform var(--transition);
}
.card:hover { box-shadow: var(--shadow-md); }
.card-header {
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.card-header h3 { font-size:15px; font-weight:600; }
.card-body { padding: 20px; }
.card-footer {
  padding: 16px 20px;
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* ===== KPI / METRIC CARDS ===== */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}
.kpi-card {
  background: var(--card);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow);
  border: 1px solid var(--border);
  transition: all var(--transition);
  position: relative;
  overflow: hidden;
}
.kpi-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
.kpi-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 4px;
  height: 100%;
}
.kpi-card.kpi-blue::before { background: var(--primary); }
.kpi-card.kpi-green::before { background: var(--success); }
.kpi-card.kpi-amber::before { background: var(--warning); }
.kpi-card.kpi-red::before { background: var(--danger); }
.kpi-label {
  font-size:12px;
  font-weight: 600;
  color: var(--text-muted);


  margin-bottom: 8px;
}
.kpi-value {
  font-size:31px;
  font-weight:600;
  color: var(--text);
  line-height: 1.1;
}
.kpi-sub {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  font-size:13px;
}
.kpi-trend { font-weight:600; }
.kpi-trend.up { color: var(--success); }
.kpi-trend.down { color: var(--danger); }
.kpi-target { color: var(--text-muted); }
.kpi-icon {
  position: absolute;
  top: 16px; right: 16px;
  width: 44px; height: 44px;
  border-radius: var(--radius-sm);
  display: flex; align-items: center; justify-content: center;
  opacity: 0.12;
}
.kpi-icon svg { width: 28px; height: 28px; }
.kpi-card.kpi-blue .kpi-icon { background: var(--primary); color: var(--primary); }
.kpi-card.kpi-green .kpi-icon { background: var(--success); color: var(--success); }
.kpi-card.kpi-amber .kpi-icon { background: var(--warning); color: var(--warning); }

/* ===== TABLES ===== */
.table-wrapper { overflow-x: auto; border-radius: var(--radius); }
/* ============================================================================
   TABLES — THERE ARE TWO, AND WHICH ONE IS NOT A MATTER OF TASTE
   ----------------------------------------------------------------------------
   An audit of every view containing a <table> found 104 of them: 70 already on a
   shared class, 34 not. Of those 34, most are correctly bespoke — badges, gate
   passes, weigh slips, invoices, offers, exports. Those are PAPER, sized in mm
   and printed; they must not inherit a screen table's padding or hover.
   The rest are screens that simply never picked one. So the standard is not
   missing — it is unstated. Stating it:

     .data-table   A TABLE OF RECORDS you read. Rows arrive from the server, you
                   scan down them, you may click one. Separated borders, sticky
                   header, hover on the row. 18 views.

     .form-table   A GRID YOU TYPE INTO. Cells hold inputs, the shape is fixed by
                   the form, and a hover would be noise while somebody is filling
                   it in. Collapsed borders, tighter. 67 views.

   Choose by what the table IS FOR, not by how it looks: a register of issue notes
   is .data-table even when it is dense; a shift's reading grid is .form-table even
   when it is wide. A page that needs neither is printing, and should say so.

   ★ NOT a migration list. Fifteen in-app screens still roll their own — gate,
   muster, payroll, leave, hr-overview, material-issues, work-permit-detail,
   register-chains, aop-workspace, power-sld and a few more. They render correctly
   today and each needs its own look before it moves, so they are named here rather
   than swept: a blind mass-rename of fifteen live screens is how a fix becomes an
   outage.
   ========================================================================== */
.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size:14px;
}
.data-table thead th {
  background: var(--tint-neutral-bg);
  color: var(--text);
  font-weight:600;
  padding: 8px 12px;
  text-align: left;
  border-bottom: 2px solid var(--border);
  position: sticky;
  top: 0;
  white-space: nowrap;
  font-size:12px;


}
.data-table tbody td {
  padding: 8px 12px;
  border-bottom: 1px solid #f1f5f9;
  white-space: nowrap;
}
.data-table tbody tr:hover { background: var(--bg); }
.data-table tbody tr:nth-child(even) { background: var(--bg); }
.data-table tbody tr:nth-child(even):hover { background: var(--tint-neutral-bg); }
.data-table .section-header td {
  background: var(--border, #e2e8f0);
  font-weight:600;
  color: var(--text);
  padding: 8px 12px;
  font-size:13px;


}
.data-table .sub-total td {
  font-weight:600;
  background: var(--tint-neutral-bg);
  border-top: 2px solid var(--border);
}
.data-table .grand-total td {
  font-weight:600;
  background: var(--border, #e2e8f0);
  border-top: 3px solid var(--primary);
  font-size:14px;
}
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-success { color: var(--success); }
.text-danger { color: var(--danger); }
.text-warning { color: var(--warning); }
.text-muted { color: var(--text-muted); }

/* ===== BADGES ===== */
.badge {
  text-transform: capitalize;
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--radius-lg);
  font-size:12px;
  font-weight:600;


}
.badge-draft { background: var(--tint-warn-bg); color: var(--tint-warn-fg); }
.badge-submitted { background: var(--tint-info-bg); color: var(--tint-info-fg); }
.badge-approved { background: var(--tint-ok-bg); color: var(--tint-ok-fg); }
.badge-pending { background: var(--tint-warn-bg); color: var(--tint-warn-fg); }
.badge-success { background: var(--tint-ok-bg); color: var(--tint-ok-fg); }
.badge-danger { background: var(--tint-danger-bg); color: var(--tint-danger-fg); }

/* ===== STATUS INDICATORS ===== */
.status-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  display: inline-block;
  margin-right: 6px;
}
.status-dot.green { background: var(--success); }
.status-dot.red { background: var(--danger); }
.status-dot.yellow { background: var(--warning); }

/* ===== PROGRESS BAR ===== */
.progress-bar {
  height: 8px;
  background: var(--border, #e2e8f0);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  border-radius: var(--radius-sm);
  transition: width 0.6s ease;
}
.progress-fill.green { background: var(--success); }
.progress-fill.blue { background: var(--primary); }
.progress-fill.amber { background: var(--warning); }
.progress-fill.red { background: var(--danger); }

/* ===== COLLAPSIBLE SECTIONS ===== */
.collapsible {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 12px;
  overflow: hidden;
  transition: box-shadow var(--transition);
}
.collapsible:hover { box-shadow: var(--shadow); }
.collapsible-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 20px;
  background: var(--bg);
  cursor: pointer;
  user-select: none;
  transition: background var(--transition);
}
.collapsible-header:hover { background: var(--tint-neutral-bg); }
.collapsible-header h4 { font-size:15px; font-weight:600; display: flex; align-items: center; gap: 8px; }
.collapsible-arrow {
  transition: transform 0.3s ease;
  width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center;
}
.collapsible-arrow svg { width: 16px; height: 16px; stroke: var(--text-muted); fill: none; stroke-width: 2.5; }
.collapsible.open .collapsible-arrow { transform: rotate(180deg); }
.collapsible-body {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.4s ease, padding 0.3s ease;
  padding: 0 20px;
}
.collapsible.open .collapsible-body {
  max-height: 2000px;
  padding: 20px;
}

/* ===== TOGGLE SWITCH ===== */
.toggle-container { display: flex; align-items: center; gap: 8px; }
.toggle {
  position: relative;
  width: 44px; height: 24px;
  background: var(--border-strong, #cbd5e1);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background var(--transition);
}
.toggle.on { background: var(--success); }
.toggle::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 20px; height: 20px;
  background: #fff;
  border-radius: 50%;
  transition: transform var(--transition);
  box-shadow: var(--el-1);
}
.toggle.on::after { transform: translateX(20px); }
.toggle-label { font-size:14px; font-weight: 600; }

/* ===== FORM PAGE STYLES ===== */
.form-page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 20px;
  flex-wrap: wrap;
  gap: 12px;
}
.form-page-header h2 { font-size:21px; font-weight:600; }
.form-summary-card {
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  color: #fff;
  border-radius: var(--radius);
  padding: 20px 24px;
  margin-bottom: 20px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 16px;
}
.summary-item .summary-label {
  font-size:12px;


  opacity: 0.8;
  margin-bottom: 4px;
}
.summary-item .summary-value { font-size:25px; font-weight:600; }
.summary-item .summary-unit { font-size:13px; opacity: 0.7; margin-left: 4px; }

/* ===== SUBMISSION STATUS ===== */
.submission-status {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 20px;
  align-items: center;
}
.submission-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: var(--radius-lg);
  font-size:12px;
  font-weight: 600;
  border: 1.5px solid;
}
.submission-chip.done { background: var(--tint-ok-bg); border-color: var(--tint-ok-bg); color: var(--tint-ok-fg); }
.submission-chip.missing { background: var(--danger-light); border-color: #fecaca; color: #991b1b; }

/* ===== DPR QUALITY GRID ===== */
.quality-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
  margin-top: 24px;
}

/* ===== GRID UTILITIES ===== */
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.gap-sm { gap: 8px; }
.gap-md { gap: 16px; }
.gap-lg { gap: 24px; }
.mb-sm { margin-bottom: 8px; }
.mb-md { margin-bottom: 16px; }
.mb-lg { margin-bottom: 24px; }
.mt-md { margin-top: 16px; }
.mt-lg { margin-top: 24px; }

/* ===== ADMIN TABLES ===== */
.admin-table { width: 100%; }
.admin-table thead th {
  background: var(--tint-neutral-bg);
  padding: 8px 12px;
  text-align: left;
  font-weight:600;
  font-size:12px;


  border-bottom: 2px solid var(--border);
}
.admin-table tbody td {
  padding: 12px 12px;
  border-bottom: 1px solid #f1f5f9;
}
.admin-table tbody tr:hover { background: var(--bg); }

/* ===== CHART PLACEHOLDERS ===== */
.chart-placeholder {
  height: 280px;
  background: linear-gradient(135deg, var(--bg, #f8fafc), #f1f5f9);
  border-radius: var(--radius-sm);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  border: 2px dashed var(--border);
}
.chart-placeholder svg { width: 48px; height: 48px; stroke: #cbd5e1; fill: none; stroke-width: 1.5; margin-bottom: 12px; }
.chart-placeholder span { font-size:14px; font-weight: 600; }

/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
  .sidebar { transform: translateX(-100%); }
  .sidebar.mobile-open { transform: translateX(0); }
  .main-content { margin-left: 0; }
  .side-toggle { display: none; }   /* collapse is desktop-only; mobile uses the off-canvas menu */
  .topbar-hamburger { display: flex; }
  .kpi-grid { grid-template-columns: repeat(2, 1fr); }
  .form-row, .form-row-3, .form-row-4, .form-row-5, .form-row-6 {
    grid-template-columns: 1fr;
  }
  .form-table { font-size:13px; }
  .form-table th, .form-table td.row-label { font-size:12px; padding: 6px 8px; }
  .form-table td input, .form-table td select { font-size:13px; padding: 4px 6px; }
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
  .quality-grid { grid-template-columns: 1fr; }
  .topbar-date label { display: none; }
  .topbar-user .user-name-sm { display: none; }
  .content-area { padding: 16px; }
}
@media (max-width: 480px) {
  .kpi-grid { grid-template-columns: 1fr; }
  .login-card { padding: 32px 24px; }
}
/* ★ THE TOP BAR ON A PHONE. Measured at 412px: the bar ran to 518px — the plant chip's two lines, a
   full-screen button a phone cannot use, the Liquid ground switch and a title that wrapped to three
   lines pushed the bell off the right edge and the whole page scrolled sideways. On a phone the bar
   is: menu · title (one line, ellipsis) · scan · bell · account. Full screen is a wall-screen control;
   the plant is on the dashboard and in the account menu; the ground switch lives in Appearance. */
@media (max-width: 600px) {
  .topbar { flex-wrap: nowrap; gap: 6px; padding-left: 8px; padding-right: 8px; overflow: hidden; }
  .topbar-title { font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .topbar-date { display: none !important; }
  #fs-btn, .lg-ground, #lang-btn { display: none !important; }
  .topbar-actions { flex: none; gap: 0; }
  .cacct .cnm, .cacct .crl, .cacct .cdl { display: none; }   /* the avatar alone opens the account menu */
  .cacct { padding: 4px; }
}

/* ===== MOBILE OVERLAY ===== */
.sidebar-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 99;
}
.sidebar-overlay.show { display: block; }

/* ===== PRINT STYLES ===== */
@media print {
  .sidebar, .topbar, .btn-group, .form-page-header .btn-group,
  .sidebar-overlay, .topbar-hamburger { display: none !important; }
  .main-content { margin-left: 0 !important; }
  .content-area { padding: 0 !important; }
  .card { box-shadow: var(--el-1); border: 1px solid #ccc !important; break-inside: avoid; }
  .kpi-card { break-inside: avoid; box-shadow: var(--el-1); border: 1px solid #ccc !important; }
  .data-table, .form-table { font-size: 9pt; }
  .data-table thead th, .form-table th { background: #eee !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  .form-table .section-header td { background: #ccc !important; color: #000 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  .form-table-wrap { overflow: visible; }
  body { background: #fff; font-size: 10pt; }
  @page { margin: 1cm; }
  .page { display: block !important; }
  .kpi-grid { grid-template-columns: repeat(4, 1fr); }
  .collapsible-body { max-height: none !important; padding: 12px 20px !important; }
}

/* ===== ANIMATIONS ===== */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.page.active { animation: fadeIn 0.3s ease; }
.kpi-card { animation: fadeIn 0.3s ease backwards; }
.kpi-card:nth-child(2) { animation-delay: 0.05s; }
.kpi-card:nth-child(3) { animation-delay: 0.1s; }
.kpi-card:nth-child(4) { animation-delay: 0.15s; }

/* ===== SEARCH ===== */
.filter-bar {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
  flex-wrap: wrap;
  align-items: center;
}
.search-input {
  flex: 1;
  min-width: 200px;
  padding: 8px 12px 8px 32px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  font-size:14px;
  background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E") 12px center no-repeat;
  outline: none;
  transition: border-color var(--transition);
}
.search-input:focus { border-color: var(--primary); }

/* ===== DEMO CREDENTIALS ===== */
.demo-creds {
  margin-top: 24px;
  padding: 16px;
  background: var(--tint-neutral-bg);
  border-radius: var(--radius-sm);
  font-size:12px;
}
.demo-creds h4 {
  font-size:12px;
  font-weight:600;


  color: #cdd4e6;
  margin-bottom: 10px;
  text-align: center;
  border-bottom: 1px solid var(--border);
  padding-bottom: 8px;
}
.demo-creds .cred-level {
  margin-bottom: 6px;
  line-height: 1.7;
}
.demo-creds .cred-label {
  font-weight:600;
  color: var(--text);
  display: inline-block;
  min-width: 110px;
}
.demo-creds .cred-value {
  color: var(--primary-dark);
  font-family: 'SFMono-Regular', Consolas, monospace;
  cursor: pointer;
  background: var(--border, #e2e8f0);
  padding: 0 4px;
  border-radius: var(--radius-sm);
  font-size:12px;
}
.demo-creds .cred-value:hover {
  background: var(--primary-light);
}

/* ===== APPROVAL STATUS PANEL ===== */
.approval-panel {
  background: var(--card);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  padding: 20px 24px;
  margin-bottom: 24px;
  box-shadow: var(--shadow);
}
.approval-panel h3 {
  font-size:15px;
  font-weight:600;
  margin-bottom: 16px;
}
.approval-row {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  border-bottom: 1px solid #f1f5f9;
  border-radius: var(--radius-sm);
  transition: background 0.15s;
}
.approval-row:hover { background: var(--bg); }
.approval-row:last-child { border-bottom: none; }
.approval-row .dept-name {
  font-weight: 600;
  font-size:14px;
  min-width: 160px;
}
.approval-row .status-info {
  font-size:13px;
  color: var(--text-muted);
  flex: 1;
  margin: 0 16px;
}
.approval-summary {
  display: flex;
  gap: 20px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 2px solid var(--border);
  font-weight:600;
  font-size:14px;
  flex-wrap: wrap;
}

/* ===== BADGE EXTENSIONS ===== */
.badge-rejected { background: var(--tint-danger-bg); color: var(--tint-danger-fg); }
.badge-hod-approved { background: var(--tint-ok-bg); color: var(--tint-ok-fg); }
.badge-plant-approved { background: var(--tint-info-bg); color: var(--tint-info-fg); }
.badge-not-submitted { background: var(--tint-neutral-bg); color: var(--tint-neutral-fg); }

/* ===== REVIEW CARD ===== */
.review-card {
  background: var(--card);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  margin-bottom: 16px;
  overflow: hidden;
  box-shadow: var(--shadow);
}
.review-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 20px;
  background: var(--bg);
  cursor: pointer;
  transition: background 0.15s;
}
.review-card-header:hover { background: var(--tint-neutral-bg); }
.review-card-header h4 { font-size:15px; font-weight:600; }
.review-card-body {
  padding: 20px;
  border-top: 1px solid var(--border);
}
.review-actions {
  display: flex;
  gap: 12px;
  align-items: flex-end;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.review-actions textarea { flex: 1; }

/* ===== PLANT APPROVE SECTION ===== */
.plant-approve-section {
  background: var(--card);
  border-radius: var(--radius);
  border: 2px solid var(--primary);
  padding: 24px;
  margin-top: 24px;
  box-shadow: var(--shadow-md);
}
.plant-approve-section h3 {
  font-size:17px;
  font-weight:600;
  margin-bottom: 16px;
  color: var(--primary-dark);
}

/* ===== PENDING MESSAGE ===== */
.pending-message {
  text-align: center;
  padding: 48px 20px;
  color: var(--text-muted);
}
.pending-message svg {
  width: 64px;
  height: 64px;
  stroke: #cbd5e1;
  fill: none;
  stroke-width: 1.5;
  margin-bottom: 16px;
}
.pending-message h3 {
  font-size:21px;
  font-weight:600;
  color: var(--text);
  margin-bottom: 8px;
}
.pending-message p { font-size:14px; }

/* ===== STATUS RESULT BAR ===== */
.status-result-bar {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  border-radius: var(--radius-sm);
  font-size:14px;
  font-weight: 600;
  margin-bottom: 16px;
}
.status-result-bar.approved { background: var(--success-light, #d1fae5); color: #065f46; border: 1px solid #a7f3d0; }
.status-result-bar.rejected { background: var(--danger-light, #fee2e2); color: #991b1b; border: 1px solid #fecaca; }
.status-result-bar.pending { background: var(--warning-light, #fef3c7); color: #92400e; border: 1px solid #fde68a; }

/* ============================================================================
   PER-USER APPEARANCE  —  <html data-theme data-size data-contrast>
   Set per account (core/appearance.js). Each axis is independent:
     data-theme    → the accent + surface "look" (only these tokens change; layout is untouched)
     data-size     → batch-scales the whole UI via root zoom (not one font)
     data-contrast → darker text + firmer borders for readability
   'blue' + 'md' + 'normal' == today's design, so unset/legacy pages are unaffected.
   ============================================================================ */

/* --- Themes (accent trio + active-nav tint; warm also warms the whole surface) --- */
/* ★ A LIGHT SIDEBAR NEEDS DARK SIDEBAR INK. These two themes repaint --sidebar-bg white/near-white
   and stopped there, so --sidebar-text (#94a3b8) and --sidebar-active (#e2e8f0) kept the values meant
   for the navy rail — pale grey on white, 1.2:1. Nothing looked broken because the nav is styled by
   its own rules here; the pair only shows up where something else reads it (.wb-scale2, the live
   weighbridge readout). A theme that moves one half of a pair owns the other half. */
[data-theme="blue"]   {
  --sidebar-text:#64748b; --sidebar-active:#0f172a;
 --primary:#3b82f6; --primary-dark:#1d4ed8; --primary-light:#dbeafe; --nav-active-bg:#eff6ff; }
[data-theme="indigo"] { --primary:#6366f1; --primary-dark:#4f46e5; --primary-light:#e0e7ff; --nav-active-bg:#eef2ff; --bg:#faf9fd; }
[data-theme="forest"] { --primary:#10b981; --primary-dark:#047857; --primary-light:#d1fae5; --nav-active-bg:#ecfdf5; --bg:#f7faf8; }
[data-theme="graphite"]{ --primary:#64748b; --primary-dark:#334155; --primary-light:#e2e8f0; --nav-active-bg:#f1f5f9; --bg:#f7f8fa; }
[data-theme="rose"]   { --primary:#e11d48; --primary-dark:#be123c; --primary-light:#ffe4e6; --nav-active-bg:#fff1f2; --bg:#fdf8f9; }
/* Anthropic Warm — cream canvas, warm text, clay accent (a complete "feel", not just a colour) */
/* ============================================================================
   FLAT — a shadcn/ui-style look, offered ALONGSIDE Liquid Glass rather than
   replacing it. Where glass is frosted, tinted and layered, this is the opposite
   discipline: opaque surfaces, hairline borders, small radii, almost no shadow,
   and a neutral palette that lets the data carry the colour. Some people read
   dense plant tables better on a flat surface; both are now a per-user choice.

   Everything here is token + surface overrides — no page knows this theme exists,
   which is exactly why the app was built on tokens. */
/* ===== WHITE — pure paper =====================================================================
   Every surface is #ffffff: canvas, cards, sidebar, top bar. No grey-blue canvas (the default sits
   on #f8fafc), no zinc rail, no ambient tint at rest — the only things dividing the page are
   hairline borders and the accent.
   It keeps the signature blue rather than going monochrome, because a monochrome white already
   exists as 'flat' and because the accent now carries real meaning in the nav — icons, open group
   headings, hover. Colour here is a cue on white paper, not decoration.
   PURE means pure: no ambient wash in ANY glass mode. The consequence is deliberate and worth
   knowing — Full glass has little visible effect on this theme, because frosted panels over a plain
   white page have nothing to refract. That is the trade this theme makes. */
[data-theme="white"] {
  /* Deep indigo accent (owner's reference image), not the app blue — on pure white paper a dark,
     saturated indigo reads as ink rather than as a highlight, which is what makes this theme look
     like a document instead of a dashboard. Everything accented follows it: nav icons, open group
     headings, hover, the avatar disc, and the pale state fills, which are color-mix()ed FROM
     --primary and so re-tint on their own. */
  --primary:#1e2a78; --primary-dark:#141c56; --primary-light:#eceffa; --nav-active-bg:#f1f3fb;
  --bg:#ffffff; --card:#ffffff;
  /* Neutral greys, not blue-greys — a blue cast on the type is what made this page read as washed
     rather than as ink. Going all the way to pure black is the user's call, not the theme's: it is
     the Text option in Appearance (data-text), so it works on every theme rather than only here. */
  --text:#171717; --text-muted:#525252; --text-secondary:#262626;
  --border:#ececf0; --border-strong:#dcdfe4;
  --radius:12px;
  --shadow:0 1px 2px rgba(17,24,39,.04);
  --shadow-md:0 2px 8px rgba(17,24,39,.06);
  --sidebar-bg:#ffffff; --sidebar-hover:#f6f7f9;
  /* The NEUTRAL surface tokens have to be redefined too, not just --bg. A theme that sets only the
     canvas leaves every table header, hover fill and neutral chip on the DEFAULT slate (#f1f5f9),
     which is exactly what "the background is not as white as pure white" was: the canvas was
     #ffffff while .data-table/.admin-table headers were still painting slate, because they read
     --tint-neutral-bg rather than --bg. Neutral here means neutral grey, not blue-grey. */
  --th-bg:#f8f9fd;
  --tint-neutral-bg:#f6f7fc; --tint-neutral-fg:#4b5563;
  --tint-info-bg:#f1f3fb;    --tint-info-fg:#1e2a78;
  --sidebar-text:#6b7280;    --sidebar-active:#111827;
}
/* NO GREY ANYWHERE. Every state fill in the app is a tint of --text, which on a white page reads as
   a grey block sitting on the paper — three of them in the sidebar alone (the open-group panels),
   which is what "make it pure white" was about. On this theme those fills become tints of the
   ACCENT instead: the paper stays #ffffff and the highlights are pale blue, so nothing on screen is
   grey. The cues themselves are unchanged — an open group still has a panel, the active row still
   has a fill; they are just no longer grey. */
[data-theme="white"] .nav-section.is-open { background: color-mix(in srgb, var(--primary) 5%, transparent); }
[data-theme="white"] .nav-item:hover,
[data-theme="white"] .nav-group:hover { background: color-mix(in srgb, var(--primary) 7%, transparent); }
[data-theme="white"] .nav-item.active,
[data-theme="white"] .nav-item.nav-parent.expanded { background: var(--nav-active-bg); }
[data-theme="white"] .cacct:hover,
[data-theme="white"] .cacct-pop .pit:hover,
[data-theme="white"] .nav-fly .pit:hover { background: color-mix(in srgb, var(--primary) 7%, transparent); }
[data-theme="white"] .cacct-sep { background: var(--border); }
/* Cards read by their edge, not by a fill difference, since there is no fill difference left. */
[data-theme="white"] .card, [data-theme="white"] .panel, [data-theme="white"] .ds-card,
[data-theme="white"] .stat, [data-theme="white"] .kpi-card, [data-theme="white"] .ox-stat {
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
[data-theme="white"] .card:hover, [data-theme="white"] .panel:hover,
[data-theme="white"] .stat:hover, [data-theme="white"] .kpi-card:hover { box-shadow: var(--shadow-md); }
[data-theme="white"] .sidebar { border-right: 1px solid var(--border); }
[data-theme="white"] .topbar { border-bottom: 1px solid var(--border); }

[data-theme="flat"] {
  --sidebar-text:#52525b; --sidebar-active:#18181b;

  --primary:#18181b; --primary-dark:#09090b; --primary-light:#f4f4f5; --nav-active-bg:#f4f4f5;
  --bg:#ffffff; --card:#ffffff;
  --text:#09090b; --text-muted:#71717a; --text-secondary:#52525b;
  --border:#e4e4e7;
  --radius:8px; --radius-sm:6px;
  --shadow:0 1px 2px 0 rgb(0 0 0 / .05);
  --shadow-md:0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);
  --sidebar-bg:#fafafa; --sidebar-hover:#f4f4f5;
  --th-bg:#fafafa;
}
/* Opaque, unblurred, no ambient gradient — the point of the theme. */
[data-theme="flat"] .app-layout { background:var(--bg); }
[data-theme="flat"] .card, [data-theme="flat"] .panel, [data-theme="flat"] .stat,
[data-theme="flat"] .kpi-card, [data-theme="flat"] .ds-card, [data-theme="flat"] .ox-stat,
[data-theme="flat"] .sidebar, [data-theme="flat"] .topbar {
  background:var(--card);
  -webkit-backdrop-filter:none; backdrop-filter:none;
  border:1px solid var(--border);
  box-shadow:var(--shadow);
}
[data-theme="flat"] .sidebar { background:var(--sidebar-bg); border:none; border-right:1px solid var(--border); box-shadow:none; }
[data-theme="flat"] .topbar  { border:none; border-bottom:1px solid var(--border); box-shadow:none; }
[data-theme="flat"] .card:hover, [data-theme="flat"] .panel:hover,
[data-theme="flat"] .stat:hover, [data-theme="flat"] .kpi-card:hover { box-shadow:var(--shadow-md); transform:none; }
/* A near-black primary needs its own button treatment — the app's default is a blue gradient. */
[data-theme="flat"] .btn-primary, [data-theme="flat"] .ox-hero {
  background:var(--primary); color:#fafafa; box-shadow:none;
}
[data-theme="flat"] .btn-primary:hover, [data-theme="flat"] .ox-hero:hover {
  background:#27272a; box-shadow:none; transform:none;
}
/* The ring is the signature of this look: a visible focus halo on every control. */
[data-theme="flat"] .form-control:focus, [data-theme="flat"] .ds-sel:focus,
[data-theme="flat"] input:focus, [data-theme="flat"] textarea:focus, [data-theme="flat"] select:focus {
  border-color:var(--primary); box-shadow:0 0 0 3px rgb(24 24 27 / .12);
}
[data-theme="flat"] .nav-item.active { background:var(--nav-active-bg); color:var(--text); font-weight:600; }

[data-theme="warm"] {
  --primary:#c15f3c; --primary-dark:#a04a2c; --primary-light:#f4e3d9; --nav-active-bg:#f7ece5;
  --bg:#faf8f3; --card:#fffdf8; --text:#2b2824;
  /* #857d70 measured exactly 4:1 on this card — under the floor. #7a7367 is 4.62:1 and only 12%
     darker, so Warm still reads warm. */
  /* 4.62:1 on --card but 4.42 on --bg, so it is taken one step further: 4.71 on both. */
  --text-muted:#726b60; --border:#ece6d9;
}
/* Warm = editorial: the page/topbar titles carry a serif, matching the approved preview.
   (UI labels, table headers and body stay Inter — the serif is a heading accent, not everywhere.) */
[data-theme="warm"] .topbar-title,
[data-theme="warm"] .content-area > h1,
[data-theme="warm"] .content-area > h2,
[data-theme="warm"] .page-title,
[data-theme="warm"] .ov-title,
[data-theme="warm"] .panel-head h3,
[data-theme="warm"] .card-header h3 { font-family: Georgia, 'Times New Roman', serif; font-weight: 600; letter-spacing: -0.01em; }

/* Black = true dark. Tokens carry most of it; the component overrides below cover the surfaces
   that assume a light page (inputs, table headers, hover tints, badge tint backgrounds). */
[data-theme="black"] {
  --primary:#60a5fa; --primary-dark:#3b82f6; --primary-light:#152238; --nav-active-bg:#141e30;
  /* --border is BLUE-LIT in black: every bordered box, table edge and divider app-wide picks it up,
     so structure is identifiable on the dark ground (owner: 'boxes need glow for identification'). */
  --bg:#0a0a0c; --card:#141418; --text:#e8e8ec; --text-muted:#9b9ba4; --border:rgba(148,172,255,.28);
  --success-light:#0f2e24; --warning-light:#2f2408; --danger-light:#331416;
  /* The dark end has to go LIGHT here — it is the text and border drawn ON the tints above, and the
     tints above are nearly black. While these were 22 hardcoded #059669 they could not follow, so a
     "done" pill in this theme was mid-green on near-black. Now they are tokens, so they can. */
  --success-dark:#34d399; --warning-dark:#fbbf24; --danger-dark:#f87171;
  /* This theme's glass panels want a 16px corner. Set once, here, so every surface takes it —
     including the 111 that read var(--radius) and were left at 12px while cards sat at 16. */
  --radius:16px; --radius-sm:12px;
  --sidebar-bg:#050507; --sidebar-hover:#141418;
  --shadow:0 1px 3px rgba(0,0,0,.5),0 1px 2px rgba(0,0,0,.4);
  --shadow-md:0 4px 6px -1px rgba(0,0,0,.55),0 2px 4px -1px rgba(0,0,0,.45);
  --shadow-lg:0 10px 15px -3px rgba(0,0,0,.6),0 4px 6px -2px rgba(0,0,0,.5);
  /* flipped status tints — dark backing, light readable text */
  --tint-warn-bg:#2f2408;  --tint-warn-fg:#fbbf24;
  --tint-hot-bg:#37200c;   --tint-hot-fg:#fb923c;   --tint-hot-dot:#f97316;
  --tint-danger-bg:#3a1618; --tint-danger-fg:#f87171;
  --tint-ok-bg:#0f2e24;    --tint-ok-fg:#34d399;
  --tint-info-bg:#152238;  --tint-info-fg:#93c5fd;
  --tint-neutral-bg:#22222a; --tint-neutral-fg:#c3c3cc;
  --th-bg:#191920;
}
/* Native dark rendering for EVERY control — inputs/selects/textareas/checkboxes/scrollbars get UA
   dark treatment, so page-specific input classes (.fi, bespoke modal fields, yard rows…) can never
   fall back to white-on-white / black-on-dark again. */
[data-theme="black"] { color-scheme: dark; }
/* Universal field look in black: translucent fill + GLOWING ring so every box edge is identifiable.
   background-COLOR (not the shorthand) so icon background-images (e.g. search fields) survive. */
[data-theme="black"] input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=file]):not([type=color]),
[data-theme="black"] select,
[data-theme="black"] textarea {
  background-color: rgba(255,255,255,.07);
  color: var(--text);
  border-color: rgba(140,170,255,.30);
  box-shadow: 0 0 0 1px rgba(140,170,255,.20), 0 0 10px -4px rgba(96,165,250,.45), inset 0 1px 1px rgba(0,0,0,.28);
}
[data-theme="black"] input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=file]):not([type=color]):focus,
[data-theme="black"] select:focus,
[data-theme="black"] textarea:focus {
  background-color: rgba(255,255,255,.10);
  border-color: rgba(120,150,255,.75);
  box-shadow: 0 0 0 3px rgba(96,165,250,.22), 0 0 18px -4px rgba(96,165,250,.6), inset 0 1px 1px rgba(0,0,0,.28);
  outline: none;
}
[data-theme="black"] input::placeholder, [data-theme="black"] textarea::placeholder { color:#7a7a86; }
[data-theme="black"] select option { background:#17171e; color:var(--text); }
[data-theme="black"] input[disabled], [data-theme="black"] select[disabled] { opacity:.55; }
[data-theme="black"] .form-control { background-color:#1a1a20; color:var(--text); }
[data-theme="black"] .form-control::placeholder { color:#6b6b74; }
[data-theme="black"] .form-table td input:hover,
[data-theme="black"] .form-table td select:hover,
[data-theme="black"] .form-table td input:focus,
[data-theme="black"] .form-table td select:focus { background:#1a1a20; }
[data-theme="black"] .modal-dialog, [data-theme="black"] .card { background:var(--card); }
/* ★ SCOPED SO IT CANNOT EAT THE SEMANTIC BUTTONS. At (0,2,0) this beat .btn-success and .btn-danger
   (0,1,0) whatever the order, so on this theme Approve and Delete rendered as the same grey slab as
   Cancel — measured, .btn.btn-success computed to rgb(26,26,32), identical to a neutral button. */
[data-theme="black"] .btn:not([class*="btn-"]) { background:#1a1a20; color:var(--text); }
[data-theme="black"] .btn-primary { background:linear-gradient(135deg,var(--primary),var(--primary-dark)); color:#fff; }
[data-theme="black"] .btn-secondary { background:#1a1a20; color:var(--text); border-color:var(--border); }
[data-theme="black"] .btn-success { color:#fff; } [data-theme="black"] .btn-danger { color:#fff; }
[data-theme="black"] .stat .d { color:#34d399; }
[data-theme="black"][data-contrast="high"] { --text:#ffffff; --text-muted:#c3c3cc; --border:#3f3f46; }

/* ===== BLACK THEME — premium depth: ambient glows, elevated gradient surfaces, glassy fields =====
   Scoped entirely to [data-theme="black"] so it lifts every page WITHOUT a per-page redesign. */
/* Ambient — richer coloured glows so glass surfaces have something luminous to sit over. */
[data-theme="black"] body{
  background:
    radial-gradient(1200px 560px at 82% -12%, rgba(59,130,246,.20), transparent 62%),
    radial-gradient(1040px 560px at 3% 116%, rgba(139,92,246,.17), transparent 62%),
    radial-gradient(780px 500px at 104% 58%, rgba(236,72,153,.09), transparent 60%),
    var(--bg);
  background-attachment:fixed;
}
/* GLASSMORPHISM surfaces — frosted translucent, bright top hairline, soft depth + more rounding. */
[data-theme="black"] .card,
[data-theme="black"] .panel,
[data-theme="black"] .stat,
[data-theme="black"] .kpi-card,
[data-theme="black"] .modal-dialog{
  background:linear-gradient(180deg, rgba(40,42,58,.55), rgba(18,19,28,.5));
  -webkit-backdrop-filter:blur(18px) saturate(150%); backdrop-filter:blur(18px) saturate(150%);
  border:1px solid rgba(140,170,255,.34);
  /* border-radius lives on --radius in this theme's token block above, not here. Setting it on
     these five selectors gave the black theme 16px cards and 12px everything-else — the same split
     that put a 16px card beside a 12px tile on the Stores desk. A theme picks ONE corner. */
  box-shadow:inset 0 1px 0 rgba(255,255,255,.09), var(--el-4), 0 0 14px -3px rgba(96,165,250,.35), 0 0 44px -14px rgba(124,58,237,.30);   /* visible neon edge */
}
/* Tables glow too — a lit ring around the table body and a bright rule under the header row. */
[data-theme="black"] .form-table,
[data-theme="black"] .data-table,
[data-theme="black"] .admin-table,
[data-theme="black"] .dtable{
  box-shadow:0 0 0 1px rgba(140,172,255,.32), 0 0 16px -5px rgba(96,165,250,.45);
  border-radius:var(--radius-sm);
}
[data-theme="black"] .form-table thead th,
[data-theme="black"] .data-table thead th,
[data-theme="black"] .admin-table thead th,
[data-theme="black"] .dtable thead th{ border-bottom:1px solid rgba(140,172,255,.45); }
[data-theme="black"] .card:hover,
[data-theme="black"] .stat:hover,
[data-theme="black"] .kpi-card:hover{
  border-color:rgba(140,170,255,.28);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.1), var(--el-4), 0 0 40px -8px rgba(96,165,250,.14);
}
[data-theme="black"] .card-header,
[data-theme="black"] .panel-head,
[data-theme="black"] .modal-header,
[data-theme="black"] .modal-footer{ border-color:rgba(255,255,255,.08); }
/* Glassy translucent inputs, softly rounded */
[data-theme="black"] .form-control,
[data-theme="black"] .form-table td input,
[data-theme="black"] .form-table td select{
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.13);
  box-shadow:inset 0 1px 1px rgba(0,0,0,.3);
}
[data-theme="black"] .form-control{ border-radius:var(--radius); }
[data-theme="black"] .form-control:focus{
  border-color:rgba(120,150,255,.7);
  background:rgba(255,255,255,.09);
  box-shadow:0 0 0 3px rgba(96,165,250,.2), 0 0 22px -6px rgba(96,165,250,.4), inset 0 1px 1px rgba(0,0,0,.3);
}
/* Primary action — gradient + accent glow */
[data-theme="black"] .btn-primary{ box-shadow:0 6px 20px -4px rgba(59,130,246,.55); }
[data-theme="black"] .btn-primary:hover{ box-shadow:0 10px 30px -4px rgba(59,130,246,.7); transform:translateY(-1px); }
/* Chrome — deep gradient sidebar, frosted topbar, hairline separators */
[data-theme="black"] .sidebar{ background:linear-gradient(180deg,#0b0b10,#050507); border-right:1px solid rgba(255,255,255,.06); }
[data-theme="black"] .topbar{ background:rgba(10,10,14,.72); -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px); border-bottom:1px solid rgba(255,255,255,.06); }
[data-theme="black"] .nav-item.active, [data-theme="black"] .nav-item.nav-parent.expanded{ box-shadow:inset 2px 0 0 var(--primary); }
/* Badges get a faint ring so tinted pills stay defined on dark */
[data-theme="black"] .badge{ box-shadow:inset 0 0 0 1px rgba(255,255,255,.07); }
[data-theme="black"] .stat .v{ color:#fff; }
/* kill the light-FLASH hover/focus states on forms & tables in dark theme (rows/inputs were turning white) */
[data-theme="black"] .form-table tr:hover td{ background:rgba(255,255,255,.028); }
[data-theme="black"] .form-table tr:hover td.row-label{ background:rgba(255,255,255,.05); }
[data-theme="black"] .form-table td input:focus,
[data-theme="black"] .form-table td select:focus{ background:#1c1c24; }
[data-theme="black"] .form-table td input[readonly]{ background:#191920; color:#9fb4d8; }
[data-theme="black"] .form-table tr.sub-header:hover td{ background:#20202a; color:var(--text); }
[data-theme="black"] .form-table tr.totals-row:hover td{ background:#152238; }
[data-theme="black"] .data-table tbody tr:nth-child(even):hover,
[data-theme="black"] .review-card-header:hover{ background:#191920; }
/* Bottom-left account popup — same lit-glass treatment as .card so the box reads as a
   glowing bordered surface on the dark ground (owner: box border must glow for identification).
   Slightly more opaque than cards for menu legibility over the sidebar; position/animation untouched.
   Separators/section labels already flip via var(--border)/color-mix(--text) — only the surface
   and a firmer translucent-white row hover are added here. */
[data-theme="black"] .cacct-pop{
  background:linear-gradient(180deg, rgba(40,42,58,.72), rgba(18,19,28,.68));
  -webkit-backdrop-filter:blur(18px) saturate(150%); backdrop-filter:blur(18px) saturate(150%);
  border:1px solid rgba(140,170,255,.34);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.09), var(--el-4), 0 0 14px -3px rgba(96,165,250,.35), 0 0 44px -14px rgba(124,58,237,.30);
}
[data-theme="black"] .cacct-pop .pit:hover{ background:rgba(255,255,255,.08); }
[data-theme="black"] .cacct:hover{ background:rgba(255,255,255,.06); }

/* --- Text size: one batch scale for the entire interface (fixed sidebar included) --- */
/* ★ THE ZOOM HAS TO BE READABLE AS A NUMBER, not only applied as an effect.
   `zoom` scales everything inside body — including how a `vh` resolves. A panel capped at
   `calc(100vh - 80px)` computes that against the viewport and is then multiplied by the zoom, so on
   Large it renders 10% TALLER than the window and on Extra Large 22% taller. Measured on the
   Appearance modal: the Save button's bottom edge sat at 1033px, then 1146px, in a 1000px window —
   the one control that commits the dialog, off the bottom of the screen, for every person who had
   ever chosen a bigger text size. It looked like a missing button and was a unit-conversion bug.
   Anything capped by vh inside the body must divide by this. */
[data-size="sm"] body { zoom: 0.94; }
[data-size="lg"] body { zoom: 1.10; }
[data-size="xl"] body { zoom: 1.22; }
:root                { --ui-zoom: 1; }
[data-size="sm"]     { --ui-zoom: 0.94; }
[data-size="lg"]     { --ui-zoom: 1.10; }
[data-size="xl"]     { --ui-zoom: 1.22; }

/* --- Contrast: High darkens text and firms borders. Placed AFTER themes so it wins;
       the warm look gets a warm-dark override at higher specificity. --- */
[data-contrast="high"] { --text:#0b1220; --text-muted:#475569; --border:#cbd5e1; }
[data-theme="warm"][data-contrast="high"] { --text:#1a1712; --text-muted:#5f584d; --border:#d8cfbc; }

/* Extra High: maximum legibility — pure-black text, firm dark borders, no washed greys; bolder
   body weight so thin glyphs don't grey out. The black theme flips to pure white on near-black. */
[data-contrast="xhigh"] { --text:#000000; --text-muted:#1f2937; --border:#64748b; }
[data-contrast="xhigh"] body { font-weight:500; }
[data-contrast="xhigh"] .form-control, [data-contrast="xhigh"] input, [data-contrast="xhigh"] select, [data-contrast="xhigh"] textarea { font-weight:500; }
[data-theme="warm"][data-contrast="xhigh"] { --text:#0e0c08; --text-muted:#463e30; --border:#a5977a; }
[data-theme="black"][data-contrast="xhigh"] { --text:#ffffff; --text-muted:#e4e4ec; --border:rgba(196,210,255,.55); --bg:#000000; --card:#101014; }

/* ============================================================================
   PREMIUM OVERVIEW COMPONENTS  —  reusable, token-based, theme-aware.
   The elevated look (generous space, clear type hierarchy, calm accent use)
   the platform dashboard is built on; other pages adopt these incrementally.
   ============================================================================ */
.ov-header { display:flex; justify-content:space-between; align-items:flex-end; gap:16px; margin:2px 0 26px; flex-wrap:wrap; }
.ov-eyebrow { font-size:11px; font-weight:600; color:var(--primary-dark); }
.ov-title { font-size:31px; font-weight:600; letter-spacing:-.015em; line-height:1.08; margin:6px 0 5px; color:var(--text); }
.ov-sub { font-size:14px; color:var(--text-muted); max-width:60ch; }

.stat-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:22px; }
.stat { background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:16px 20px 16px; transition:box-shadow var(--transition); }
.stat:hover { box-shadow:var(--shadow); }
.stat .l { font-size:11px; font-weight:600; color:var(--text-muted); }
.stat .v { font-size:31px; font-weight:600; letter-spacing:-.02em; line-height:1; font-variant-numeric:tabular-nums; margin:10px 0 9px; color:var(--text); }
.stat .d { display:inline-flex; align-items:center; gap:4px; font-size:12px; font-weight:600; border-radius:var(--radius-lg); padding:2px 8px; background:var(--success-light); color:var(--success-dark); }
.stat .d.flat { background:var(--bg); color:var(--text-muted); border:1px solid var(--border); }

.panels { display:grid; grid-template-columns:1.7fr 1fr; gap:16px; align-items:start; }
.panel { background:var(--card); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; margin-bottom:16px; }
.panel:last-child { margin-bottom:0; }
.panel-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px 12px; border-bottom:1px solid var(--border); }
.panel-head h3 { font-size:17px; font-weight:600; letter-spacing:-.01em; margin:0; color:var(--text); }
.panel-head a { font-size:12px; font-weight:600; color:var(--primary-dark); text-decoration:none; }
.panel-head a:hover { text-decoration:underline; }

.dtable { width:100%; border-collapse:collapse; }
.dtable thead th { text-align:left; font-size:11px; font-weight:600; color:var(--text-muted); padding:12px 20px 12px; }
.dtable thead th.num, .dtable td.num { text-align:right; font-variant-numeric:tabular-nums; }
.dtable tbody td { padding:12px 20px; border-top:1px solid var(--border); font-size:14px; vertical-align:middle; color:var(--text); }
.dtable tbody tr { transition:background var(--transition); }
.dtable tbody tr:hover { background:var(--bg); }
.dtable td.num { color:var(--text-muted); }
.dtable td.time { white-space:nowrap; color:var(--text-muted); font-variant-numeric:tabular-nums; }
.dtable td.det { color:var(--text-muted); font-size:12px; }
.apill { display:inline-flex; font-size:11px; font-weight:600; border-radius:var(--radius-sm); padding:2px 8px; background:var(--primary-light); color:var(--primary-dark); }
.apill.g { background:var(--tint-ok-bg); color:var(--tint-ok-fg); }
.apill.a { background:var(--tint-warn-bg); color:var(--tint-warn-fg); }
.apill.d { background:var(--tint-danger-bg); color:var(--tint-danger-fg); }

.entity { display:flex; align-items:center; gap:12px; }
.entity .av { width:32px; height:32px; border-radius:var(--radius-sm); background:var(--primary-light); color:var(--primary-dark); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:600; flex:none; border:1px solid var(--border); }
.entity .nm { font-weight:600; letter-spacing:-.005em; }
.entity .cc { font-size:12px; color:var(--text-muted); margin-top:1px; }

.spill { display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:600; border-radius:var(--radius-lg); padding:2px 8px; background:var(--tint-ok-bg); color:var(--tint-ok-fg); }
.spill .dot { width:5px; height:5px; border-radius:50%; background:currentColor; }
.spill.off { background:var(--bg); color:var(--text-muted); }

.ghost-btn { font-size:12px; font-weight:600; color:var(--text); border:1px solid var(--border); background:var(--card); border-radius:var(--radius-sm); padding:6px 12px; cursor:pointer; text-decoration:none; display:inline-block; transition:background var(--transition); }
.ghost-btn:hover { background:var(--bg); }

.feed { padding:6px 0; }
.feed-row { display:flex; gap:12px; padding:8px 20px; }
.feed-row .fd { width:8px; height:8px; border-radius:50%; margin-top:5px; flex:none; background:var(--primary); }
.feed-row .ft { font-size:13px; line-height:1.45; color:var(--text); }
.feed-row .ft b { font-weight:600; }
.feed-row .fm { font-size:12px; color:var(--text-muted); margin-top:2px; }

@media (max-width:900px) { .panels { grid-template-columns:1fr; } .stat-grid { grid-template-columns:repeat(2,1fr); } }
@media (max-width:520px) { .stat-grid { grid-template-columns:1fr; } }

/* ============================================================================
   LIQUID GLASS  —  Apple macOS-style translucency + Anthropic warmth.
   Frosted, layered surfaces over a soft theme-tinted backdrop. Applies across the
   whole app shell; the tint follows the per-user theme (uses var(--primary)).
   Each glass value has an rgba fallback first, then a color-mix() upgrade, so
   older engines degrade gracefully.
   ============================================================================ */
:root {
  --glass-bg: rgba(255,255,255,0.60);
  --glass-bg: color-mix(in srgb, var(--card) 66%, transparent);
  --glass-strong: rgba(255,255,255,0.74);
  --glass-strong: color-mix(in srgb, var(--card) 82%, transparent);
  --glass-brd: rgba(255,255,255,0.60);
  --glass-brd: color-mix(in srgb, var(--card) 62%, var(--border));
  --glass-blur: saturate(1.3) blur(20px);
  /* ★ THIS WAS A SEPIA SHADOW ON EVERY CARD IN THE APP, ON ALL FOURTEEN THEMES. rgba(60,48,38)
     is brown, and `.card` reads --glass-shadow — so every surface cast a warm shadow whatever
     the theme's palette was, which is exactly what makes a cool theme read muddy. It survived
     because a token is read, not looked at: the value is right for `warm` and was declared in
     :root. Found by collapsing elevation, not by eye. */
  --glass-shadow: var(--el-3), inset 0 1px 0 rgba(255,255,255,0.5);
}

/* Backdrop — a soft gradient that picks up the theme accent as gentle colour blooms */
.app-layout { background: var(--bg); }
.app-layout {
  background:
    radial-gradient(780px 460px at 8% -10%, color-mix(in srgb, var(--primary) 20%, transparent), transparent 60%),
    radial-gradient(700px 520px at 100% 112%, color-mix(in srgb, var(--primary) 13%, transparent), transparent 56%),
    radial-gradient(520px 380px at 84% 2%, color-mix(in srgb, var(--primary) 8%, transparent), transparent 60%),
    linear-gradient(158deg, var(--bg), color-mix(in srgb, var(--bg) 88%, #000 8%));
  background-attachment: fixed;
}

/* Sidebar → frosted glass */
.sidebar {
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-right: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}

/* Topbar → the SAME surface as the page, diffused.
   It used to paint var(--glass-strong), i.e. 90% of --card, on top of the canvas wash — so the bar
   came out whiter than everything beneath it and read as a separate white slab. That is the whole
   complaint: the sidebar and canvas were made continuous and the bar was left behind.
   Transparent + blur is also what a real iOS/macOS toolbar is: it has no colour of its own, it
   takes the colour of whatever is behind it and diffuses what scrolls under. */
.topbar {
  background: transparent;
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}

/* Cards, panels, tiles → floating glass */
.card, .panel, .stat, .kpi-card, .ds-card {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--glass-shadow);
}
/* ★ A CARD TAKES THE THEME'S CORNER, like every other surface.
   This line used to read `.card, .panel { border-radius: 16px }` — a literal, at top level, so it
   won every theme. Three things went wrong at once:
     · the themes each choose a radius on purpose (white 12px, flat 8px, soft 16px) and a card
       ignored all of them — a "flat" look with 16px cards is not flat;
     · --radius stayed 12px while cards were 16px, so EVERY other panel that correctly reads
       var(--radius) — .modal-dialog, .kpi-card, .review-card, .table-wrapper, .st-tile and 111
       more — sat beside a card with a visibly different corner. That mismatch is what was
       reported on the Stores desk, and it was never a Stores bug;
     · it was written inside the glass block, so a look-specific decision leaked to every look.
   Nothing replaces it: .card already sets border-radius: var(--radius) at line 959. */

/* …EXCEPT a modal. Glass suits a tile resting on the page, but a dialog sits over arbitrary content:
   at 66% opacity a 1px table border has almost no contrast against whatever shows through, so
   requisition rows appeared to have no lines at all until hover painted a solid background behind
   one. A dialog — especially a document you print — is a sheet. Opaque in every theme. */
.ovl > .card, #preview-body, .modal .modal-content, .khtfw-modal {
  background: var(--card);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}

/* FLOATING MENUS are the opposite case, and were getting nothing at all. A dropdown panel, the
   sidebar flyout, the export menu and the account popover are chrome hovering over the page, not
   sheets you read a table on — they are exactly what glass is for, and the topbar beside them is
   already frosted. Without this the Appearance setting promised "dialogs and every card frosted"
   and then left eight surfaces flat. */
.ds-pop, .nav-fly, .ui-expmenu, .cacct-pop, .ox-cardpop, .ch-emoji {
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-color: var(--glass-brd);
}
/* (was `.stat, .kpi-card { border-radius: 15px }` — a glass-section literal at top level. It beat
   every theme and cancelled .kpi-card's own border-radius:var(--radius). Both read the token now,
   so a stat, a KPI, a panel and a card finally share one corner.) */
.card:hover, .panel:hover, .stat:hover, .kpi-card:hover {
  box-shadow: var(--el-4), inset 0 1px 0 rgba(255,255,255,0.55);
}

/* THE PREMISES PICKER TAKES THE NAV'S OWN SURFACE.
   The top bar is deliberately transparent — it has no colour of its own and diffuses whatever is
   behind it. The picker inside it was an opaque var(--card) box, which on the seven themes where
   --card is #ffffff read as a white slab pasted onto a tinted bar. Giving it the SIDEBAR's surface
   (--glass-strong + the same blur) makes the one control in the chrome match the chrome. */
.topbar .ds-sel {
  /* Measured, not assumed: the sidebar computes to rgba(0,0,0,0) in BOTH glass modes and only takes
     a colour (var(--bg)) when glass is off. So matching it means being transparent and blurring what
     is behind — which is also exactly what the top bar itself does. The border is what keeps it
     readable as a control once the fill is gone. */
  background: transparent;
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-color: color-mix(in srgb, var(--border) 75%, transparent);
}
/* Glass off: the sidebar falls back to var(--bg), so the picker does too — still matching the nav. */
[data-glass="off"] .topbar .ds-sel {
  background: var(--bg);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border-color: var(--border);
}
/* Full glass turns the sidebar fully transparent. The picker does NOT follow it there — a control
   you are meant to press cannot dissolve completely — so it keeps the frosted surface. */

/* Active nav item → vibrant accent glass pill */
.nav-item.active, .nav-item.nav-parent.expanded {
  background: color-mix(in srgb, var(--primary) 15%, transparent);
  color: var(--primary-dark);
  box-shadow: 0 3px 10px color-mix(in srgb, var(--primary) 22%, transparent), inset 0 1px 0 rgba(255,255,255,0.4);
  border: 1px solid color-mix(in srgb, var(--primary) 28%, transparent);
}

/* Primary actions gain a soft accent glow + top highlight */
.btn-primary, .ds-btn, .btn.btn-primary {
  box-shadow: 0 6px 16px color-mix(in srgb, var(--primary) 30%, transparent), inset 0 1px 0 rgba(255,255,255,0.35);
}

/* The picker modal + user-menu popovers read as glass too */
.apx-modal, .side-user-menu, .user-menu {
  background: var(--glass-strong);
  -webkit-backdrop-filter: saturate(1.3) blur(30px);
  backdrop-filter: saturate(1.3) blur(30px);
}

/* ============================================================================
   PURE CLAUDE.AI SIDEBAR — flat warm paper, neutral monochrome states, line icons.
   Overrides the glass on the sidebar chrome: the nav carries NO accent colour, so
   colour lives in the content. Neutral tints derive from --text so they follow the
   theme's temperature (warm-grey on warm, cool-grey on graphite).
   ============================================================================ */
.sidebar {
  /* TRANSPARENT, not var(--bg). This block's whole idea is that the nav is the SAME paper as the
     canvas, separated by nothing but a hairline — but Liquid Glass later layered an ambient tinted
     wash onto .app-layout only, so "same paper" quietly stopped being true: the two matched at the
     top of the viewport and drifted to roughly 8% apart plus a blue cast by the bottom. A step that
     VARIES down the screen reads as a rendering artifact rather than a decision.
     The sidebar sits inside .app-layout and that gradient is background-attachment:fixed, so it is
     already painted underneath — letting it through makes the two genuinely continuous again.
     Themes that dress their own sidebar on purpose (black, flat) and [data-glass] both carry higher
     specificity, so they are unaffected. Mobile is restored to opaque below — see the media query. */
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-right: 1px solid var(--border);
  overflow: hidden;   /* brand + account stay pinned; only the nav scrolls */
}
.sidebar-brand { flex: 0 0 auto; }
.sidebar-nav { flex: 1 1 auto; overflow-y: auto; min-height: 0; }
.side-foot { flex: 0 0 auto; }

/* Minimal sidebar scrollbar — thin, subtle, appears on hover.
   WebKit (Chrome/Safari): 4px custom bar. NOTE: Chrome ignores ::-webkit-scrollbar when the
   standard `scrollbar-width` is also set, so we give that ONLY to Firefox (which lacks the
   ::-webkit-scrollbar selector) via @supports — otherwise Chrome falls back to a ~11px "thin" bar. */
.sidebar::-webkit-scrollbar, .sidebar-nav::-webkit-scrollbar { width: 4px; height: 4px; }
.sidebar::-webkit-scrollbar-track, .sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.sidebar::-webkit-scrollbar-thumb, .sidebar-nav::-webkit-scrollbar-thumb { background: transparent; border-radius: var(--radius-sm); }
.sidebar:hover::-webkit-scrollbar-thumb, .sidebar-nav:hover::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--text) 22%, transparent); }
.sidebar::-webkit-scrollbar-thumb:hover, .sidebar-nav::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--text) 40%, transparent); }
@supports not (selector(::-webkit-scrollbar)) {
  .sidebar, .sidebar-nav { scrollbar-width: thin; scrollbar-color: transparent transparent; }
  .sidebar:hover, .sidebar-nav:hover { scrollbar-color: color-mix(in srgb, var(--text) 22%, transparent) transparent; }
}
.nav-item { color: color-mix(in srgb, var(--text) 72%, transparent); font-weight: 450; }
.nav-item:hover { background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text); }
.nav-item.active, .nav-item.nav-parent.expanded {
  background: color-mix(in srgb, var(--text) 9%, transparent);
  color: var(--text);
  font-weight: 500;
  box-shadow: none;
  border: none;
}
.nav-item .nav-icon { color: color-mix(in srgb, var(--text) 40%, transparent); }
.nav-item:hover .nav-icon, .nav-item.active .nav-icon, .nav-item.nav-parent.expanded .nav-icon { color: color-mix(in srgb, var(--text) 58%, transparent); }
/* Account row + popover at the bottom (Claude.ai account menu) */
.cacct { position: relative; display: flex; align-items: center; gap: 8px; padding: 8px 8px; border-radius: var(--radius); cursor: pointer; }
.cacct:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
.cacct .cav { width: 29px; height: 29px; border-radius: 50%; background: color-mix(in srgb, var(--text) 12%, transparent); color: var(--text); display: flex; align-items: center; justify-content: center; font-size:12px; font-weight: 600; flex: none; }
.cacct .cnm { font-size:13px; font-weight: 500; color: var(--text); line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ★ A 48% WASH FAILED IN EVERY THEME — 2.22:1 under Soft, 4.28:1 at best under Black, never once
   reaching 4.5:1. Even 75% still failed under Soft. An arbitrary alpha of --text is not a colour
   the themes can reason about; --text-muted is, and it is now above the floor everywhere. */
.cacct .crl { font-size:11px; color: var(--text-muted); }
.cacct .cdl { margin-left: auto; color: color-mix(in srgb, var(--text) 42%, transparent); display: flex; flex: none; }
/* ★ `--menu-bg` IS A CHANNEL, NOT A DEFAULT. A theme needs to be able to answer this ONE surface
   without winning a specificity war against the four other rules that also paint it (the overlay
   list, the shared pane recipe, the floating-surface list, and a direct .cacct-pop rule). Three
   attempts to override it from the theme block failed — last-in-file, then scoped through
   .sidebar at (0,3,0) — and the reason never became provable from the CSSOM. A token read at the
   point of use cannot be outranked: whoever wins the cascade still reads --menu-bg.
   Same pattern as --toast-bg in public/js/core/api.js and --over-bg on #notif-panel. The
   fallback keeps all twelve other themes byte-identical. */
.cacct-pop { position: absolute; left: 0; right: 0; bottom: calc(100% + 6px); background: var(--menu-bg, var(--card)); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-lg); padding: 6px; display: none; z-index: 70; }
.cacct-pop.show { display: block; }
/* ★ 48% OF --text IS NOT A COLOUR, IT IS A CONTRAST BUG. The same color-mix wash was already taken
   off `.cacct .crl` two lines above for failing AA; this line kept it and measured below the floor
   on both grounds. --text-muted is the token for secondary ink and it is answered per theme. */
.cacct-mail { font-size:12px; color: var(--text-muted); padding: 8px 12px 6px; word-break: break-all; }
.cacct-sec { font-size:11px; font-weight: 600; color: color-mix(in srgb, var(--text) 40%, transparent); padding: 8px 12px 4px; }
.cacct-pop .pit { display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px 12px; border: none; background: none; border-radius: var(--radius-sm); font: inherit; font-size:13px; font-weight: 450; color: color-mix(in srgb, var(--text) 82%, transparent); cursor: pointer; text-align: left; text-decoration: none; }
.cacct-pop .pit:hover { background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text); }
.cacct-pop .pit svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 1.7; color: color-mix(in srgb, var(--text) 45%, transparent); flex: none; }
.cacct-pop .pit .pr { margin-left: auto; font-size:12px; color: color-mix(in srgb, var(--text) 40%, transparent); }
.cacct-sep { height: 1px; background: var(--border); margin: 5px 8px; }
.app-layout.collapsed .cacct .cnm-wrap, .app-layout.collapsed .cacct .cdl { display: none; }
.app-layout.collapsed .cacct { justify-content: center; }

/* ===== Org chart (shared) — nested lists drawn as a branching top-down tree with connector lines.
   Used by the Roles tab, the Org & Roles handbook and the printable export via /js/org-chart.js. ===== */
.oc-wrap { overflow-x: auto; padding: 8px 0 4px; }
.oc-tree ul { position: relative; padding: 16px 0 0; white-space: nowrap; margin: 0; text-align: center; list-style: none; }
.oc-tree ul::after { content: ''; display: table; clear: both; }
.oc-tree li { display: inline-block; vertical-align: top; text-align: center; list-style-type: none; position: relative; padding: 16px 6px 0; }
.oc-tree li::before, .oc-tree li::after { content: ''; position: absolute; top: 0; right: 50%; border-top: 2px solid var(--border); width: 50%; height: 18px; }
.oc-tree li::after { right: auto; left: 50%; border-left: 2px solid var(--border); }
.oc-tree li:only-child::after, .oc-tree li:only-child::before { display: none; }
.oc-tree li:only-child { padding-top: 0; }
.oc-tree li:first-child::before, .oc-tree li:last-child::after { border: 0 none; }
.oc-tree li:last-child::before { border-right: 2px solid var(--border); border-radius: 0 var(--radius-sm) 0 0; }
.oc-tree li:first-child::after { border-radius: var(--radius-sm) 0 0 0; }
.oc-tree ul ul::before { content: ''; position: absolute; top: 0; left: 50%; border-left: 2px solid var(--border); width: 0; height: 18px; }
.oc-node { display: inline-block; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--card); padding: 6px 12px; white-space: normal; min-width: 118px; max-width: 190px; text-align: left; box-shadow: var(--el-1); }
.oc-node.corp { background: var(--primary-light); border-color: var(--primary); }
.oc-node.lead { background: var(--success-light); border-color: var(--success); }
.oc-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; vertical-align: middle; }
.oc-name { font-size:13px; font-weight:600; color: var(--text); line-height: 1.3; }
.oc-tag { font-size:9px; font-weight:600; color: var(--primary-dark); background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0 4px; margin-left: 5px; white-space: nowrap; }
.oc-sub { font-size:11px; color: var(--text-muted); margin-top: 2px; line-height: 1.3; }

/* Left-to-right (horizontal) org tree — root at the left, branches grow DOWN the page, so a deep org
   fits a portrait sheet. Enable by adding .oc-h to .oc-tree (OrgChart.render(..,{orientation:'horizontal'})).
   Overrides the default top-down connectors above; each rule resets border/side the top-down set. */
.oc-tree.oc-h ul { display: flex; flex-direction: column; justify-content: center; margin: 0; padding: 0 0 0 32px; position: relative; white-space: normal; text-align: left; }
.oc-tree.oc-h > ul { padding-left: 0; }
.oc-tree.oc-h li { display: flex; flex-direction: row; align-items: center; position: relative; padding: 2px 0; text-align: left; }
.oc-tree.oc-h ul li::before { content: ''; position: absolute; left: -17px; right: auto; top: 50%; width: 17px; height: 2px; border: 0; border-radius: 0; background: var(--border); }
.oc-tree.oc-h ul li::after { content: ''; position: absolute; left: -17px; right: auto; top: -3px; bottom: -3px; width: 2px; height: auto; border: 0; border-radius: 0; background: var(--border); }
.oc-tree.oc-h ul li:first-child::after { top: 50%; }
.oc-tree.oc-h ul li:last-child::after { bottom: 50%; }
.oc-tree.oc-h ul li:only-child { padding-top: 2px; }
.oc-tree.oc-h ul li:only-child::after { display: none; }
.oc-tree.oc-h > ul > li::before, .oc-tree.oc-h > ul > li::after { display: none; }
.oc-tree.oc-h li > ul::before { content: ''; position: absolute; left: 0; top: 50%; width: 17px; height: 2px; border: 0; background: var(--border); }

/* OPEN unit group reads as ONE contained, slightly-darker block (owner): the expanded header keeps
   the theme's active treatment and fuses with its submenu via a soft text-tint — color-mix follows
   every theme (darker on light themes, gently lifted on black). */
.nav-item.nav-parent.expanded { border-radius: var(--radius-sm) var(--radius-sm) 0 0; margin-bottom: 0; }
.nav-item.nav-parent.expanded + .nav-sub {
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  margin-left: 0;
  border-left: none;
  padding: 4px 6px 6px 16px;
  margin-bottom: 4px;
}

/* ===== UI KIT (public/js/core/ui.js) — the one page contract for module/list pages ===== */
/* ★ A HINT IS STILL TEXT. At 62% of --text-muted the column headers and every row's meta line
   measured 2.3–3.3:1 across eleven themes — the dimmest real content in the app. 12px needs 4.5;
   --text-muted itself is tuned to clear that on every theme, so hint text is muted text, and the
   hierarchy is carried by size and weight rather than by fading ink below what the eye can read. */
:root { --text-hint: var(--text-muted, #64748b); }
.ui-pill { display:inline-flex; align-items:center; gap:6px; font-size:11px; font-weight:600;
  padding:2px 8px; border-radius:var(--radius); white-space:nowrap; letter-spacing:.02em; }
.ui-pill-dot { width:6px; height:6px; border-radius:50%; flex:none; }
.ui-stats { display:grid; grid-template-columns:repeat(auto-fill,minmax(158px,1fr)); gap:12px; margin:14px 0; }
.ui-stat { padding:12px 16px; }
.ui-stat-top { display:flex; align-items:flex-start; justify-content:space-between; gap:8px; }
.ui-stat-val { font-size:25px; font-weight:600; line-height:1; font-variant-numeric:tabular-nums; }
.ui-stat-ico { width:28px; height:28px; border-radius:var(--radius-sm); display:flex; align-items:center; justify-content:center; flex:none; }
.ui-stat-label { font-size:11px; color:var(--text-muted); margin-top:7px; font-weight:600; }
.ui-stat-hint { font-size:11px; color:var(--text-muted); margin-top:3px; }
.ui-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; padding:32px 16px; }
.ui-empty-ico { width:52px; height:52px; border-radius:var(--radius); background:var(--tint-neutral-bg,#f1f5f9);
  color:var(--text-muted); display:flex; align-items:center; justify-content:center; margin-bottom:12px; }
.ui-empty-title { font-weight:600; color:var(--text); font-size:15px; }
.ui-empty-hint { font-size:13px; color:var(--text-muted); margin-top:5px; max-width:46ch; line-height:1.5; }
.skel { display:inline-block; height:12px; border-radius:var(--radius-sm); background:linear-gradient(90deg,
  var(--tint-neutral-bg,#f1f5f9) 25%, var(--border,#e2e8f0) 50%, var(--tint-neutral-bg,#f1f5f9) 75%);
  background-size:200% 100%; animation:skel-slide 1.2s ease-in-out infinite; }
@keyframes skel-slide { from { background-position:200% 0; } to { background-position:-200% 0; } }
@media (prefers-reduced-motion: reduce) { .skel { animation:none; } }
.ui-tab-n { display:inline-block; min-width:17px; padding:0 4px; border-radius:var(--radius-sm); font-size:11px;
  font-weight:600; background:var(--tint-neutral-bg,#f1f5f9); color:var(--text-muted); text-align:center; }
.ui-tab-n.on { background:rgba(255,255,255,.25); color:#fff; }
/* The second count on a tab — the one that wants attention. Inside the button, in warning colour:
   a number sitting outside the control is one people read as decoration and stop seeing. */
.ui-tab-alert { display:inline-block; padding:0 6px; border-radius:var(--radius-sm); font-size:11px; font-weight:600;
  background:var(--warning-light,#fef3c7); color:#b45309; margin-left:2px; white-space:nowrap; }
.btn-primary .ui-tab-alert { background:rgba(255,255,255,.9); color:#b45309; }
.ui-counter { font-size:11px; color:var(--text-hint,var(--text-muted)); margin-top:3px; text-align:right; font-variant-numeric:tabular-nums; }
.ui-rail { border-radius:var(--radius-sm); padding:8px 12px; font-size:12px; margin-top:10px; }
.ui-rail.ok { background:var(--success-light,#d1fae5); color:var(--tint-ok-fg,#065f46); display:flex; align-items:center; gap:6px; font-weight:600; }
.ui-rail.warn { background:var(--tint-warn-bg,#fef3c7); color:var(--tint-warn-fg,#92400e); }
.ui-rail-item { display:inline-block; background:var(--card,#fff); border:1px solid var(--border);
  border-radius:var(--radius-sm); padding:2px 8px; margin:5px 5px 0 0; font-size:12px; font-weight:600; color:var(--text); }
/* ui.deptClearance — "which other departments must clear this work?", shared by the PM schedule form
   and the work-order form so the two can never drift into different ideas of a department. */
.ui-dc-acc { border:1px solid var(--border); border-radius:var(--radius-sm); padding:8px 12px; margin-bottom:8px; }
.ui-dc-body { display:none; margin-top:10px; padding-top:8px; border-top:1px solid var(--border); }
.ui-dc-body.open { display:block; }
.ui-dc-n { font-size:12px; color:var(--text-muted); }
.ui-dc-unit { font-size:11px; color:var(--text-muted); margin-bottom:5px; }
.ui-dc-row { display:flex; gap:8px; align-items:flex-start; cursor:pointer; margin-bottom:6px; }
.ui-dc-row input { margin-top:3px; }
.ui-dc-row input:disabled { cursor:not-allowed; }
.ui-dc-who { display:block; font-style:normal; font-size:12px; color:var(--text-muted); margin-top:2px; }
.ui-dc-count { font-size:12px; color:var(--text-muted); margin-top:6px; }
/* Page header. The module skin (.ox-head) is the target look; these older names are kept as
   aliases so every page that already uses them lands on the same header without being rewritten. */
.ui-page-head { display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; margin-bottom:4px; }
.ui-page-head h2 { font-size:31px; font-weight:600; letter-spacing:-.02em; color:var(--text); margin:2px 0 4px !important; }
.ui-page-eyebrow { font-size:11px; font-weight:600; color:var(--primary-dark); }
.ui-page-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
/* The small secondary buttons these headers use become the skin's chips, and the primary its hero. */
.ui-page-actions .btn-sm { height:40px; padding:0 16px; border-radius:var(--radius-sm); font-size:14px; font-weight:600; }
.ui-page-actions .btn-secondary { background:var(--card); border:1px solid var(--border-strong,#cbd5e1); color:var(--text); }
.ui-page-actions .btn-secondary:hover { background:var(--bg); }
.ui-page-actions .btn-primary { background:linear-gradient(135deg,var(--primary),var(--primary-dark)); border:none; color:var(--on-primary, #fff); font-weight:600; }
.ui-page-actions .form-control { height:40px; border-radius:var(--radius-sm); }

/* ===== SELECT (public/js/core/select.js) — our dropdown, not the OS one ===== */
/* Sized from the SAME box model as .form-control — padding + font, not a fixed height. It used to be
   a hard 44px against a 39px text input and a 36px button, which is what made every dropdown look
   fat next to the field beside it. Deriving it instead also means it still tracks the per-user text
   size setting: a hardcoded height would stay put while everything around it grew. */
.ds-sel { display:flex; align-items:center; gap:8px; width:100%; padding:8px 12px;
  border:1.5px solid var(--border); border-radius:var(--radius-sm); background:var(--card);
  font-size:15px; line-height:1.25; color:var(--text); cursor:pointer; box-sizing:border-box;
  transition:border-color .12s, box-shadow .12s; }
.ds-sel:hover { border-color:var(--primary); }
.ds-sel:focus-visible, .ds-sel.is-open { outline:none; border-color:var(--primary);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); }
.ds-sel.is-disabled { background:var(--bg); color:var(--text-muted); cursor:not-allowed; border-color:var(--border); }
/* Compact — for chrome (top bar, toolbars), where the 44px form-control height is far too heavy.
   Sizes to its content rather than filling the row, with a floor so a short plant name still gives
   the control a sane width. */
.ds-sel.is-sm { height:32px; padding:0 8px; line-height:1; gap:6px; font-size:13px; border-radius:var(--radius-sm);
  width:auto; min-width:160px; max-width:260px; border-color:var(--border); }
.ds-sel.is-sm .ds-sel-caret svg { width:14px; height:14px; }
.ds-sel-val { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Two-line value (data-sub-in-value): the name, with its context small beneath it. */
.ds-sel-val.has-sub { display:flex; flex-direction:column; justify-content:center; line-height:1.15; gap:0; }
.ds-sel-val.has-sub .ds-sel-main { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:600; }
.ds-sel-val.has-sub .ds-sel-sub { font-size:11px; font-weight:400; color:var(--text-muted);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* The compact variant is a fixed 32px for one line — a second line needs the room. */
.ds-sel.is-sm:has(.ds-sel-val.has-sub) { height:auto; min-height:40px; padding:4px 8px; }
.ds-sel-val.is-ph { color:var(--text-hint,var(--text-muted)); }
.ds-sel-caret { display:flex; color:var(--text-muted); flex:none; transition:transform .15s; }
.ds-sel.is-open .ds-sel-caret { transform:rotate(180deg); color:var(--primary); }

/* THIN SCROLLBARS on every floating panel. The OS default is ~15px, which inside a 250px dropdown
   is a bar almost as wide as the tick column — it reads as part of the control rather than as
   chrome. Same 4px treatment the sidebar already uses, and the same browser split it documents:
   Chrome IGNORES ::-webkit-scrollbar as soon as the standard `scrollbar-width` is also set, so that
   property is handed to Firefox alone via @supports. Applied to the popovers together because they
   are the same kind of surface and a thin bar in one with a fat bar in the next reads as a bug. */
.ds-list::-webkit-scrollbar, .ds-pop::-webkit-scrollbar, .cacct-pop::-webkit-scrollbar, .nav-fly::-webkit-scrollbar,
.apx-modal::-webkit-scrollbar, .ovl::-webkit-scrollbar, .ui-expmenu::-webkit-scrollbar,
.ch-emscroll::-webkit-scrollbar, .ch-list::-webkit-scrollbar, .ch-body::-webkit-scrollbar {
  /* 4px — the same weight as the sidebar rail, so every scrollbar in the app is one thickness. */
  width: 4px; height: 4px;
}
.ds-list::-webkit-scrollbar-track, .ds-pop::-webkit-scrollbar-track, .cacct-pop::-webkit-scrollbar-track, .nav-fly::-webkit-scrollbar-track,
.apx-modal::-webkit-scrollbar-track, .ovl::-webkit-scrollbar-track, .ui-expmenu::-webkit-scrollbar-track,
.ch-emscroll::-webkit-scrollbar-track, .ch-list::-webkit-scrollbar-track, .ch-body::-webkit-scrollbar-track {
  background: transparent;
}
.ds-list::-webkit-scrollbar-thumb, .ds-pop::-webkit-scrollbar-thumb, .cacct-pop::-webkit-scrollbar-thumb, .nav-fly::-webkit-scrollbar-thumb,
.apx-modal::-webkit-scrollbar-thumb, .ovl::-webkit-scrollbar-thumb, .ui-expmenu::-webkit-scrollbar-thumb,
.ch-emscroll::-webkit-scrollbar-thumb, .ch-list::-webkit-scrollbar-thumb, .ch-body::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text) 22%, transparent);
  border-radius: var(--radius-sm);
}
.ds-list::-webkit-scrollbar-thumb:hover, .ds-pop::-webkit-scrollbar-thumb:hover, .cacct-pop::-webkit-scrollbar-thumb:hover,
.nav-fly::-webkit-scrollbar-thumb:hover, .apx-modal::-webkit-scrollbar-thumb:hover,
.ovl::-webkit-scrollbar-thumb:hover, .ui-expmenu::-webkit-scrollbar-thumb:hover,
.ch-emscroll::-webkit-scrollbar-thumb:hover, .ch-list::-webkit-scrollbar-thumb:hover,
.ch-body::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--text) 38%, transparent);
}
@supports not (selector(::-webkit-scrollbar)) {
  .ds-list, .ds-pop, .cacct-pop, .nav-fly, .apx-modal, .ovl, .ui-expmenu,
  .ch-emscroll, .ch-list, .ch-body {
    scrollbar-width: thin;
    scrollbar-color: color-mix(in srgb, var(--text) 25%, transparent) transparent;
  }
}

/* <optgroup> heading inside a ds-select popup — quiet, sticky-free, matches the nav group label. */
.ds-group { padding:8px 12px 4px; font-size:.7em; font-weight:600;
 color:color-mix(in srgb, var(--text) 45%, transparent); }
.ds-group + .ds-opt { border-top:none; }
/* A heading and its options must not share a left edge — the indent IS the grouping. A rule on the
   heading alone cannot do it, so the options are wrapped and the wrapper carries the step. */
.ds-grouped { padding-left: 8px; }
.ds-grouped .ds-opt { padding-left: 12px; }
.ds-grouped + .ds-group { margin-top: 4px; border-top: 1px solid var(--border); padding-top: 8px; }
.ds-pop { position:absolute; z-index:2000; display:none; background:var(--card);
  border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--el-4);
  overflow:hidden; min-width:180px; }
.ds-pop.show { display:block; animation:ds-in .13s ease-out; }
@keyframes ds-in { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:none; } }
@media (prefers-reduced-motion: reduce) { .ds-pop.show { animation:none; } }
.ds-search { padding:8px 8px; border-bottom:1px solid var(--border); }
.ds-search input { width:100%; height:34px; border:1px solid var(--border); border-radius:var(--radius-sm); padding:0 8px;
  font:inherit; font-size:.9em; color:var(--text); background:var(--bg); box-sizing:border-box; }
.ds-search input:focus { outline:none; border-color:var(--primary); }
.ds-list { max-height:262px; overflow-y:auto; padding:4px; }
/* em, not rem: the panel is given the trigger's font-size in JS, so the list reads at the size of
   the field it belongs to instead of a fixed global size. .93em keeps the default case visually
   identical to what it was (14.25px trigger -> 13.3px option) while a compact 12px trigger now gets
   an 11.2px list rather than a 13.2px one that dwarfed it. */
.ds-opt { display:flex; align-items:center; gap:8px; padding:8px 12px; border-radius:var(--radius-sm); cursor:pointer;
  font-size:.93em; color:var(--text); }
.ds-opt:hover { background:var(--bg); }
/* ★ THE COMPACT LIST, for the compact control — see `is-sm` in public/js/core/select.js. A one-word
   option in a 38px row surrounded by 9px of padding is a list that looks like it is holding
   sentences; and .ds-opt sizes in `em`, so in a popup that inherits the body's 12px the type came out
   at 11.16px — smaller than the closed box it was opened from. Both fixed here: rows sized for a
   word, type stated in rem so nothing it is appended to can shrink it. */
.ds-pop.is-sm { padding:4px; }
.ds-pop.is-sm .ds-opt { padding:4px 8px; border-radius:var(--radius-sm); font-size:13px; gap:6px; }
.ds-pop.is-sm .ds-opt-sub { font-size:12px; }
.ds-pop.is-sm .ds-group { padding:4px 8px 2px; font-size:11px; }
.ds-opt.is-sel { background:var(--primary-light); color:var(--primary-dark); font-weight:600; }
.ds-opt.is-off { opacity:.45; cursor:not-allowed; }
.ds-opt-main { flex:1; min-width:0; }
.ds-opt-label { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ds-opt-sub { display:block; font-size:.78em; color:var(--text-hint,var(--text-muted)); margin-top:1px; font-weight:400; }
/* Graded options (data-tone / data-icon / data-note) — an ordered scale shown as a scale. The pill
   and its note sit on one line so four steps read as a ramp, not as four paragraphs. */
.ds-opt.has-tone .ds-opt-main { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.ds-opt-ico { display:inline-flex; align-items:center; flex:none; margin-right:2px; }
.ds-opt-pill { display:inline-flex; align-items:center; font-size:.78em; font-weight:600; letter-spacing:.01em;
  padding:2px 8px; border-radius:var(--radius); white-space:nowrap; }
.ds-opt-note { font-size:.78em; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ds-sel-val > .ds-opt-pill { font-size:.8em; }
.ds-opt-hint { font-size:12px; color:var(--text-hint,var(--text-muted)); font-family:var(--font-mono,ui-monospace,monospace);
  letter-spacing:.02em; flex:none; }
.ds-opt-tick { display:none; color:var(--primary); flex:none; }
.ds-opt.is-sel .ds-opt-tick { display:flex; }
.ds-none { padding:16px; text-align:center; font-size:.87em; color:var(--text-hint,var(--text-muted)); }
/* Create-as-you-type row (select[data-add]) — pinned under the list so a missing value can be added
   without leaving the form. Reads as an action, not an option: primary colour, its own top border. */
.ds-add { display:flex; align-items:center; gap:8px; padding:8px 12px; border-top:1px solid var(--border);
  background:var(--bg); color:var(--primary-dark); font-size:13px; font-weight:600; cursor:pointer; }
.ds-add:hover { background:var(--primary-light); }
.ds-add.is-busy { opacity:.6; pointer-events:none; }
.ds-add-plus { display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px; flex:none;
  border-radius:var(--radius-sm); background:var(--primary); color:#fff; font-weight:600; line-height:1; }

/* ===== PAGE ENTRY — load, then reveal =====
   A module page fetches before it can show anything true. Rather than a blank flash or a bare
   "Loading…", the shell paints skeletons immediately, then the real content fades up in a short
   stagger so the eye follows the page assembling itself. */
.ui-enter { opacity:0; transform:translateY(10px); animation:ui-rise .42s cubic-bezier(.22,.8,.3,1) forwards; }
.ui-enter-1 { animation-delay:.02s; } .ui-enter-2 { animation-delay:.08s; }
.ui-enter-3 { animation-delay:.14s; } .ui-enter-4 { animation-delay:.20s; } .ui-enter-5 { animation-delay:.26s; }
@keyframes ui-rise { to { opacity:1; transform:none; } }
.ui-fadeout { animation:ui-fade .18s ease forwards; }
@keyframes ui-fade { to { opacity:0; } }

/* ── BUSY ─────────────────────────────────────────────────────────────────────────────────────
   A button that has been pressed and is waiting on the server must SAY so, or the reader presses
   it again. `.is-busy` spins whatever icon it holds and blocks a second press. */
@keyframes ui-spin { to { transform: rotate(360deg); } }
.ui-spin, .is-busy svg { animation: ui-spin .9s linear infinite; transform-origin: 50% 50%; }
.is-busy { pointer-events: none; opacity: .75; }
@media (prefers-reduced-motion: reduce) { .ui-spin, .is-busy svg { animation: none; } }
@media (prefers-reduced-motion: reduce) {
  .ui-enter { opacity:1; transform:none; animation:none; }
  .ui-fadeout { animation:none; opacity:0; }
}
.ui-skelcard { background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:16px 16px; }
.ui-skelrow { display:flex; align-items:center; gap:12px; padding:16px 20px; border-bottom:1px solid var(--border); }
.ui-skelcirc { width:38px; height:38px; border-radius:var(--radius-sm); flex:none; background:linear-gradient(90deg,
  var(--tint-neutral-bg,#f1f5f9) 25%, var(--border,#e2e8f0) 50%, var(--tint-neutral-bg,#f1f5f9) 75%);
  background-size:200% 100%; animation:skel-slide 1.2s ease-in-out infinite; }

/* ================================================================================================
   MODULE PAGE SKIN (.ox-*) — the one contract every plant/company module page follows:
   header → customisable summary cards → search band → filter chips → grouping views → tabs →
   badge rows → pagination. Approved on Equipment Master and Work Orders; reuse, never re-invent.
   Icons: icons.js turns <i class="ti"> into <span><svg>, so style the SPAN, never `.ti`.
   ============================================================================================== */
/* margin-bottom gives every module header a gap before its summary cards. Board pages already insert a
   spacer (the card-customiser bar) so their margins just collapse against it — no double gap; hand-built
   pages (Inspections, Purchasing, Material Issues, Shutdowns) get the breathing room they were missing. */
/* .ox-head is layout only — the wash it briefly carried moved up to .content-area, so that EVERY
   page gets one rather than the seven that happen to use this component. */
.ox-head { display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap;
  margin-bottom:20px; }
/* ★ THE ACTIONS STAY ON THE RIGHT. FLEXBOX WRAPS BEFORE IT SHRINKS.
   Line-breaking uses each item's hypothetical size, and the title block's flex-basis was `auto` —
   its full content width. So a long tagline (PM's is 130 characters, 879px) made the block wider
   than the row could hold beside the buttons, the whole .ox-acts was pushed to a second line, and
   `justify-content:space-between` with one item left on that line put it on the LEFT. Eleven of
   nineteen module pages were doing this. No amount of flex-shrink helped: shrinking happens only
   AFTER items are placed on a line, and by then the buttons were already on their own.
   `flex-basis:0` makes the title contribute nothing to that decision, so the buttons keep their
   place and the title grows into whatever is left — the tagline wraps to two lines instead, which
   is also shorter than surrendering a whole row (125px -> 90px headers).
   min-width is the floor at which the title stops giving way; below it the buttons wrap among
   themselves, which is the honest outcome for a header carrying too many. */
.ox-head > :first-child { flex:1 1 0; min-width:260px; }
.ox-head > .ox-acts { flex:0 1 auto; justify-content:flex-end; }
/* The live facts that sit on the band — one or two, and only ones worth a sentence. */
.ox-facts { display:flex; gap:8px; flex-wrap:wrap; margin-top:13px; }
.ox-facts:empty { display:none; }

/* ── WHAT IS KNOWN ABOUT ONE THING, LABELLED ──────────────────────────────────────────────────────
   ★ THE APP HAD NO WAY TO STATE A SET OF FACTS, so every detail page invented one. The candidate
   profile ran Phone, Email, "Came to us by" and the CV together into a single wrapping line of
   `<span><i>label</i> value</span>` — at a glance one strip of grey text, with no column to scan
   down and no way to see at a distance which fact was missing.
   A grid, with the label above the value in the same small-uppercase idiom .ox-rowright already
   uses for the fact on the end of a list row — so a fact reads the same whether it is on a row or
   on a page. auto-fit means it is three columns on a desk and one on a phone without a media query.
   `.none` is for a fact that has no answer yet: still shown, because "no CV on file" is itself
   worth knowing, but visibly lighter than one that does. */
.ui-kv { display:grid; grid-template-columns:repeat(auto-fit,minmax(168px,1fr)); gap:12px 20px; margin-top:14px; }
.ui-kv > div { min-width:0; }
.ui-kv small { display:block; font-size:11px;
  color:var(--text-hint,var(--text-muted)); font-weight:600; margin-bottom:3px; }
.ui-kv b { font-size:14px; font-weight:600; color:var(--text); overflow-wrap:anywhere; }
.ui-kv b.none { color:var(--text-muted); font-weight:500; }
.ui-kv a { font-size:14px; font-weight:600; color:var(--primary); text-decoration:none;
  display:inline-flex; align-items:center; gap:4px; }
.ui-kv a:hover { text-decoration:underline; }
/* ★ WHAT YOUR ACCESS REACHES IS A FACT, NOT AN ACTION. Work Orders and PM both stated it as a
   PERMANENTLY DISABLED .ox-chip sitting in the row of buttons — same wording, same title attribute,
   two copies. A button that can never be pressed teaches people the row is not all pressable, and it
   spent ~160px of the action row on both pages, which is part of why their buttons wrapped. It reads
   as what it is here: a quiet statement beside the tagline. */
.ox-scope { display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:600;
  color:var(--text-muted); background:var(--bg); border:1px solid var(--border);
  border-radius:var(--radius-pill); padding:2px 8px; }
.ox-scope > span, .ox-scope svg { color:var(--text-muted); }

/* ==============================================================================================
   THE PAGE HERO BAND
   A wash across the top of the content area, behind whatever header the page happens to render.

   Why here and not on each page: forty views draw their own heading, in a dozen different wrappers.
   Styling each one would mean forty edits and the next page anyone writes would still miss it. Put
   on .content-area it is inherited by every page in the app — including pages that do not exist
   yet, which is the whole requirement — and no view has to know it is there.

   It FADES OUT rather than ending on a line. Headers vary from one line to four, so any fixed edge
   would cut some in half and float above others; a fade has nothing to misalign. It is also the
   only surface in the app that sits on colour, which is what a glass badge needs — on flat white
   paper a translucent pill has nothing to be translucent against.
   ============================================================================================== */
/* THE WASH IS A BACKGROUND, NOT A LAYER.
   It began as a ::before with `.content-area > * { position:relative; z-index:1 }` to keep page
   content above it. That rule put a STACKING CONTEXT at level 1 around every direct child — and the
   "How this works" panel (position:fixed, z-index:1600) lives inside one of them. A stacking context
   traps its descendants: the panel was pinned at level 1 and rendered behind the sidebar and top bar
   on every page that has one.

   A background-image needs none of that. It paints behind all of the element's content by
   definition, creates no stacking context, and touches no z-index — so nothing inside can be
   trapped. The fade is built into the gradient itself rather than applied with a mask, for the same
   reason: a mask would have needed its own layer back. */
.content-area {
  background-image:
    radial-gradient(620px 230px at 18% 0, color-mix(in srgb, var(--primary) 34%, transparent) 0%, transparent 62%),
    linear-gradient(180deg, color-mix(in srgb, var(--primary-light) 90%, transparent) 0%,
                            color-mix(in srgb, var(--warning-light) 45%, transparent) 46%,
                            transparent 100%);
  background-repeat: no-repeat, no-repeat;
  background-size: 100% 210px, 100% 210px;
  background-position: 0 0, 0 0;
  background-attachment: local, local;
}
/* Plain paper means plain paper — someone who turned the ambient wash off must not be given one. */
[data-bg="plain"] .content-area { background-image: none; }
/* On the true dark theme the pale wash would be a white slab across the top, so it is rebuilt dark. */
[data-theme="black"] .content-area {
  background-image:
    radial-gradient(620px 230px at 18% 0, color-mix(in srgb, var(--primary) 20%, transparent) 0%, transparent 64%),
    linear-gradient(180deg, color-mix(in srgb, var(--primary) 9%, transparent) 0%, transparent 100%);
}
/* Print gets paper, never a wash. */
@media print { .content-area { background-image: none; } }
.ox-title { margin:0 0 6px; font-size:31px; font-weight:600; letter-spacing:-.02em; color:var(--text); }
/* ★ THE TAGLINE UNDER EVERY MODULE TITLE — the sentence that says what the page is FOR — was
   --text-hint, a 62% wash. Measured 2.13–3.36:1 across all six themes against a 4.5:1 floor, on
   the one line a person reads to decide whether they are in the right place. Same defect and the
   same cause as `.ox-stat small`: --text-hint is for a hint beside a field somebody is already
   filling in, not for words that carry meaning on their own. */
.ox-sub { font-size:15px; color:var(--text-muted); }
.ox-acts { display:flex; gap:8px; flex-wrap:wrap; }
.ox-chip { display:inline-flex; align-items:center; gap:6px; height:40px; padding:0 16px; border-radius:var(--radius-sm);
  background:var(--card); border:1px solid var(--border-strong,#cbd5e1); color:var(--text); font:inherit;
  font-size:14px; font-weight:600; cursor:pointer; text-decoration:none; }
.ox-chip:hover { background:var(--bg); }
.ox-chip:disabled { opacity:.55; cursor:default; }
/* A hero that would print nothing says so rather than opening an empty sheet. */
.ox-hero.is-disabled { opacity: .5; pointer-events: none; }
.ox-chip > span { color:var(--text-muted); }
.ox-hero { display:inline-flex; align-items:center; gap:8px; height:40px; padding:0 16px; border:none; border-radius:var(--radius-sm);
  background:linear-gradient(135deg,var(--btn-grad-from, var(--primary)),var(--primary-dark)); color:var(--on-primary, #fff); font:inherit; font-size:14px;
  font-weight:600; cursor:pointer; transition:box-shadow .15s, transform .15s; }
.ox-hero:hover { box-shadow:0 4px 14px rgba(59,130,246,.4); transform:translateY(-1px); }

.ox-cardpop { display:none; position:absolute; top:46px; right:0; z-index:50; width:250px; background:var(--card);
  border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--el-4); padding:6px; }
.ox-cardpop.show { display:block; }
/* With glass on, every card's backdrop-filter makes it a stacking context, and sibling cards paint
   in DOM order — so this popover's z-index:50 counted only INSIDE its own card and the next card
   sliced over "Narrow the list". The host card must outrank its later siblings while the pop is
   open, and only then, or it would sit over its neighbours' own popovers for ever. */
.card:has(.ox-cardpop.show) { position:relative; z-index:55; }
.ox-cardpop-h { font-size:13px; font-weight:600; color:var(--text); padding:8px 12px 8px; border-bottom:1px solid var(--border); margin-bottom:4px; }
.ox-cardpop label { display:flex; align-items:center; gap:8px; padding:8px 12px; border-radius:var(--radius-sm); cursor:pointer; font-size:14px; }
.ox-cardpop label:hover { background:var(--bg); }
.ox-cardpop input[type=checkbox] { width:15px; height:15px; accent-color:var(--primary); }

/* The summary row needs air beneath it — .stat-grid, the older card row, has carried margin-bottom
   since it was written, and this one never did. Every page built with ui.statCards therefore had its
   cards sitting flush against the toolbar or table below, which reads as one crowded block rather
   than as two sections. Same 22px, so the two card rows space identically. */
.ox-stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(178px,1fr)); gap:12px; margin-bottom:22px; }
.ox-stat { display:flex; align-items:center; gap:12px; background:var(--stat-tint,var(--card)); border:1px solid var(--border);
  /* var(--radius), not a literal 13px. A summary card sits directly beside .card and .st-tile, and
     a one-pixel difference in the corner is exactly the kind of thing that reads as "these are from
     two different screens" without being nameable. It also ignored the themes, which choose 8px
     (flat) and 16px (soft) on purpose. */
  border-radius:var(--radius); padding:16px 16px; cursor:pointer; transition:border-color .12s, transform .12s; }
.ox-stat:hover { border-color:var(--primary); transform:translateY(-1px); }
.ox-ic { width:38px; height:38px; border-radius:var(--radius-sm); display:flex; align-items:center; justify-content:center; flex:none; }
.ox-stat b { display:block; font-size:25px; font-weight:600; line-height:1; color:var(--text); font-variant-numeric:tabular-nums; }
/* ★ THE LINE THAT SAYS WHAT THE NUMBER MEANS MUST BE READABLE.
   This read --text-hint, which is `color-mix(--text-muted 62%, transparent)` — so the label under
   every statistic in the product was a 62% wash of an already-secondary grey. Measured on the HR
   overview it came out 2.13–3.31:1 in ALL SIX themes, against a 4.5:1 floor: a large dark number
   with an unreadable line beneath it explaining what it counts. --text-hint is right for a hint
   beside a field somebody is already looking at; it is wrong for the only words that give a
   figure its meaning. */
.ox-stat small { display:block; font-size:12px; color:var(--text-muted); margin-top:5px; font-weight:600; }
/* ★ A TINT IS DATA ON THE CARD, NOT A FIGHT OVER `background`.
   These four used to set `background` directly, at specificity (0,2,0) — and every surface
   treatment that came later outranked them:
     [data-glass="full"] … .ox-stat            (0,2,0, but later in the file, so it won)
     [data-theme="soft"][data-glass] .ox-stat  (0,3,0, so it won outright)
   The result was that under glass — and under the SOFT theme in particular — every summary card
   painted the same colour. Measured on Purchasing: "Low stock" (tint-bad), "To receive" (tint-info)
   and two tint-ok cards all rendered #e6ecea. The strip still had its tints in the markup and told
   the reader nothing; a red alarm and an all-clear were the same tile.
   Setting a CUSTOM PROPERTY instead takes the tint out of the contest. Each surface rule now reads
   `var(--stat-tint, <its own default>)`, so it keeps its own look for an untinted card and yields
   to the tint when there is one — and any surface treatment added later inherits that for free
   instead of silently flattening the row again. */
.ox-stat.tint-info { --stat-tint:var(--primary-light); border-color:color-mix(in srgb,var(--primary) 30%,transparent); }
/* ★ THE LAST TINT STILL REACHING PAST ITS OWN TOKEN. warn, ok and danger all read --tint-*-fg;
   this one read --primary-dark, which the dark themes resolve to the BRIGHT #3b82f6 — 4.33:1 on
   Black's own info tint. --tint-info-fg has existed in :root since line 124 and every theme already
   overrides it (Black sets #93c5fd for exactly this). Four tints, one rule now. */
.ox-stat.tint-info b, .ox-stat.tint-info small { color:var(--tint-info-fg,#1d4ed8); }
.ox-stat.tint-info .ox-ic { background:color-mix(in srgb,var(--primary) 18%,transparent); color:var(--primary-dark); }
.ox-stat.tint-warn { --stat-tint:var(--tint-warn-bg,#fef3c7); border-color:color-mix(in srgb,var(--warning,#f59e0b) 45%,transparent); }
.ox-stat.tint-warn b, .ox-stat.tint-warn small { color:var(--tint-warn-fg,#92400e); }
.ox-stat.tint-warn .ox-ic { background:color-mix(in srgb,var(--warning,#f59e0b) 20%,transparent); color:var(--tint-warn-fg,#92400e); }
.ox-stat.tint-bad { --stat-tint:var(--tint-danger-bg,#fee2e2); border-color:color-mix(in srgb,var(--danger) 38%,transparent); }
/* ★ THE ALARM CARD WAS THE ONE THAT COULD NOT BE READ. Its two siblings already use a dark
   foreground on their tint — tint-ok #065f46 at 6.78:1, tint-warn #92400e at 6.37:1 — while this one
   reached for --danger (#ef4444), the bright FILL red, and measured 3.44:1 on its own tint. The card
   that says something is wrong was the least legible card on the page.
   --danger-dark (#dc2626) is not enough either: 4.41:1, still under.
   ★ AND THE TOKEN ALREADY EXISTED. --tint-danger-fg has been declared since line 122 and is what
   .badge-danger uses; the dark themes already override it alongside --tint-warn-fg. Inventing a new
   one (I did, briefly) would have left the dark themes with a hardcoded dark red on a dark tint —
   measured 2.48:1, worse than what it replaced. Use the family's own token and the themes come free. */
.ox-stat.tint-bad b, .ox-stat.tint-bad small { color:var(--tint-danger-fg,#991b1b); }
.ox-stat.tint-bad .ox-ic { background:color-mix(in srgb,var(--danger) 15%,transparent); color:var(--danger); }
/* The green there was never defined — a card that is WITHIN its limit had no way to say so, and
   "not red" is not the same as "fine". */
.ox-stat.tint-ok { --stat-tint:var(--tint-ok-bg,#d1fae5); border-color:color-mix(in srgb,var(--success) 38%,transparent); }
.ox-stat.tint-ok b, .ox-stat.tint-ok small { color:var(--tint-ok-fg,#065f46); }
.ox-stat.tint-ok .ox-ic { background:color-mix(in srgb,var(--success) 18%,transparent); color:var(--tint-ok-fg,#065f46); }

/* ---- A RUNNING TIME, AGAINST ITS LIMIT ---------------------------------------------------------
   A number on its own does not answer the question anybody is asking. "5h 12m" means nothing until
   you know it was estimated at 4h; "38m left" means nothing until you see it against an eight-hour
   window. So the card carries a BAR: how much of the allowance has gone, filling as it goes, and
   overshooting visibly rather than just recolouring.
     green   comfortably inside
     amber   past 80% — the point at which somebody should be told
     red     over, and the card says by how much rather than only turning colour              */
.ox-stat.ox-time { display:block; padding:12px 16px 12px; cursor:default; }
.ox-time-top { display:flex; align-items:center; gap:12px; }
.ox-time-top .ox-ic { width:34px; height:34px; }
.ox-time b { font-size:25px; }
.ox-time-bar { height:6px; border-radius:var(--radius-lg); margin-top:11px; overflow:hidden;
  background:color-mix(in srgb,var(--text) 10%,transparent); }
.ox-time-fill { height:100%; border-radius:var(--radius-lg); background:var(--success);
  transition:width .4s ease, background-color .3s ease; }
.ox-stat.tint-warn .ox-time-fill { background:var(--warning,#f59e0b); }
.ox-stat.tint-bad  .ox-time-fill { background:var(--danger); }
/* The notice. Colour tells you at a glance; the words tell you what to do about it, and only the
   words survive being printed, colour-blind or read aloud over a radio. */
.ox-time-note { margin-top:9px; font-size:12px; font-weight:600; line-height:1.45;
  display:flex; align-items:flex-start; gap:6px; }
.ox-time-note > span:first-child { flex:none; display:inline-flex; margin-top:1px; }
@supports not (background: color-mix(in srgb, red 10%, transparent)) {
  .ox-time-bar { background:var(--border, #e2e8f0); }
  .ox-stat.tint-ok { --stat-tint:#d1fae5; }
}

/* THE CLOCK FACE. Tabular figures so the digits do not shift as they tick — a timer whose width
   changes every ten seconds reads as broken — and a slightly tighter tracking so 05:17:42 sits as
   one object rather than eight characters. */
.ox-time b.ox-clock { font-variant-numeric:tabular-nums; letter-spacing:-.01em; font-size:25px; }

/* OVER TIME: THE CARD PULSES. Colour alone is a state you stop noticing after ten minutes on a wall
   screen; movement is what pulls the eye back. The ring pulses, not the digits — a blinking number
   is one you cannot read, which is the opposite of the point.
   The seconds keep their own quiet beat so the timer is visibly ALIVE even before it goes over. */
@keyframes ox-time-pulse {
  0%   { box-shadow:0 0 0 0 color-mix(in srgb,var(--danger) 55%,transparent); }
  70%  { box-shadow:0 0 0 10px color-mix(in srgb,var(--danger) 0%,transparent); }
  100% { box-shadow:0 0 0 0 color-mix(in srgb,var(--danger) 0%,transparent); }
}
@keyframes ox-time-blink { 0%,100% { opacity:1; } 50% { opacity:.35; } }
.ox-stat.ox-time.is-over { animation:ox-time-pulse 1.8s ease-out infinite; border-color:var(--danger); }
.ox-stat.ox-time.is-over .ox-time-note > span:first-child { animation:ox-time-blink 1.1s ease-in-out infinite; }
.ox-time .ox-sec { opacity:.62; font-size:.72em; }
/* An expired permit countdown, wherever it appears. */
.ox-blink { animation:ox-time-blink 1.1s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .ox-blink { animation:none; } }
/* A pulsing card on a page somebody is reading is a distraction, not a warning. Respect the setting. */
@media (prefers-reduced-motion: reduce) {
  .ox-stat.ox-time.is-over { animation:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--danger) 35%,transparent); }
  .ox-stat.ox-time.is-over .ox-time-note > span:first-child { animation:none; }
  .ox-time-fill { transition:none; }
}
@supports not (box-shadow:0 0 0 0 color-mix(in srgb,red 50%,transparent)) {
  @keyframes ox-time-pulse { 0% { box-shadow:0 0 0 0 rgba(239,68,68,.55); } 100% { box-shadow:0 0 0 10px rgba(239,68,68,0); } }
}

.ox-searchband { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
.ox-search { flex:1; min-width:240px; display:flex; align-items:center; gap:8px; height:44px; padding:0 16px;
  border-radius:var(--radius); background:var(--card); border:1px solid var(--border-strong,#cbd5e1); }
.ox-search > span { color:var(--text-muted); }
.ox-search input { flex:1; border:none; outline:none; background:transparent; font:inherit; font-size:14px; color:var(--text); }
.ox-search:focus-within { border-color:var(--primary); box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 15%,transparent); }
.ox-fchips { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:12px; }
.ox-fchip { display:inline-flex; align-items:center; gap:6px; background:var(--primary-light); color:var(--primary-dark);
  font-size:12px; font-weight:600; padding:4px 12px; border-radius:var(--radius-sm); }
.ox-fchip > span { cursor:pointer; opacity:.65; }
.ox-fchip > span:hover { opacity:1; }
.ox-fclear { background:none; border:none; color:var(--text-muted); font:inherit; font-size:12px; font-weight:600; cursor:pointer; text-decoration:underline; }

.ox-views { display:inline-flex; background:var(--tint-neutral-bg,#f1f5f9); border-radius:var(--radius-sm); padding:4px; gap:2px; }
.ox-views button { display:inline-flex; align-items:center; gap:6px; background:none; border:none; border-radius:var(--radius-sm);
  padding:6px 12px; font:inherit; font-size:13px; font-weight:600; color:var(--text-muted); cursor:pointer; }
.ox-views button.on { background:var(--card); color:var(--text); box-shadow:var(--el-1); }
/* A switcher with more choices than fit on one line. .ox-views is inline-flex and does not wrap —
   correct for the three or four views a list page offers, wrong for the Expatriate report, which
   offers fifteen ways to read the same list and would otherwise run off the side of the column. */
.ox-views.ox-views-wrap { display:flex; flex-wrap:wrap; }
/* ★ A TAB ROW WIDER THAN ITS COLUMN SCROLLS IN ITS OWN STRIP - a rail, never the page. Where even the
   tabs' narrowest form did not fit, the row pushed out of its column and the whole PAGE scrolled
   sideways, the last tabs hanging off the right edge: Accounting on a tablet at 1280px and below
   (measured 08-10; eleven pages carry this row). Where it fits nothing changes; where it does not, the
   row scrolls itself. The focus ring is drawn inside the tab, which the strip would otherwise clip. */
.ox-tabs2 { display:grid; grid-auto-flow:column; grid-auto-columns:1fr; border-bottom:1px solid var(--border);
  overflow-x:auto; overflow-y:hidden; }
/* ★ a 3px bar (19-10, "reduce the scroll bar size also") - Chrome ignores ::-webkit-scrollbar once `scrollbar-width` is set, so the
   standard property goes only to a browser without the WebKit bar; the arrows at each end (§47) are the way to move it */
.ox-tabs2::-webkit-scrollbar { height:3px; }
.ox-tabs2::-webkit-scrollbar-track { background:transparent; }
.ox-tabs2::-webkit-scrollbar-thumb { background:color-mix(in srgb, var(--text) 16%, transparent); border-radius:3px; }
.ox-tabs2:hover::-webkit-scrollbar-thumb { background:color-mix(in srgb, var(--text) 30%, transparent); }
@supports not (selector(::-webkit-scrollbar)) { .ox-tabs2 { scrollbar-width:thin; } }
.ox-tabs2 button { display:inline-flex; align-items:center; justify-content:center; gap:8px; background:none; border:none;
  padding:16px; font:inherit; font-size:14px; font-weight:600; color:var(--text-muted); cursor:pointer; border-bottom:2px solid transparent; }
.ox-tabs2 button:focus-visible { outline-offset:-2px; }
.ox-tabs2 button.on { color:var(--primary-dark); border-bottom-color:var(--primary); background:var(--primary-light); }
.ox-selbar { display:flex; justify-content:space-between; align-items:center; gap:8px; padding:12px 20px;
  background:var(--bg); border-bottom:1px solid var(--border); }

.ox-row { display:flex; align-items:center; gap:12px; padding:16px 20px; border-bottom:1px solid var(--border);
  transition:background .12s; cursor:pointer; }
.ox-row:hover { background:var(--bg); }
.ox-row-kid { background:color-mix(in srgb,var(--primary) 4%,transparent); padding-left:40px; }
.ox-kidmark { color:var(--text-muted); font-weight:400; }
.ox-rowic { width:38px; height:38px; border-radius:var(--radius-sm); background:var(--primary-light); color:var(--primary-dark);
  display:flex; align-items:center; justify-content:center; flex:none; }
.ox-rowmain { flex:1; min-width:0; }
.ox-rowtop { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.ox-rowtop b { font-size:15px; color:var(--text); }
.ox-rowmeta { font-size:12px; color:var(--text-hint,var(--text-muted)); margin-top:3px; }
.ox-badges { display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; }
.ox-b { font-size:11px; font-weight:600; padding:2px 8px; border-radius:var(--radius-sm); }
.ox-b.info { background:var(--primary-light); color:var(--primary-dark); }
.ox-b.bad { background:var(--tint-danger-bg,#fee2e2); color:var(--danger); }
.ox-b.warn { background:var(--tint-warn-bg,#fef3c7); color:var(--tint-warn-fg,#92400e); }
.ox-b.ok { background:var(--success-light); color:var(--tint-ok-fg); }
.ox-b.plain { background:var(--tint-neutral-bg,#f1f5f9); color:var(--text-muted); }
.ox-rowright { text-align:right; flex:none; min-width:96px; }
.ox-rowright small { display:block; font-size:11px; color:var(--text-hint,var(--text-muted)); font-weight:600; }
.ox-rowright b { font-size:14px; color:var(--text); font-variant-numeric:tabular-nums; }
.ox-icobtn { width:36px; height:36px; border-radius:var(--radius-sm); border:1px solid var(--border); background:var(--card);
  color:var(--text-muted); cursor:pointer; display:flex; align-items:center; justify-content:center; flex:none; }
.ox-icobtn:hover { border-color:var(--primary); color:var(--primary); }

.ox-group { display:flex; justify-content:space-between; align-items:center; padding:12px 20px; background:var(--bg);
  border-bottom:1px solid var(--border); font-size:12px; font-weight:600;
 color:var(--text-muted); }
.ox-groupn { background:var(--card); border:1px solid var(--border); border-radius:var(--radius-lg); padding:0 8px; font-size:12px; }
.ox-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(268px,1fr)); gap:12px; padding:16px 20px; }
/* ★ THE CORNER COMES FROM THE TOKEN. This pinned a literal 13px, so it disagreed with --radius in
   five of the six themes (12 white/blue/warm, 8 flat, 16 black) — only Soft happened to override
   it — and every page using .ox-card sat beside a .card with a different corner. Same defect the
   standard already names for .card, in a component the rule's list had simply never included. */
.ox-card { background:var(--card); border:1px solid var(--border); border-radius:var(--radius); padding:16px; cursor:pointer;
  transition:border-color .12s, transform .12s; }
.ox-card:hover { border-color:var(--primary); transform:translateY(-2px); }
.ox-cardfoot { display:flex; justify-content:space-between; gap:8px; margin-top:12px; padding-top:12px;
  border-top:1px solid var(--border); font-size:12px; color:var(--text-muted); font-variant-numeric:tabular-nums; }
.ox-pager { display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;
  padding:12px 20px; font-size:13px; color:var(--text-muted); }
.ox-pager:empty { display:none; }
.ox-page { display:inline-flex; align-items:center; justify-content:center; min-width:34px; height:34px;
  border-radius:var(--radius-sm); background:var(--primary); color:#fff; font-weight:600; font-size:13px; }

/* Export format picker (ui.exportMenu) — one Export control offering CSV / Excel / PDF, so a module
   page wires its export once and the choice lives in the shared contract rather than three buttons. */
.ui-expmenu { position:absolute; z-index:2000; min-width:230px; background:var(--card); border:1px solid var(--border);
  border-radius:var(--radius-sm); box-shadow:var(--el-4); padding:6px; }
.ui-expmenu button { display:flex; flex-direction:column; align-items:flex-start; gap:0; width:100%; text-align:left;
  background:none; border:none; border-radius:var(--radius-sm); padding:8px 8px; cursor:pointer; font:inherit; color:var(--text); }
/* ★ HOVER IS GREY; LIGHT BLUE MEANS "THIS ONE IS SELECTED".
   This hovered var(--primary-light) — the same colour .ds-opt.is-sel uses to say which option is
   currently chosen. So in the select dropdown light blue meant "selected" and in this menu it meant
   "the pointer is here", and a person moving between the two is being told two different things by
   one colour. The other four menus in the app (.ds-opt, .ox-cardpop label, .user-menu-item,
   .nav-fly .pit) all hover on the neutral --bg; this was the odd one out. */
.ui-expmenu button:hover { background:var(--bg); }
/* A menu that DOES carry a selection keeps the blue for it — the meaning is preserved, not removed. */
.ui-expmenu button.is-sel { background:var(--primary-light); color:var(--primary-dark); font-weight:600; }
.ui-expmenu-t { font-size:14px; font-weight:600; }
.ui-expmenu-s { font-size:12px; color:var(--text-muted); }

/* Import review (ui.importPreview) — a duplicate is shown NEXT TO the record it clashes with, before
   anything is written. No "add anyway" control exists: two stock lines for one material split the
   book stock, so it is not an outcome the UI should make reachable. */
.ui-imp-sum { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:10px; }
.ui-imp-pill { font-size:12px; font-weight:600; border-radius:var(--radius-lg); padding:4px 12px; }
.ui-imp-pill.ok  { background:var(--success-light); color:#059669; }
.ui-imp-pill.dup { background:var(--warning-light); color:#b45309; }
.ui-imp-pill.bad { background:var(--danger-light, #fee2e2); color:#dc2626; }
.ui-imp-h { font-size:var(--text-xs); font-weight:600;
  color:var(--text-muted); margin:14px 0 6px; }
.ui-imp-scroll { max-height:240px; overflow:auto; border:1px solid var(--border); border-radius:var(--radius-sm); }
.ui-imp-t { width:100%; border-collapse:collapse; font-size:12px; }
.ui-imp-t th { position:sticky; top:0; background:var(--bg); text-align:left; font-weight:600; color:var(--text-muted);
  padding:6px 8px; border-bottom:1px solid var(--border); font-size:12px; }
.ui-imp-t td { padding:8px 8px; border-bottom:1px solid var(--border); vertical-align:top; color:var(--text); }
.ui-imp-t tr:last-child td { border-bottom:none; }
.ui-imp-row { font-weight:600; color:var(--text-muted); white-space:nowrap; }
.ui-imp-k { display:inline-block; min-width:52px; font-size:11px; color:var(--text-muted); }
.ui-imp-why { color:#b45309; }
.ui-imp-why.bad { color:var(--danger); }
.ui-imp-none { color:var(--text-muted); font-style:italic; }
.ui-imp-clean { font-size:13px; color:#059669; background:var(--success-light); border-radius:var(--radius-sm); padding:8px 12px; }

/* PRF line stock strip — what the store already holds for the part on this line. A requisition
   raised without this is how a store orders what it already has on the shelf.
   ★ THE BASE RULE IS NOT HERE. It was, and it was DEAD: a second `.li-stock { … }` further down
   (search "QUESTION OF WHERE") set the same seven properties and won all of them. The two disagreed
   by a 70px indent — this one hung the strip at margin-left:106px, the live one at 36px — so anyone
   tuning the indent here would have moved a number that changes nothing on screen.
   The MODIFIERS below are not duplicated anywhere and do apply; they are what colours the strip. */
.li-stock.ok   { border-left-color:var(--success); background:var(--success-light); color:#059669; }
.li-stock.info { border-left-color:var(--primary); background:var(--primary-light); color:var(--primary-dark); }
.li-stock.warn { border-left-color:var(--warning); background:var(--warning-light); color:#b45309; }
.li-stock.bad  { border-left-color:var(--danger);  background:var(--danger-light, #fee2e2); color:#dc2626; }
.li-stock-q { font-weight:600; }
.li-stock-v { color:var(--text-muted); }
.li-stock-x { flex-basis:100%; color:var(--text); }
/* Stock verdict under a requisition line (raiser, purchase desk and approver all see the same). */
.prf-note { font-size:11px; font-weight:600; margin-top:2px; }
.prf-note.ok   { color:var(--success); }
.prf-note.warn { color:#b45309; }
.prf-note.bad  { color:var(--danger); }

/* Purchase requisition rendered as a document — header identity, numbered items, signature trail. */
.prf-head { display:grid; grid-template-columns:repeat(5,1fr); gap:12px 16px; padding:12px 12px;
  background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); margin-bottom:14px; }
.prf-l { font-size:11px; color:var(--text-muted); font-weight:600; }
.prf-v { font-size:14px; color:var(--text); font-weight:600; margin-top:1px; }
.prf-sec { font-size:11px; font-weight:600;
  color:var(--text-muted); margin:16px 0 7px; padding-bottom:4px; border-bottom:1px solid var(--border); }
.prf-sign { display:flex; justify-content:space-between; align-items:flex-start; gap:12px;
  padding:8px 12px; border-left:3px solid var(--border); border-radius:0 var(--radius-sm) var(--radius-sm) 0; margin-bottom:5px; background:var(--bg); }
.prf-sign.done { border-left-color:var(--success); background:var(--success-light); }
.prf-sign.done .prf-v, .prf-sign.done .prf-l, .prf-sign.done .prf-when { color:var(--tint-ok-fg,#059669); }
.prf-sign.now  { border-left-color:var(--warning); background:var(--warning-light); }
.prf-sign.now .prf-v, .prf-sign.now .prf-l, .prf-sign.now .prf-when { color:var(--tint-warn-fg,#92400e); }
.prf-sign.bad  { border-left-color:var(--danger); background:var(--tint-danger-bg,#fee2e2); }
/* The tint is a light red in every theme, so the text cannot inherit the theme's own --text — in a
   dark theme that is near-white and vanishes on it. Pin the foreground to the danger colour. */
.prf-sign.bad .prf-v, .prf-sign.bad .prf-l, .prf-sign.bad .prf-when { color:var(--danger); }
.prf-sign.wait { opacity:.72; }
.prf-when { font-size:12px; color:var(--text-muted); white-space:nowrap; }
@media (max-width:820px){ .prf-head { grid-template-columns:repeat(2,1fr); } }

/* Purchase-requisition line editor. The old row was six unlabelled boxes in a 720px modal; a person
   raising a requisition could not tell which box was which. Each field carries its label, the line is
   numbered, and its remark sits on its own row. */
.li-card { border:1px solid var(--border); border-radius:var(--radius-sm); padding:12px 12px; margin-bottom:10px; background:var(--card); }
.li-grid { display:grid; grid-template-columns:26px 120px minmax(150px,1.1fr) minmax(150px,1.3fr) 78px 104px 30px; gap:8px 8px; align-items:end; }
.li-grid.has-price { grid-template-columns:26px 114px minmax(135px,1fr) minmax(140px,1.15fr) 72px 100px 94px 30px; }
.li-sr { align-self:center; font-weight:600; color:var(--text-muted); font-size:13px; text-align:center;
  background:var(--bg); border-radius:var(--radius-sm); padding:4px 0; }
.li-f { display:flex; flex-direction:column; gap:2px; min-width:0; }
.li-f > span:first-child { font-size:11px; font-weight:600; color:var(--text-muted); }
.li-f .form-control, .li-f .ds-sel { height:34px; }
.li-frem { grid-column:2 / -1; }
.li-del { align-self:center; height:34px; width:30px; border:1px solid var(--border); background:var(--card);
  color:var(--text-muted); border-radius:var(--radius-sm); cursor:pointer; font-size:14px; }
.li-del:hover { border-color:var(--danger); color:var(--danger); }
@media (max-width:900px){ .li-grid, .li-grid.has-price { grid-template-columns:26px 1fr 1fr 30px; } .li-frem { grid-column:1 / -1; } }

/* Stock answered as a QUESTION OF WHERE — every position in the plant, not one number. */
.li-stock { display:flex; flex-wrap:wrap; gap:6px 12px; align-items:center; margin:9px 0 0 36px;
  font-size:12px; border-left:3px solid var(--border); padding:6px 12px; border-radius:0 var(--radius-sm) var(--radius-sm) 0; }
.li-locs { display:flex; flex-wrap:wrap; gap:6px; flex-basis:100%; }
.li-loc { background:var(--card); border:1px solid var(--border); border-radius:var(--radius-sm); padding:2px 8px; color:var(--text); }
.li-loc.zero { opacity:.55; }
.li-bin { color:var(--text-muted); }
/* Justification + per-line remark on the requisition document. */
.prf-just { font-size:14px; line-height:1.55; color:var(--text); background:var(--bg);
  border-left:3px solid var(--primary); border-radius:0 var(--radius-sm) var(--radius-sm) 0; padding:8px 12px; }
/* A remark is a sentence about the line, so it gets its own labelled row under it rather than being
   squeezed in beside the item name where neither could be read. */
.prf-remrow td { border-top:none !important; padding-top:0 !important; }
.prf-remk { display:inline-block; font-size:9px; font-weight:600;
 color:var(--text-muted); margin-right:8px; vertical-align:1px; }
.prf-remv { font-size:12px; color:var(--text); }
.prf-locs { display:flex; flex-wrap:wrap; gap:4px; margin-top:5px; font-size:11px; }
/* Signature marker and priority on the requisition document. */
.prf-mk { display:inline-flex; align-items:center; justify-content:center; width:22px; height:22px; flex:none;
  border-radius:50%; font-size:12px; font-weight:600; }
.prf-mk.done { background:var(--success); color:#fff; }
.prf-mk.now  { background:var(--warning); color:#fff; }
.prf-mk.wait { background:var(--border); color:var(--text-muted); }
.prf-pri { display:inline-block; font-size:12px; font-weight:600; border-radius:var(--radius-lg); padding:2px 8px; }
.prf-pri.low    { background:var(--tint-neutral-bg,#f1f5f9); color:var(--text-muted); }
.prf-pri.normal { background:var(--primary-light); color:var(--primary-dark); }
.prf-pri.high   { background:var(--warning-light); color:#b45309; }
.prf-pri.urgent { background:var(--danger-light, #fee2e2); color:#dc2626; }
/* Company letterhead on the requisition — the COMPANY owns the document, the plant is where it was
   raised. Showing only the plant name read as an abbreviation of the company. */
.prf-co { border-bottom:2px solid var(--text); padding-bottom:8px; margin-bottom:12px; }
.prf-co-n { font-size:17px; font-weight:600; letter-spacing:-.2px; color:var(--text); }
.prf-co-s { font-size:12px; color:var(--text-muted); margin-top:1px; }
/* Signature row: the verdict is the point, so it reads as a badge on the right, before the date. */
.prf-right { display:flex; align-items:center; gap:12px; flex:none; white-space:nowrap; }
.prf-badge { font-size:11px; font-weight:600; letter-spacing:.06em; border-radius:var(--radius-lg); padding:2px 8px;
  background:var(--border); color:var(--text-muted); }
.prf-badge.done { background:var(--success); color:#fff; }
.prf-badge.now  { background:var(--warning); color:#fff; }
.prf-badge.bad  { background:var(--danger);  color:#fff; }
/* The item code is what purchasing quotes and the store picks against — it reads as an identifier. */
.prf-code { font-weight:600; font-variant-numeric:tabular-nums; letter-spacing:.02em; }
.prf-code2 { font-size:11px; color:var(--text-muted); margin-top:1px; }

/* ============================================================================
   GLASS EFFECTS  —  a per-user switch (html[data-glass]), independent of theme.
   Set from Appearance; stored in user_prefs.glass. Three values:

     standard  the app exactly as it was before this setting existed — cards and
               topbar frosted, sidebar deliberately flat paper (the "PURE
               CLAUDE.AI SIDEBAR" block above). This is the DEFAULT, so nobody's
               screen changes until they choose otherwise, and nothing below
               needs to fire for it.
     full      the iOS / visionOS treatment: every structural surface frosted,
               including the sidebar and dialogs, over a brighter lit backdrop.
     off       everything opaque, on any theme. The honest answer for a weak GPU
               (backdrop-filter is composited per layer and a board with fifty
               cards is fifty of them) and for anyone who reads translucent
               small type badly.

   This block sits at the END of the file on purpose. Theme blocks like
   [data-theme="flat"] .sidebar carry the same specificity, so source order is
   what lets the switch win — glass is a decision the user made second.
   ============================================================================ */

/* ---------- FULL — iOS / visionOS ----------------------------------------- */
/* Counter-intuitively these are MORE opaque than the 66% default, not less. The
   iOS character comes from a heavy blur over a lit backdrop, not from being
   see-through; and 66% is where a 1px table border stops having contrast
   against whatever shows through, which is the bug the modal exception above
   was written for. Blur hard, stay legible. */
[data-glass="full"] {
  /* 82% was not glass, it was tinted paper — you could not see anything through it, so there was
     nothing to read as depth. Real translucency plus a much wider blur radius, a saturation lift
     (what makes colour bloom through frosted glass rather than go grey), a bright top edge for the
     specular highlight, and a soft wide shadow so the panel reads as floating ABOVE the page.
     Dialogs stay denser than tiles: a sheet you read a table on cannot be see-through. */
  --glass-bg: rgba(255,255,255,0.55);
  --glass-bg: color-mix(in srgb, var(--card) 58%, transparent);
  --glass-strong: rgba(255,255,255,0.78);
  --glass-strong: color-mix(in srgb, var(--card) 80%, transparent);
  --glass-brd: rgba(255,255,255,0.7);
  --glass-brd: color-mix(in srgb, #fff 62%, var(--border));
  --glass-blur: saturate(2) blur(44px);
  --glass-shadow:
    0 22px 60px rgba(15,23,42,0.14),
    0 4px 14px rgba(15,23,42,0.07),
    inset 0 1px 0 rgba(255,255,255,0.75),
    inset 0 -1px 0 rgba(255,255,255,0.22);
}

/* FULL GLASS ON THE DARK THEME.
   The tokens above are built for a light page: the border is 62% PURE WHITE and the specular
   highlight is rgba(255,255,255,.75). Over white paper that reads as light catching the edge of
   glass. Over the black theme's #0a0a0c ground the identical values are simply white lines, so every
   card came out drawn as a white box.

   Rebuilt for dark ground: the edge becomes a faint lift of the theme's own blue-lit border instead
   of white, and the specular highlight drops to about a tenth of its strength.

   SCOPED TO BOTH ATTRIBUTES ON PURPOSE. [data-theme="black"][data-glass="full"] can only match when
   the theme is black AND glass is full — no other theme, and not black on standard or off glass, can
   ever see it. That is a guarantee of the selector, not of care taken. */
[data-theme="black"][data-glass="full"] {
  --glass-brd: color-mix(in srgb, #fff 12%, var(--border));
  --glass-shadow:
    0 22px 60px rgba(0,0,0,0.55),
    0 4px 14px rgba(0,0,0,0.4),
    inset 0 1px 0 rgba(255,255,255,0.08),
    inset 0 -1px 0 rgba(255,255,255,0.03);
}

/* The lit backdrop. Frosted glass over a flat fill just looks grey — the blur
   needs something with colour and gradient behind it to be worth having. */
[data-glass="full"] .app-layout {
  /* The old version ended on linear-gradient(... var(--bg) 84%, #000 10%) — a black wash across the
     whole page. THAT is what made the colours look dirty: every theme's backdrop drifted to muddy
     grey at the bottom no matter what its accent was. There is no black here at all now, only light
     colour on the theme's own paper: three wide, low-opacity blooms placed off the corners so the
     tint moves gently across the viewport and gives the blur something to actually refract. */
  background:
    radial-gradient(1200px 780px at 10% -10%, color-mix(in srgb, var(--primary) 16%, transparent), transparent 70%),
    radial-gradient(1000px 700px at 92% 4%, color-mix(in srgb, var(--primary) 10%, transparent), transparent 66%),
    radial-gradient(1100px 760px at 55% 106%, color-mix(in srgb, var(--primary) 8%, transparent), transparent 68%),
    var(--bg);
  background-attachment: fixed;
}

/* Flat is the exception, and it has to be handled or the theme looks broken. The blooms above are
   colour-mixed FROM --primary, which on every other theme is a colour — but Flat's primary is
   #18181b, so they come out as a grey smudge and the page reads dirty next to the near-white
   frosted panels. (Reported: "the sidebar bg & page bg don't match" — the sidebar was fine; the
   canvas had gone grey underneath it.) Flat's identity is un-tinted white paper, so it gets a
   neutral lift just strong enough to give the blur something to work with. */
[data-theme="flat"][data-glass="full"] .app-layout {
  /* Flat's --primary is near-black, so the blooms above would be grey smudges. Give it cool light
     instead of dark: a faint blue-slate that behaves like daylight on white paper. */
  background:
    radial-gradient(1200px 780px at 10% -10%, rgba(148,163,184,0.20), transparent 70%),
    radial-gradient(1000px 700px at 92% 4%, rgba(148,163,184,0.13), transparent 66%),
    radial-gradient(1100px 760px at 55% 106%, rgba(148,163,184,0.10), transparent 68%),
    var(--bg);
  background-attachment: fixed;
}

/* The sidebar stays CONTINUOUS with the canvas here too, not frosted.
   Frosting only means something when content passes behind the surface — the top bar has the page
   scrolling under it, cards sit over the wash. On desktop the sidebar is a fixed column with
   nothing behind it, so making it a frosted panel does not read as depth; it just paints it a
   different white from the page next to it. That difference is exactly what was reported as "the
   sidebar bg and page bg don't match", and it is why the earlier fix appeared to do nothing: it
   only applied to 'standard', and this rule outranked it in 'full'. */
[data-glass="full"] .sidebar {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-right: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
/* Same reasoning as the sidebar above: no colour of its own, so the bar IS the page surface. */
[data-glass="full"] .topbar {
  background: transparent;
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}

/* Every tile — .ox-stat included, which the original glass selector never
   covered, so module summary cards used to sit flat among frosted ones. */
[data-glass="full"] .card, [data-glass="full"] .panel, [data-glass="full"] .stat,
[data-glass="full"] .kpi-card, [data-glass="full"] .ds-card, [data-glass="full"] .ox-stat {
  background: var(--stat-tint, var(--glass-bg));
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--glass-shadow);
}

/* Dialogs. In 'standard' these are forced opaque because a sheet you print
   should not be see-through; under 'full' they become the strongest glass
   instead — translucent enough to read as a dialog floating over the page,
   opaque enough that the table inside it keeps its lines. */
[data-glass="full"] .ovl > .card, [data-glass="full"] .modal .modal-content,
[data-glass="full"] .apx-modal, [data-glass="full"] #preview-body,
[data-glass="full"] .khtfw-modal {
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--el-4), inset 0 1px 0 rgba(255,255,255,0.5);
}
/* The scrim blurs the page behind the dialog — the single most recognisable
   part of the iOS look, and it costs one layer rather than one per card. */
[data-glass="full"] .ovl, [data-glass="full"] .modal, [data-glass="full"] .apx-overlay,
[data-glass="full"] .khtfw-overlay {
  -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
}

/* Translucency costs border contrast, and the app's tables are 1px lines. Give
   them back what the glass took, or requisition rows read as having no lines
   at all until hover paints a solid background behind one. */
[data-glass="full"] .form-table th, [data-glass="full"] .form-table td {
  border-color: color-mix(in srgb, var(--text) 20%, transparent);
}

/* ---------- OFF — opaque on any theme ------------------------------------- */
[data-glass="off"] .app-layout { background: var(--bg); }
/* .ox-stat is listed separately below: it is the one surface here that carries a MEANING colour, and
   folding it in with the rest set every summary card to var(--card) regardless of its tint. */
[data-glass="off"] .card, [data-glass="off"] .panel, [data-glass="off"] .stat,
[data-glass="off"] .kpi-card, [data-glass="off"] .ds-card,
[data-glass="off"] .topbar, [data-glass="off"] .ovl > .card,
[data-glass="off"] .modal .modal-content,
[data-glass="off"] .apx-modal, [data-glass="off"] #preview-body {
  background: var(--card);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 1px solid var(--border);
  box-shadow: var(--el-1);
}
/* Same treatment as its neighbours above, but yielding to the card's own tint — an "Overdue" that
   is red with glass on must stay red with glass off. Turning glass off is a preference about
   MATERIAL; it was silently also turning off the only colour on the row that carries information. */
[data-glass="off"] .ox-stat {
  background: var(--stat-tint, var(--card));
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 1px solid var(--border);
  box-shadow: var(--el-1);
}
[data-glass="off"] .sidebar {
  background: var(--bg);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border-right: 1px solid var(--border);
}
[data-glass="off"] .topbar { border-bottom: 1px solid var(--border); box-shadow: none; }
[data-glass="off"] .card:hover, [data-glass="off"] .panel:hover,
[data-glass="off"] .stat:hover, [data-glass="off"] .kpi-card:hover {
  box-shadow: var(--el-3);
}

/* ---------- Guards -------------------------------------------------------- */
/* On a phone the sidebar is a DRAWER that slides over the page, not a column beside it, so it must
   be opaque in EVERY mode or the nav reads on top of live content. It lives here, at the end and
   next to the other cross-cutting guards, because `[data-glass] .sidebar` ties on specificity with
   the per-mode rules above — source order is what makes it win. */
@media (max-width: 768px) {
  .sidebar, [data-glass] .sidebar {
    background: var(--bg);
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
}

/* An OS-level "reduce transparency" is an accessibility request, not a
   preference to negotiate with — it wins over the switch. */
@media (prefers-reduced-transparency: reduce) {
  [data-glass="full"] .sidebar, [data-glass="full"] .topbar, [data-glass="full"] .card,
  [data-glass="full"] .panel, [data-glass="full"] .stat, [data-glass="full"] .kpi-card,
  [data-glass="full"] .ds-card, [data-glass="full"] .ox-stat,
  [data-glass="full"] .ovl > .card, [data-glass="full"] .apx-modal {
    background: var(--card);
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  [data-glass="full"] .app-layout { background: var(--bg); }
}
/* Paper has no backdrop to filter. A printed requisition must be plain white
   whatever the person who pressed Print happens to have chosen on screen. */
@media print {
  [data-glass] .card, [data-glass] .panel, [data-glass] .stat, [data-glass] .kpi-card,
  [data-glass] .ds-card, [data-glass] .ox-stat, [data-glass] .ovl > .card,
  [data-glass] .modal .modal-content, [data-glass] #preview-body {
    background: #fff !important;
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
    box-shadow: var(--el-1);
  }
  [data-glass] .app-layout { background: #fff !important; }
}

/* NESTED GROUP HEADERS — a unit row has children, so it is a heading, not a leaf. Open ones take the
   theme accent like every top-level heading does; leaf nav items keep the monochrome treatment the
   Claude.ai sidebar block above defines. Without this a group one level down looked unrelated to the
   group containing it, which is where the hierarchy stops being readable. */
.nav-item.nav-parent.expanded { color: var(--primary); font-weight: 600; }
.nav-item.nav-parent.expanded .nav-icon,
.nav-item.nav-parent.expanded .nav-caret { color: var(--primary); }

/* SIDE MENU ICONS take the theme colour. The Claude.ai sidebar block above deliberately greys them
   (its rule is that the nav carries no accent at all); this overrides that one decision on the
   owner's instruction, while leaving the LABELS monochrome so the rail still reads as text with
   colour used as a cue rather than as decoration. Appended so it wins on source order. */
.nav-item .nav-icon { color: var(--primary); }
.nav-item:hover .nav-icon { color: var(--primary-dark); }
.nav-item.active .nav-icon,
.nav-item.nav-parent.expanded .nav-icon { color: var(--primary); }

/* AVATAR MENU icons follow the nav — same accent, same reason. The account popover and the
   collapsed-rail flyout are built to look like each other (see .nav-fly), so colouring one and not
   the other would split them apart the moment the rail is collapsed. Label text stays monochrome
   here too; only the glyphs take the theme. */
.cacct-pop .pit svg,
.nav-fly .pit svg,
.side-set svg { color: var(--primary); stroke: var(--primary); }
.cacct-pop .pit:hover svg,
.nav-fly .pit:hover svg,
.side-set:hover svg { color: var(--primary-dark); stroke: var(--primary-dark); }

/* AVATAR MENU HEADINGS take the accent, matching the nav group headings — the popover is a menu of
   groups too ("ADMIN", and the flyout's unit name), so its headings should read the same way. */
.cacct-sec,
.nav-fly .nav-fly-head { color: var(--primary); }

/* THE AVATAR CIRCLE is theme-coloured rather than a neutral grey disc. Deliberately the SOLID
   treatment .sidebar-user .user-avatar already used — primary field, white initials — rather than a
   light tint: that rule was already themed, so tinting this one down would have left the app with
   two differently-weighted avatars. It also ties to .brand-icon at the top of the same rail, so the
   sidebar starts and ends on the accent. */
.cacct .cav {
  background: var(--accent-solid);   /* a fill carrying white ink — --primary is #10b981 on Forest (2.5:1 under white) */
  color: #fff;
}

/* HOVER takes the accent. The label was going to --text on hover, which on a rail whose resting
   label is already a muted --text meant the only feedback was the faint background tint. The menus
   that mirror the nav (avatar popover, collapsed-rail flyout) get it too — they are the same
   control in a different place, and hovering them should not feel different. */
.nav-item:hover { color: var(--primary); }
.nav-item.nav-parent:hover { color: var(--primary); }
.cacct-pop .pit:hover,
.nav-fly .pit:hover,
.side-set:hover { color: var(--primary); }

/* PURE WHITE — the page background is #ffffff everywhere, whatever the glass mode. The ambient wash
   lives on [data-glass="full"] .app-layout, which ties with a plain [data-theme="white"] selector
   and beat it on order; naming both attributes outranks it outright, so this cannot be undone by a
   rule added later in either family. */
[data-theme="white"] .app-layout,
[data-theme="white"][data-glass] .app-layout {
  background: #ffffff;
  background-image: none;
}
[data-theme="white"] body, [data-theme="white"] .main-content { background: #ffffff; }

/* TEXT TONE — Appearance > Text. 'black' means MAXIMUM CONTRAST body text, which is pure black on a
   light theme and pure white on a dark one; #000 on a dark page would be unreadable, so the dark
   themes invert rather than opting out. Only the three text tokens move — everything in the app
   derives its colour from them, so nothing else needs a rule, and borders/weights stay put (that is
   what the Contrast setting is for, and conflating the two would make each less predictable). */
[data-text="black"] {
  --text: #000000;
  --text-secondary: #1a1a1a;
  --text-muted: #3f3f46;
}
/* …and let the glyphs render at their real weight. body sets -webkit-font-smoothing:antialiased,
   which on macOS turns OFF subpixel rendering and makes every stroke visibly thinner — normally a
   fair trade for a softer look, but it fights the one thing this option exists for. Restoring the
   default smoothing thickens the type WITHOUT changing the typeface weight, which a font-weight bump
   would (that is what Contrast > Extra High is for, and it should stay the heavier-type control).
   Scoped to this option so nothing else on the site shifts. */
[data-text="black"] body,
[data-text="black"] .form-control,
[data-text="black"] input,
[data-text="black"] select,
[data-text="black"] textarea,
[data-text="black"] button {
  -webkit-font-smoothing: auto;
  -moz-osx-font-smoothing: auto;
}
[data-theme="black"][data-text="black"] {
  --text: #ffffff;
  --text-secondary: #ededed;
  --text-muted: #c9c9d1;
}

/* LABEL + FIELD ROW — one fixed label column, so every control in a settings list starts at the SAME
   left edge. The pattern it replaces was flex + justify-content:space-between, which pushes the field
   to the right of whatever width the label happened to take: with labels of different lengths (and
   some wrapping to two lines) no two fields lined up, and a form of ten rows read as ragged.
   A grid fixes the column instead of negotiating it per row. */
/* A LABELLED FIELD LIST — caption on the left, control on the right, one per line. This was called
   .form-row, which is also the name of the auto-fit filter grid defined near the top of this file.
   Being later in the source it won every time, so six older pages (purchase/create, purchase/active,
   qc/inhouse, qc/vehicle-test, purchase/completed, parts-inventory) that wanted the grid were being
   forced into a 148px label column instead — silently, because nothing errors when two rules share a
   name. Renamed to .form-line so both patterns can exist. */
.form-line {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.form-line:last-child { border-bottom: none; }
.form-line > .form-line-label {
  font-size:14px;
  color: var(--text);
  line-height: 1.35;
}
.form-line > .form-line-label .sub { display: block; font-size:12px; color: var(--text-muted); }
/* The field fills its column — a width set per control is what made them ragged in the first place. */
.form-line > .form-control, .form-line > .ds-sel, .form-line > select { width: 100%; }
/* Section heading inside such a list. */
.form-line-sec {
  font-size:11px; font-weight:600;
  color: var(--text-muted); margin: 16px 0 4px; padding-top: 8px;
  border-top: 1px solid var(--border);
}
@media (max-width: 560px) {
  .form-row { grid-template-columns: 1fr; gap: 6px; align-items: start; }
}

/* ============================================================================
   BACKGROUND — Appearance > Background. 'plain' strips the page back to flat
   paper on ANY theme: no ambient wash, no glass, no surface tint. It is the Pure
   White canvas offered to all nine looks, with each keeping its own accent — so
   Rose stays rose and Forest stays green, they simply stop tinting the paper.

   Placed last so it wins over the theme blocks AND over [data-glass], and written
   with both attributes where it has to outrank a two-attribute selector rather
   than rely on source order.

   On a DARK theme plain means flat dark, not white — white type on white paper is
   not a plainer page, it is an unreadable one.
   ============================================================================ */
[data-bg="plain"] .app-layout,
[data-bg="plain"][data-glass] .app-layout {
  background: #ffffff;
  background-image: none;
}
[data-bg="plain"] body, [data-bg="plain"] .main-content { background: #ffffff; }
[data-bg="plain"] .card, [data-bg="plain"] .panel, [data-bg="plain"] .stat,
[data-bg="plain"] .kpi-card, [data-bg="plain"] .ds-card, [data-bg="plain"] .ox-stat,
[data-bg="plain"] .topbar, [data-bg="plain"] .ovl > .card,
[data-bg="plain"] .modal .modal-content, [data-bg="plain"] .apx-modal,
[data-bg="plain"][data-glass] .card, [data-bg="plain"][data-glass] .panel,
[data-bg="plain"][data-glass] .stat, [data-bg="plain"][data-glass] .kpi-card,
[data-bg="plain"][data-glass] .ds-card, [data-bg="plain"][data-glass] .ox-stat,
[data-bg="plain"][data-glass] .topbar, [data-bg="plain"][data-glass] .ovl > .card {
  background: #ffffff;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 1px solid var(--border);
  box-shadow: var(--el-1);
}
[data-bg="plain"] .sidebar, [data-bg="plain"][data-glass] .sidebar {
  background: #ffffff;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border-right: 1px solid var(--border);
}
[data-bg="plain"] .topbar, [data-bg="plain"][data-glass] .topbar {
  border: none; border-bottom: 1px solid var(--border); box-shadow: none;
}
/* The nav's own tints go too — "remove the background colours" means these as much as the wash.
   The accent stays on icons, open headings and hover TEXT, which is where it carries meaning. */
[data-bg="plain"] .nav-section.is-open { background: none; }
[data-bg="plain"] .nav-item:hover, [data-bg="plain"] .nav-group:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
[data-bg="plain"] .nav-item.active, [data-bg="plain"] .nav-item.nav-parent.expanded {
  background: color-mix(in srgb, var(--text) 7%, transparent);
}

/* Dark themes: plain means FLAT DARK. */
[data-theme="black"][data-bg="plain"] .app-layout,
[data-theme="black"][data-bg="plain"][data-glass] .app-layout,
[data-theme="black"][data-bg="plain"] body { background: var(--bg); background-image: none; }
[data-theme="black"][data-bg="plain"] .card, [data-theme="black"][data-bg="plain"] .panel,
[data-theme="black"][data-bg="plain"] .stat, [data-theme="black"][data-bg="plain"] .kpi-card,
[data-theme="black"][data-bg="plain"] .ds-card, [data-theme="black"][data-bg="plain"] .ox-stat,
[data-theme="black"][data-bg="plain"] .sidebar, [data-theme="black"][data-bg="plain"] .topbar,
[data-theme="black"][data-bg="plain"] .ovl > .card {
  background: var(--card); background-image: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 1px solid var(--border);
}

/* ==============================================================================================
   MODULE DASHBOARD CARD + IDENTITY BADGES
   Shared by the three pages that show "Your plant modules" — the department workspace, the plant
   hub and the dashboard landing — so all three change together instead of drifting.

   Why the card changed: the old card printed its figures as a row of bare numbers, and on a quiet
   day fifteen of the eighteen were 0. Red and green were applied by LABEL ("Overdue" is a bad-news
   word, so it was red) which meant a completely clear board still read as an emergency. The rules
   now are: a number gets a box, the box carries the tone, and A ZERO CARRIES NO VERDICT WHATEVER
   ITS LABEL — "0 overdue" is good news and is drawn as grey, not red.

   Every tone uses the --tint-*-bg/fg PAIRS that already exist for badges and status pills, which is
   the only family in this file that flips for [data-theme="black"]. Mixing our own colours here
   would have frozen the boxes light-on-dark on that theme. ============================================ */
/* align-items:start — a card is as tall as its own contents. Without it every card in a row is
   stretched to the tallest, and one module reporting four figures (Purchase) leaves a hand's width
   of empty card under the three-figure ones beside it. */
.md-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(272px, 1fr)); gap: 12px;
           align-items: start; }

/* The card keeps the app's .card class and this adds ONLY what is particular to a module tile.
   Deliberately no background / border / radius here: .card is re-skinned by the white, flat and
   black themes and by the per-user [data-glass] toggle, and re-declaring the surface at equal
   specificity later in this file would beat all of them — module tiles would sit flat and opaque
   while every other card on the same page turned to glass or paper. */
.md-card { padding: 16px 16px; text-decoration: none; color: inherit; display: block;
           transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease; }
.md-card:hover { border-color: var(--primary); transform: translateY(-2px);
                 box-shadow: 0 6px 18px rgba(59,130,246,.16); }
/* One quiet mark for "this module has something wrong RIGHT NOW" — a left edge, not a red card. */
.md-card.md-flag { border-left: 3px solid var(--danger); }

.md-hd { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.md-ic { width: 36px; height: 36px; border-radius: var(--radius-sm); flex: none; display: flex;
         align-items: center; justify-content: center;
         background: var(--primary-light); color: var(--primary-dark); }
.md-nm { font-weight:600; font-size:15px; color: var(--text); flex: 1; min-width: 0; }
.md-sc { display: block; font-size:11px; color: var(--text-muted); font-weight: 400; margin-top: 2px; }
.md-sc.md-unitless { color: var(--text-hint); font-style: italic; }

/* The access mark. It replaced a row of Create/Edit/Approve chips that looked like buttons you
   could press on a card that is itself one big link. The SYMBOL carries the meaning — pencil you
   may write, check you may authorise, eye you may only look — and the word just names it. Frosted
   rather than filled so it sits on the card without competing with the figures below it. */
.md-perm { display: inline-flex; align-items: center; gap: 6px; flex: none; white-space: nowrap;
           padding: 6px 12px; border-radius: var(--radius-pill); font-size:12px;
           background: color-mix(in srgb, var(--card) 55%, transparent);
           -webkit-backdrop-filter: saturate(1.5) blur(26px); backdrop-filter: saturate(1.5) blur(26px);
           border: 1px solid color-mix(in srgb, #fff 55%, transparent);
           box-shadow: var(--el-3), inset 0 1px 0 rgba(255,255,255,.7); }
.md-perm b { font-weight:600; color: var(--text-muted); }
/* icons.js turns the <i class="ti …"> into a <span><svg>, so a rule written against .ti never
   lands — and a rule written by POSITION grabs the icon instead of the text. Both are addressed
   by class. */
.md-perm > span:not(.k) { display: inline-flex; color: var(--text-muted); }
.md-perm > span:not(.k) svg { display: block; }
.md-perm.can b, .md-perm.can > span:not(.k) { color: var(--primary-dark); }
.md-perm.can { border-color: color-mix(in srgb, var(--primary) 32%, #fff); }
[data-theme="black"] .md-perm {
  background: color-mix(in srgb, #fff 9%, transparent);
  border-color: color-mix(in srgb, #fff 16%, transparent);
  box-shadow: var(--el-3), inset 0 1px 0 rgba(255,255,255,.14); }

/* The boxes. Equal widths so the row reads as a set the eye compares, rather than three separate
   readings. Column count follows the number of figures the module actually reports (2, 3 or 4). */
.md-cubes { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
/* ★ THE SAME TILES, IN A WIDE CARD. .md-cubes divides its container into equal columns, which is
   right inside a module card a third of the grid wide. The registers strip sits in a card the full
   width of the page, so three 1fr columns made each figure enormous — the number was the same size
   but the box around it was three times the width it needed. Here they size to their content and
   sit left, which is what a row of readings wants. */
/* Centred: the strip sits in a card the full width of the page, so a short row of readings left
   against one edge leaves the card looking half-filled. Centring makes the row read as the card's
   content rather than as something that ran out. */
.md-cubes.md-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
/* ONE WIDTH FOR ALL OF THEM. flex:0 0 auto sized each box to its own label, so a row of readings
   came out 98, 98, 116, 130 — ragged, and the eye reads the widest as the most important when
   nothing of the sort is meant. A fixed basis makes them a set: same box, different number. The
   label wraps inside rather than widening the box. */
.md-cubes.md-row .md-cube { flex: 0 0 132px; width: 132px; padding-left: 8px; padding-right: 8px; }
.md-cubes.md-row .md-cube .l { overflow-wrap: anywhere; }
.md-cubes[data-n="1"] { grid-template-columns: 1fr; }
.md-cubes[data-n="2"], .md-cubes[data-n="4"] { grid-template-columns: repeat(2, 1fr); }
.md-cube { border-radius: var(--radius); padding: 12px 8px 8px; text-align: center;
           border: 1px solid transparent; min-width: 0; }
.md-cube .n { font-size:25px; font-weight:600; line-height: 1; font-variant-numeric: tabular-nums; }
.md-cube .l { font-size:11px; font-weight:600;
              margin-top: 5px; line-height: 1.3; opacity: .85; }

.md-cube.bad     { background: var(--tint-danger-bg);  color: var(--tint-danger-fg);
                   border-color: color-mix(in srgb, var(--tint-danger-fg) 20%, transparent); }
.md-cube.warn    { background: var(--tint-warn-bg);    color: var(--tint-warn-fg);
                   border-color: color-mix(in srgb, var(--tint-warn-fg) 20%, transparent); }
.md-cube.ok      { background: var(--tint-ok-bg);      color: var(--tint-ok-fg);
                   border-color: color-mix(in srgb, var(--tint-ok-fg) 20%, transparent); }
.md-cube.info    { background: var(--tint-info-bg);    color: var(--tint-info-fg);
                   border-color: color-mix(in srgb, var(--tint-info-fg) 20%, transparent); }
/* Every other tone sets a border to match its tint; this one did not, so a plain figure sat
   edgeless next to four that had an edge and it read as a rule rather than the omission it was. */
.md-cube.plain   { background: var(--primary-light);   color: var(--primary-dark);
                   border-color: color-mix(in srgb, var(--primary-dark) 20%, transparent); }
/* A zero is not a verdict — grey whatever the label says. */
.md-cube.zero    { background: var(--tint-neutral-bg); color: var(--tint-neutral-fg);
                   border-color: var(--border); }
.md-cube.zero .n { font-weight:600; opacity: .65; }
.md-cube.zero .l { opacity: .7; }
.md-empty { font-size:13px; color: var(--text-muted); }

/* ---- PER-PREMISES SPLIT on a module card ------------------------------------------------------
   For a reader whose rights span several sites. The card's own figures stay the headline (the
   corporate total is the corporate view); this says where that total actually sits, one line per
   premises. Deliberately quiet — it is a breakdown, not a second set of statistics — and it only
   appears when there is more than one site and none has been picked.
   Every colour is a token, because these cards are read under nine themes. */
.md-prem { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--border); display: grid; gap: 2px; }
.md-prem-cap { font-size:9px; font-weight:600;
               color: var(--text-muted); margin-bottom: 3px; }
.md-prem-row { display: flex; align-items: baseline; gap: 8px; font-size:12px; color: var(--text-muted); }
.md-prem-row .p { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.md-prem-row .v { font-weight:600; font-variant-numeric: tabular-nums; color: var(--text); flex: none; }
/* A site with something genuinely wrong is the reason to read this at all, so it says so. */
.md-prem-row.hot .v { color: var(--danger); }
.md-prem-alert { font-size:11px; color: var(--danger); margin: -1px 0 3px; }

/* The dashboard's own premises picker — links, not a select, because the page is server-rendered and
   the choice belongs in the URL where it can be reloaded and shared. */
.md-prem-pick { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size:12px; }
/* the unit strip under a module card — one bar per unit, heights against the loudest unit; the filled
   bar is the unit to look at; colour only when the figure is a state (bad / warn), grey on zero */
/* ★ THE BAR HAS A TRACK OF ITS OWN (15-10). The bar's height was a share of the WHOLE 44 px column, so the tallest bar took
   the column and its value and its unit's name were pushed past the strip's edge - the names under the busiest units were
   cut in half on every Home. The value and the name now take their own lines and the bar fills only the track between. */
.md-ustrip { display: flex; align-items: stretch; gap: 3px; height: 58px; margin-top: 14px; overflow: hidden; }
.md-ubar { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; text-decoration: none; color: inherit; position: relative; }
.md-ubar .t { flex: 1 1 auto; min-height: 0; width: 100%; display: flex; align-items: flex-end; }
.md-ubar i { display: block; width: 100%; background: var(--primary-light); border-radius: 3px 3px 0 0; min-height: 2px; }
.md-ubar.prem { cursor: default; }
.md-ubar.prem i { background-image: repeating-linear-gradient(135deg, transparent 0 3px, color-mix(in srgb, var(--text-muted) 22%, transparent) 3px 5px); }
.md-ubar.top i { background: var(--primary); }
.md-ubar.warn i { background: var(--warning); } .md-ubar.bad i { background: var(--danger); }
.md-ubar .v { flex: none; font-size:9px; font-weight:600; color: var(--text); line-height: 1.1; margin-bottom: 2px; min-height: 10px; font-variant-numeric: tabular-nums; }
.md-ubar .u { flex: none; font-size:8.5px; font-weight:600; color: var(--text-muted); letter-spacing: .02em; line-height: 1.25; margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.md-ubar:hover i { filter: brightness(.92); }
/* the rail on the landing page — units of the premises in view, as link chips */
.md-unit-pick { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size:12px; }
.md-unit-pick a { text-decoration: none; color: var(--text); border: 1px solid var(--border); background: var(--card); border-radius: var(--radius-sm); padding: 4px 10px; font-weight:600; font-size:12px; }
.md-unit-pick a.on { background: var(--accent-solid, var(--primary-dark)); color: #fff; border-color: transparent; }
.md-unit-pick > span { color: var(--text-muted); font-weight:600; }
.md-prem-pick > span { color: var(--text-muted); }
.md-prem-pick a {
  padding: 2px 8px; border-radius: var(--radius-pill); text-decoration: none;
  color: var(--text-muted); border: 1px solid var(--border); background: var(--card);
  transition: background .15s, color .15s, border-color .15s;
}
.md-prem-pick a:hover { color: var(--text); border-color: var(--text-muted); }
/* --accent-solid, not --primary: this pill carries WHITE ink, and --primary is a fill that flips
   bright on every dark theme — measured on Liquid Dark it became rgb(31,187,211) and the label of
   the premises you are looking at read 2.31:1. --accent-solid is the token for exactly this pair
   and is dark in both halves (5.36:1 on Liquid, 6.70:1 on Glass). */
.md-prem-pick a.on { background: var(--accent-solid); border-color: var(--accent-solid); color: #fff; font-weight: 600; }

/* ---- IDENTITY BADGES -------------------------------------------------------------------------
   Where a person sits: their department and their unit. These used to run together in one grey
   line joined by middots — but a middot only says "and also", never what the next token IS, so a
   reader had to already know the company to tell a unit name from a department name. The badge
   states both: the VALUE in the strong weight, WHAT KIND OF THING IT IS in the soft one. */
.ident { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 7px; }
.id-badge { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
            line-height: 1.2; font-size:12px; max-width: 100%;
            padding: 4px 12px; border-radius: var(--radius-pill);
            border: 1px solid var(--border); background: var(--card); }
.id-badge b { font-weight:600; color: var(--text); }
.id-badge .k { font-weight: 400; color: var(--text-muted); }
.id-badge > span:not(.k) { display: inline-flex; flex: none; color: var(--text-muted); }
.id-badge > span:not(.k) svg { display: block; }

/* ==============================================================================================
   PLAN VS ACTUAL — the target rows on the analytics board (unit / plant / corporate all share the
   same unitCard, so this is defined once).

   What it replaced: a bar reading "3,387 / 8,250 TPD · 41%" with the colour hardcoded to
   #10b981 / #f59e0b / #ef4444, which froze on every theme. Three things were wrong beyond that:

     1. An UNKNOWN actual was drawn as an empty bar, identical to "produced nothing". On a plant
        where 20 of 24 rows had nothing filed yet, the board read as a dead site.
     2. Month-to-date and year-to-date were measured against the WHOLE period's plan, so a board is
        behind by definition until the last day of the period — 23% of an annual plan looks like a
        disaster in July when only a third of the year has gone.
     3. The target's uom is a RATE (TPD = tonnes per day). It was printed against the monthly and
        annual figures too, so the board said "99,000 TPD" when it meant 99,000 tonnes.
   ============================================================================================== */
.pva { padding: 12px 0; border-bottom: 1px solid var(--border); }
.pva:last-child { border-bottom: none; padding-bottom: 2px; }
.pva .pva-top { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.pva .pva-hz { font-size:12px; font-weight:600;
               color: var(--text-muted); flex: 0 0 104px; }
.pva .pva-sp { flex: 1; min-width: 0; }
.pva .pva-fig { font-size:13px; }
.pva .pva-fig b { font-weight:600; font-variant-numeric: tabular-nums; }
.pva .pva-fig i { font-style: normal; color: var(--text-muted); }

/* The reading and the verdict, in the two-weight badge: the value in the strong weight, what it
   MEANS in the soft one. Tones come from the --tint-* pairs, the one family that flips on the dark
   theme, so no row can freeze light-on-dark. */
.pva-b { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; line-height: 1.2;
         padding: 4px 12px; border-radius: var(--radius-pill); font-size:12px; border: 1px solid transparent; }
.pva-b b { font-weight:600; font-variant-numeric: tabular-nums; }
.pva-b i { font-style: normal; font-weight: 400; opacity: .85; }
.pva-b.ok   { background: var(--tint-ok-bg);     color: var(--tint-ok-fg);
              border-color: color-mix(in srgb, var(--tint-ok-fg) 20%, transparent); }
.pva-b.warn { background: var(--tint-warn-bg);   color: var(--tint-warn-fg);
              border-color: color-mix(in srgb, var(--tint-warn-fg) 20%, transparent); }
.pva-b.bad  { background: var(--tint-danger-bg); color: var(--tint-danger-fg);
              border-color: color-mix(in srgb, var(--tint-danger-fg) 20%, transparent); }
/* Nothing filed is not nought produced — neutral, and it says so in words. */
.pva-b.none { background: var(--tint-neutral-bg); color: var(--tint-neutral-fg); border-color: var(--border); }

.pva-trk { height: 7px; background: var(--tint-neutral-bg); border-radius: var(--radius-sm); overflow: hidden;
           position: relative; }
.pva-trk > i { display: block; height: 100%; border-radius: var(--radius-sm); }
.pva-trk.ok > i   { background: var(--success); }
.pva-trk.warn > i { background: var(--warning); }
.pva-trk.bad > i  { background: var(--danger); }
.pva-src { font-size:11px; color: var(--text-muted); margin-top: 9px; }

/* ==============================================================================================
   THE TWO-WEIGHT BADGE — .b2w
   One phrase, two weights: a strong term and a soft one that read together —
   "4 requisitions awaiting authorisation", "10 of 10 received".

       <span class="b2w"><b>4 requisitions</b><i>awaiting authorisation</i></span>

   The TREATMENT is a per-user appearance choice ([data-badge] on <html>, picked in Appearance),
   not a per-page decision, because the same person reads this app all day and the badge is the
   thing they read most often. The eight below are the variants that survived the design review,
   kept at their review numbers so the picker, this file and that review all name the same things.
   Every one is built from tokens, so each also follows the user's theme.
   ============================================================================================== */
.b2w { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; line-height: 1.2;
       vertical-align: middle; }
.b2w > b { font-weight:600; }
.b2w > i { font-style: normal; font-weight: 400; }

/* 1 · GLASS — the reference. Translucent, blurred, a light hairline along the top edge. */
[data-badge="1"] .b2w { padding: 6px 16px; border-radius: var(--radius-pill); font-size:13px;
  background: color-mix(in srgb, var(--card) 55%, transparent);
  -webkit-backdrop-filter: saturate(1.5) blur(26px); backdrop-filter: saturate(1.5) blur(26px);
  border: 1px solid color-mix(in srgb, #fff 55%, transparent);
  box-shadow: var(--el-3), inset 0 1px 0 rgba(255,255,255,.7); }
[data-badge="1"] .b2w > b { color: var(--text); }
[data-badge="1"] .b2w > i { color: var(--text-muted); }
[data-theme="black"][data-badge="1"] .b2w {
  background: color-mix(in srgb, #fff 9%, transparent);
  border-color: color-mix(in srgb, #fff 16%, transparent);
  box-shadow: var(--el-3), inset 0 1px 0 rgba(255,255,255,.14); }

/* 2 · SOLID — a filled accent pill. The loudest of the eight. */
[data-badge="2"] .b2w { padding: 6px 16px; border-radius: var(--radius-pill); font-size:13px;
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  box-shadow: 0 5px 14px color-mix(in srgb, var(--primary) 36%, transparent); }
[data-badge="2"] .b2w > b { color: #fff; }
[data-badge="2"] .b2w > i { color: rgba(255,255,255,.78); }

/* 3 · HAIRLINE — no fill at all. The quietest, and the safest in a dense list. */
[data-badge="3"] .b2w { padding: 6px 12px; border-radius: var(--radius-pill); font-size:13px;
  border: 1px solid var(--border); background: var(--card); }
[data-badge="3"] .b2w > b { color: var(--text); }
[data-badge="3"] .b2w > i { color: var(--text-muted); }

/* 4 · SPLIT — the strong half tinted, the soft half plain: a label and its value. */
[data-badge="4"] .b2w { border-radius: var(--radius-pill); font-size:13px; overflow: hidden; gap: 0;
  border: 1px solid var(--border); background: var(--card); }
[data-badge="4"] .b2w > b { background: var(--primary-light); color: var(--primary-dark); padding: 6px 12px; }
[data-badge="4"] .b2w > i { color: var(--text-muted); padding: 6px 12px; }

/* 6 · LEFT RULE — a bar of colour instead of a pill; carries a tone without another rounded shape. */
[data-badge="6"] .b2w { padding: 6px 12px; font-size:13px; background: var(--bg);
  border-left: 3px solid var(--primary); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
[data-badge="6"] .b2w > b { color: var(--text); }
[data-badge="6"] .b2w > i { color: var(--text-muted); }

/* 8 · COUNT — the number set apart in its own chip. For figures rather than states. */
[data-badge="8"] .b2w { padding: 4px 12px 4px 4px; border-radius: var(--radius-pill); font-size:13px;
  background: var(--card); border: 1px solid var(--border); }
[data-badge="8"] .b2w > b { background: var(--text); color: var(--card); border-radius: var(--radius-pill);
  padding: 4px 8px; font-variant-numeric: tabular-nums; }
[data-badge="8"] .b2w > i { color: var(--text-muted); }

/* 10 · SOFT TINT — the tone fills the badge, as our status pills already do. */
[data-badge="10"] .b2w { padding: 6px 12px; border-radius: var(--radius-pill); font-size:13px;
  background: var(--primary-light); }
[data-badge="10"] .b2w > b { color: var(--primary-dark); }
[data-badge="10"] .b2w > i { color: color-mix(in srgb, var(--primary-dark) 72%, transparent); }

/* 11 · DARK GLASS — the same glass inverted, for a dark bar or a hero band. */
[data-badge="11"] .b2w { padding: 6px 16px; border-radius: var(--radius-pill); font-size:13px;
  background: rgba(15,23,42,.42);
  -webkit-backdrop-filter: saturate(1.4) blur(22px); backdrop-filter: saturate(1.4) blur(22px);
  border: 1px solid rgba(255,255,255,.22);
  box-shadow: var(--el-4), inset 0 1px 0 rgba(255,255,255,.28); }
[data-badge="11"] .b2w > b { color: #fff; }
[data-badge="11"] .b2w > i { color: rgba(255,255,255,.72); }

/* Blur is expensive and some people simply cannot read through it. Both glass treatments fall back
   to an opaque surface when the user has turned glass off or asked for reduced transparency. */
[data-glass="off"] .ds-pop, [data-glass="off"] .nav-fly, [data-glass="off"] .ui-expmenu,
[data-glass="off"] .cacct-pop, [data-glass="off"] .ox-cardpop {
  background: var(--card);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border-color: var(--border);
}
[data-glass="off"][data-badge="1"] .b2w, [data-glass="off"][data-badge="11"] .b2w {
  -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--card);
  border-color: var(--border); box-shadow: var(--shadow); }
[data-glass="off"][data-badge="11"] .b2w > b { color: var(--text); }
[data-glass="off"][data-badge="11"] .b2w > i { color: var(--text-muted); }

/* The badge in its approved places -------------------------------------------------------------
   The look of .b2w is the user's Appearance choice; these rules only say how it SITS on each
   surface, and let the data tint it where the data carries a verdict. */

/* 1 · Document header — the number and its state, ahead of the context. */
.doc-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.doc-ctx { font-size:13px; color: var(--text-muted); }

/* 3 · Approval queue — the count and what it is, as one phrase instead of a number over a caption.
   The tone rides on the badge whatever treatment the user picked, which is why it is written as an
   override of the two halves rather than as a whole new pill. */
/* ★ THE ONE PLACE .b2w MUST BE ALLOWED TO WRAP. `.b2w` is `white-space: nowrap` by design — it is
   an inline badge inside table rows and headers, where a wrap would break the row's rhythm. But a
   queue tile is a FIXED-WIDTH GRID CELL (minmax(220px,1fr)), and these labels are data, not
   fixtures: "Work orders to close" measures a 206px badge inside a 240px inner box, while "Overdue
   PM to defer or clear" measures 252px — 12px past the padding at 1440, and more in a narrower
   column or a longer translation. Being nowrap it could neither wrap nor shrink, so it spilled and
   the next tile (later in the DOM) painted over it: the label read "…to defer or cle" with the
   neighbouring card's edge cutting through it. Truncating would throw away the words, which is the
   complaint; the count keeps the first line and the label flows onto a second. */
.queue-tile .b2w { font-size:15px; max-width: 100%; }
.queue-tile .b2w > b { font-size:17px; font-variant-numeric: tabular-nums; flex: none; }
/* NOT flex-wrap on the badge: given the choice, flex moves a whole item to the next line rather
   than shrinking it, which put the count alone on line one with the label beneath and a tile's
   width of dead space beside it. Keeping the row and letting the LABEL shrink and wrap inside its
   own box keeps "13" next to the first words. `min-width:0` is the part that does it — a flex item
   will not go below its content width without it. `align-items: baseline` (inherited from .b2w)
   then aligns the count to the label's FIRST line, not its last. */
.queue-tile .b2w > i { white-space: normal; min-width: 0; flex: 1 1 auto; }
.b2w.is-bad  > b { color: var(--tint-danger-fg); }
.b2w.is-warn > b { color: var(--tint-warn-fg); }
[data-badge="2"] .b2w.is-bad  { background: linear-gradient(135deg, var(--danger), color-mix(in srgb, var(--danger) 70%, #000)); }
[data-badge="2"] .b2w.is-warn { background: linear-gradient(135deg, var(--warning), color-mix(in srgb, var(--warning) 70%, #000)); }
[data-badge="2"] .b2w.is-bad > b, [data-badge="2"] .b2w.is-warn > b { color: #fff; }
[data-badge="10"] .b2w.is-bad  { background: var(--tint-danger-bg); }
[data-badge="10"] .b2w.is-bad > i { color: var(--tint-danger-fg); }
[data-badge="10"] .b2w.is-warn { background: var(--tint-warn-bg); }
[data-badge="10"] .b2w.is-warn > i { color: var(--tint-warn-fg); }
[data-badge="6"] .b2w.is-bad  { border-left-color: var(--danger); }
[data-badge="6"] .b2w.is-warn { border-left-color: var(--warning); }
[data-badge="8"] .b2w.is-bad  > b { background: var(--danger); color: #fff; }
[data-badge="8"] .b2w.is-warn > b { background: var(--warning); color: #fff; }

/* 4 · Topbar — a badge in the chrome must not grow the bar, so it is size-capped there. */
.topbar .b2w { font-size:12px; }
.topbar .b2w > b { font-weight:600; }

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   IN-PAGE CREATE FORM (ui.formPage) — raising a work order, a PM schedule or a permit.

   These are long, consequential forms, so they get the whole content column rather than a floating
   dialog: title bar sticky at the top so you always know what you are filling in, actions sticky at
   the bottom WITH the "still needed" rail beside them, so the thing that is missing and the button it
   blocks are never in different places. Sits in the 1–60 page-content band of the z-index scale
   documented on .topbar — under the top bar (90), never over it.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */
.ds-fp { display: none; }
.ds-fp.ds-fp-open { display: block; }   /* full content width — a form is not a dialog, it is the page */
/* The bar holds BOTH rows — the title and the section navigation — so one sticky element keeps them
   together. Two separate sticky elements have to agree about each other's height, and they stop
   agreeing the moment a title wraps. */
.ds-fp-bar {
  position: sticky; top: var(--topbar-height); z-index: 30;
  display: flex; flex-direction: column; align-items: stretch; gap: 12px;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius) var(--radius) 0 0;
  border-bottom-color: var(--border); padding: 12px 16px 0;
  /* A pinned bar needs to sit ON TOP of the form, visibly — the fields scroll underneath it, and
     without a shadow the row passing behind the strip reads as part of the strip. */
  box-shadow: var(--el-3);
}
.ds-fp-bar-top { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.ds-fp-back {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; flex: none; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg);
  color: var(--text); cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
.ds-fp-back:hover { background: var(--card); border-color: var(--primary); color: var(--primary); }
.ds-fp-heading { flex: 1; min-width: 220px; }
.ds-fp-eyebrow { font-size:11px; font-weight:600;
  color: var(--primary); }
.ds-fp-title { margin: 2px 0 0; font-size:21px; font-weight:600; letter-spacing: -.01em; color: var(--text); }
.ds-fp-sub { font-size:13px; color: var(--text-hint, var(--text-muted)); margin-top: 3px; }
.ds-fp-bar-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.ds-fp-body { background: var(--card); border: 1px solid var(--border); border-top: none; padding: 16px; }

/* THE SECTION NAVIGATION — a strip along the FOOT OF THE TITLE BAR, so it is on screen at every
   depth of the form and never scrolls away with the fields it describes. Full-bleed inside the bar's
   padding, with an underline for the current section: a form's progress belongs in its chrome. */
.ds-fp-nav { display: flex; flex-direction: row; align-items: stretch; gap: 2px;
  /* Centred in the bar. `safe center` matters on a narrow screen: plain centring pushes the first
     item off the scrollable start and it can never be reached, so the row falls back to starting at
     the left edge the moment the sections stop fitting. */
  justify-content: safe center;
  margin: 0 -18px; padding: 0 16px; border-top: 1px solid var(--border); overflow-x: auto;
  scrollbar-width: thin; }
.ds-fp-nav::-webkit-scrollbar { height: 5px; }
.ds-fp-nav::-webkit-scrollbar-thumb { background: var(--border); border-radius: var(--radius-sm); }
.ds-fn-item { display: inline-flex; align-items: center; gap: 8px; flex: none; white-space: nowrap;
  margin: 7px 0 9px; padding: 6px 12px; border: 1px solid transparent; border-radius: var(--radius-pill);
  background: none; cursor: pointer;
  font-family: inherit; font-size:13px; font-weight: 600; color: var(--text-muted);
  transition: color .15s, background .15s, border-color .15s; }
.ds-fn-item:hover { color: var(--text); background: var(--bg); }
/* The section you are in is a BADGE in the page's own accent — a rule under the label competed with
   the bar's own bottom edge, and on a themed page an underline is the one thing that cannot pick up
   the accent without also looking like a border. Tokens throughout, so it follows the user's theme. */
.ds-fn-item.is-here { background: var(--primary-light); color: var(--primary-dark);
  border-color: color-mix(in srgb, var(--primary) 35%, transparent); }
.ds-fn-item.is-here .ds-fn-mark { background: var(--primary); color: #fff; }
.ds-fn-item.is-here .ds-fn-mark.is-done { background: var(--success, #10b981); color: #fff; }
.ds-fn-txt { min-width: 0; }
.ds-fn-txt small { font-size:12px; font-weight:600; color: var(--warning-dark, #b45309); margin-left: 7px; }
.ds-fn-mark { flex: none; width: 22px; height: 22px; border-radius: 50%; display: inline-flex;
  align-items: center; justify-content: center; font-size:11px; font-weight:600;
  background: var(--tint-neutral-bg, #f1f5f9); color: var(--text-muted); }
.ds-fn-mark.is-todo { background: var(--tint-warn-bg, #fef3c7); color: var(--tint-warn-fg, #92400e); }
.ds-fn-mark.is-done { background: var(--success-light, #d1fae5); color: var(--success-dark, #059669); }

/* Section heading, shared by all three forms so they cannot drift into three looks. */
.ds-fp-sec { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size:12px; font-weight:600;
  color: var(--text-muted); margin: 22px 0 11px; padding-top: 16px; border-top: 1px solid var(--border); }
.ds-fp-sec:first-child { border-top: none; padding-top: 0; margin-top: 0; }
.ds-fp-sec .ds-fp-sec-note { font-weight: 500; text-transform: none; letter-spacing: 0;
  font-size:12px; color: var(--text-hint, var(--text-muted)); }
.ds-fp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ds-fp-grid .wide { grid-column: 1 / -1; }

.ds-fp-foot {
  position: sticky; bottom: 0; z-index: 30;
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  background: var(--card); border: 1px solid var(--border); border-radius: 0 0 var(--radius) var(--radius);
  border-top: 1px solid var(--border); padding: 12px 16px; box-shadow: var(--el-3);
}
.ds-fp-foot .ds-fp-railslot { flex: 1; min-width: 200px; }
.ds-fp-foot .ui-rail { margin-top: 0; }               /* it is inline in the bar here, not stacked under a form */
.ds-fp-foot-acts { display: flex; align-items: center; gap: 8px; }

@media (max-width: 900px) {
  .ds-fn-item { padding: 8px; font-size:12px; }
  .ds-fn-txt small { display: none; }        /* the mark already says a section is outstanding */
}
@media (max-width: 720px) {
  .ds-fp-bar, .ds-fp-foot { padding: 12px 12px; }
  .ds-fp-grid { grid-template-columns: 1fr; }
  .ds-fp-foot { flex-direction: column; align-items: stretch; }
  .ds-fp-foot-acts { justify-content: flex-end; }
  .ds-fp-title { font-size:17px; }
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   MODULE STORE — what a customer buys with, so it has to read like something you would buy from.
   Floating cards that lift on hover, one accent per module, and a security panel that carries the
   claims true of all of them. Tokens throughout: this page re-themes like every other.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */
.st-hero { display: flex; justify-content: space-between; align-items: flex-end; gap: 20px; flex-wrap: wrap;
  padding: 20px 24px; margin-bottom: 20px; border-radius: var(--radius); border: 1px solid var(--border);
  background: linear-gradient(135deg, var(--primary-light) 0%, var(--card) 62%); box-shadow: var(--shadow); }
.st-eyebrow { font-size:11px; font-weight:600; color: var(--primary-dark); }
.st-h1 { margin: 4px 0 0; font-size:25px; font-weight:600; letter-spacing: -.02em; color: var(--text); }
.st-sub { margin: 6px 0 0; max-width: 62ch; font-size:14px; line-height: 1.55; color: var(--text-hint, var(--text-muted)); }
.st-hero-plant { min-width: 210px; }

.st-sec-h { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin: 26px 0 12px; }
.st-sec-h h3 { margin: 0; font-size:17px; font-weight:600; color: var(--text); }
.st-sec-h span { font-size:13px; color: var(--text-hint, var(--text-muted)); }

/* ---- a premium module ---- */
.st-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 16px; }
.st-card { position: relative; display: flex; flex-direction: column; gap: 12px;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 16px;
  box-shadow: var(--el-1);
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease; }
.st-card::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 4px; border-radius: var(--radius) 0 0 var(--radius);
  background: var(--st-accent, var(--primary)); }
.st-card:hover { transform: translateY(-3px); box-shadow: var(--el-3);
  border-color: color-mix(in srgb, var(--st-accent, var(--primary)) 40%, var(--border)); }
.st-card.is-picked { border-color: var(--st-accent, var(--primary));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--st-accent, var(--primary)) 18%, transparent), var(--el-3); }
.st-card.is-owned { opacity: .82; }
.st-blue   { --st-accent: var(--primary); }
.st-green  { --st-accent: var(--success, #10b981); }
.st-violet { --st-accent: #8b5cf6; }
.st-amber  { --st-accent: var(--warning, #f59e0b); }

.st-top { display: flex; align-items: flex-start; gap: 12px; }
.st-ico { flex: none; width: 40px; height: 40px; border-radius: var(--radius); display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--st-accent, var(--primary)) 14%, transparent); color: var(--st-accent, var(--primary)); }
.st-name { font-size:15px; font-weight:600; color: var(--text); }
.st-tag { font-size:13px; color: var(--text-hint, var(--text-muted)); margin-top: 2px; line-height: 1.45; }
.st-price { text-align: right; white-space: nowrap; }
.st-price b { display: block; font-size:21px; font-weight:600; color: var(--text); }
.st-price small { font-size:11px; color: var(--text-muted); }
.st-owned { flex: none; display: inline-flex; align-items: center; gap: 4px; font-size:11px; font-weight:600;
  padding: 4px 8px; border-radius: var(--radius-pill); background: var(--success-light, #d1fae5); color: var(--success-dark, #059669); }

.st-limit { font-size:12px; color: var(--text); background: var(--bg); border-radius: var(--radius-sm); padding: 6px 8px;
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.st-limit span { color: var(--text-muted); font-weight: 400; }
.st-list { margin: 0; padding-left: 16px; display: flex; flex-direction: column; gap: 6px; }
.st-list li { font-size:13px; line-height: 1.5; color: var(--text); }

.st-sec { border-top: 1px solid var(--border); padding-top: 8px; }
.st-sec summary { cursor: pointer; font-size:12px; font-weight:600; color: var(--text-muted);
  display: flex; align-items: center; gap: 6px; list-style: none; }
.st-sec summary::-webkit-details-marker { display: none; }
.st-sec summary:hover { color: var(--text); }
.st-sec[open] summary { color: var(--text); margin-bottom: 7px; }
.st-sec ul { margin: 0; padding-left: 16px; display: flex; flex-direction: column; gap: 6px; }
.st-sec li { font-size:12px; line-height: 1.5; color: var(--text-hint, var(--text-muted)); }

.st-foot { margin-top: auto; padding-top: 4px; }
.st-have { font-size:12px; color: var(--text-muted); }

/* ---- security panel ---- */
.st-shield { margin: 22px 0 6px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--sidebar-bg, #0f172a); color: #e2e8f0; padding: 20px 20px; }
.st-shield-h { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 16px; color: #fff; }
.st-shield-h b { display: block; font-size:17px; font-weight:600; }
.st-shield-h span { display: block; font-size:12px; color: #94a3b8; margin-top: 2px; }
.st-shield-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 16px; }
.st-shield-item { display: flex; gap: 12px; align-items: flex-start; }
.st-shield-item > span { flex: none; width: 30px; height: 30px; border-radius: var(--radius-sm); display: inline-flex;
  align-items: center; justify-content: center; background: rgba(59,130,246,.18); color: #93c5fd; }
.st-shield-item b { font-size:13px; color: #fff; }
.st-shield-item p { margin: 3px 0 0; font-size:12px; line-height: 1.5; color: #cbd5e1; }

/* ---- production packages ---- */
.st-cat {
  text-transform: capitalize; font-size:11px; font-weight:600;
  color: var(--text-muted); margin: 16px 0 8px; }
.st-pkgs { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 8px; }
.st-pkg { display: flex; gap: 8px; align-items: flex-start; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 12px; background: var(--card);
  transition: border-color .15s, box-shadow .15s, transform .15s; }
.st-pkg:hover { border-color: var(--primary); box-shadow: var(--el-3); transform: translateY(-1px); }
.st-pkg.is-owned { opacity: .6; cursor: default; }
.st-pkg.is-owned:hover { border-color: var(--border); box-shadow: none; transform: none; }
.st-pkg input { margin-top: 3px; width: 15px; height: 15px; accent-color: var(--primary); flex: none; }
.st-pkg-n { font-weight: 600; font-size:14px; color: var(--text); }
.st-pkg-p { font-size:13px; font-weight:600; color: var(--primary); margin-top: 2px; }
.st-pkg-o { font-size:11px; color: var(--success-dark, #059669); display: flex; align-items: center; gap: 4px; margin-top: 2px; }

/* ---- extra login seats (the ten-pack picker) ---- */
.st-seats { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;
  border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; background: var(--card); }
.st-seat-qty { display: inline-flex; align-items: center; gap: 12px; }
.st-seat-qty b { min-width: 22px; text-align: center; font-variant-numeric: tabular-nums; }

/* ---- the running total ---- */
.st-bar { position: sticky; bottom: 0; z-index: 20; margin-top: 22px;
  display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 12px 16px; box-shadow: var(--el-3); }

@media (max-width: 640px) {
  .st-hero { padding: 16px; } .st-h1 { font-size:21px; }
  .st-grid { grid-template-columns: 1fr; }
}

/* Chat & Calls — one surface. The tab strip above the conversation list, and the call buttons that
   let you ring the person whose thread is open. */
.ch-callbtns { display: inline-flex; gap: 6px; margin-left: auto; }
.ch-callbtn { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
  border: 1px solid var(--border); border-radius: 50%; background: var(--card); color: var(--primary);
  cursor: pointer; transition: background .15s, color .15s, border-color .15s; }
.ch-callbtn:hover { background: var(--primary); border-color: var(--primary); color: #fff; }

/* A call, drawn INSIDE the conversation — the same event WhatsApp puts in the thread rather than on a
   page of its own. Centred like a day divider, because it belongs to both people. */
.ch-callev { align-self: center; display: inline-flex; align-items: center; gap: 8px; max-width: 92%;
  margin: 4px auto; padding: 6px 12px; border-radius: var(--radius-pill); background: var(--bg);
  border: 1px solid var(--border); font-size:13px; color: var(--text); }
.ch-callev > span { display: inline-flex; color: var(--text-muted); }
.ch-callev i { font-style: normal; font-size:12px; color: var(--text-muted); }
.ch-callev.missed { background: var(--tint-danger-bg, #fee2e2); border-color: transparent; color: var(--danger); }
.ch-callev.missed > span { color: var(--danger); }
.ch-callback { border: none; background: none; padding: 0 0 0 4px; font: inherit; font-size:12px;
  font-weight:600; color: var(--primary); cursor: pointer; }
.ch-callback:hover { text-decoration: underline; }

/* ─────────────────────────────────────────────────────────────────────────────────────────────
   THE CHAIN — a governed document's steps, drawn as one connected run.

   Every approval surface in this app was showing its trail as a stack of separate boxes: a tick
   here, a clock there, nothing joining them. A reader could see the STEPS and not the FLOW, which
   is the wrong way round — the whole point of a chain is that it goes somewhere, and where it has
   got to is the first thing anybody wants to know.

   So the steps are joined by a spine. The line is solid behind what is done and faded ahead of it,
   which means "how far has this got" is answered by the shape before a single word is read. The
   step being waited on is the only one that is filled in and ringed; everything ahead of it is
   deliberately quiet, because a reader scanning for what to do next should find exactly one thing.

   Used by the gate pass, the outward pass and anything else with a signature trail — one look, so
   two governed documents can never disagree about what "waiting" looks like.
   ───────────────────────────────────────────────────────────────────────────────────────────── */
.ui-chain.is-trail { position: relative; padding: 2px 0; }
.ui-chain.is-trail .ui-chain-step { position: relative; display: flex; gap: 12px; padding: 0 0 16px 0; }
.ui-chain.is-trail .ui-chain-step:last-child { padding-bottom: 0; }

/* The spine. Drawn on each step rather than once behind them all, so it can change colour at the
   point the chain has reached instead of being one flat line the whole way down. */
.ui-chain.is-trail .ui-chain-step::before {
  content: ''; position: absolute; left: 13px; top: 28px; bottom: 0; width: 2px;
  background: var(--border);
}
.ui-chain.is-trail .ui-chain-step:last-child::before { display: none; }
.ui-chain.is-trail .ui-chain-step.done::before { background: var(--success); }
.ui-chain.is-trail .ui-chain-step.rejected::before { background: var(--danger); }

.ui-chain.is-trail .ui-chain-node {
  width: 28px; height: 28px; border-radius: 50%; flex: none; display: grid; place-items: center;
  font-size:12px; font-weight:600; position: relative; z-index: 1;
  background: var(--card); border: 2px solid var(--border); color: var(--text-muted);
}
.ui-chain.is-trail .ui-chain-step.done .ui-chain-node { background: var(--success); border-color: var(--success); color: #fff; }
.ui-chain.is-trail .ui-chain-step.rejected .ui-chain-node { background: var(--danger); border-color: var(--danger); color: #fff; }
.ui-chain.is-trail .ui-chain-step.now .ui-chain-node {
  background: var(--primary); border-color: var(--primary); color: #fff;
  box-shadow: 0 0 0 4px var(--primary-light);
}
.ui-chain.is-trail .ui-chain-step.skipped .ui-chain-node { opacity: .45; }

.ui-chain.is-trail .ui-chain-body { flex: 1; min-width: 0; padding-top: 2px; }
.ui-chain.is-trail .ui-chain-who { font-size:14px; font-weight:600; color: var(--text); }
.ui-chain.is-trail .ui-chain-step.ahead .ui-chain-who { color: var(--text-muted); font-weight: 600; }
.ui-chain.is-trail .ui-chain-what { font-size:12px; color: var(--text-muted); }
.ui-chain.is-trail .ui-chain-meta { font-size:12px; color: var(--text-muted); margin-top: 2px; }
.ui-chain.is-trail .ui-chain-note { font-size:12px; color: var(--text-muted); margin-top: 4px;
                 border-left: 2px solid var(--border); padding-left: 8px; }
.ui-chain.is-trail .ui-chain-step.now .ui-chain-note { border-left-color: var(--primary); }

/* The one step that is somebody's to act on gets the action inline, on the chain, rather than in a
   separate card underneath — the decision belongs at the point it is being asked for. */
.ui-chain.is-trail .ui-chain-act { margin-top: 9px; padding: 12px 12px; border: 1px solid var(--primary);
                background: var(--primary-light); border-radius: var(--radius); }

/* ---- BACK. Every page opened FROM somewhere gets one, and it is the first thing on the page.
   A reader who followed a chain into a record and cannot get out of it stops following chains. */
.ui-crumbs { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:11px; }
.ui-crumb-sep { color:var(--border); }
.ui-crumb-here { color:var(--text-muted); font-size:13px; font-weight:600; }
/* A real control with a real hit area — this was a text link with no padding, which on a phone is a
   12px target you have to aim at. The arrow is an SVG from the icon set, never the character "←". */
.ui-back { display: inline-flex; align-items: center; gap: 6px; line-height: 1.2; min-height: 30px;
           padding: 4px 12px 4px 8px; border: 1px solid var(--border); border-radius: var(--radius-pill);
           background: var(--card); color: var(--primary-dark); text-decoration: none;
           font-size:13px; font-weight: 600;
           transition: background var(--transition), border-color var(--transition); }
.ui-back:hover { background: var(--primary-light); border-color: var(--primary); }
.ui-back > span { display: inline-flex; }

/* The stage timer. Small, on the same line as the step's name, because it is an attribute of that
   step and not a fact of its own — and it turns red rather than growing, so a stall is spotted by
   colour on a page somebody is scanning, not by reading every line. */
.ui-chain-timer { font-size:12px; font-weight: 600; color: var(--text-muted);
                  margin-left: 8px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.ui-chain-timer.waiting { color: var(--warning); }
.ui-chain-timer.waiting.over { color: var(--danger); }

/* ─────────────────────────────────────────────────────────────────────────────────────────────
   THE CLOCK FACE — how long this stage has been waiting, as digits rather than as a sentence.

   "waiting 2h 14m" is read; 02 : 14 under HOUR / MINUTE is SEEN. On a page somebody scans rather
   than reads, that difference decides whether a stalled step gets noticed at all.

   Each digit sits in its own tile so the width never changes as the number does — a timer that
   reflows every time a digit rolls reads as broken. Tabular figures for the same reason.
   ───────────────────────────────────────────────────────────────────────────────────────────── */
/* ★ THE JOB CLOCK (14-10, design A "with indications like D", approved). The digit TILES above are gone: one figure,
   big numbers with the unit a small letter after each (80 d 02 h 28 m), seconds dimmed - read at a distance, the
   shape fixed while it ticks (tabular figures). Its condition is a LAMP beside it, not the digits' colour alone. */
.ui-run { display: inline-flex; align-items: baseline; gap: 6px; font-variant-numeric: tabular-nums; line-height: 1;
          color: var(--text); white-space: nowrap; }
.ui-run-g { display: inline-flex; align-items: baseline; gap: 1px; }
.ui-run-g b { font-size: var(--fs-head); font-weight: 800; letter-spacing: -.01em; }
.ui-run-g i { font-style: normal; font-size: var(--fs-aux); font-weight: 600; color: var(--text-muted); }
.ui-run-g.is-sec b { font-weight: 600; color: var(--text-hint, var(--text-muted)); }
.ui-run.lg { gap: 10px; }
.ui-run.lg .ui-run-g b { font-size: 34px; letter-spacing: -.02em; }
.ui-run.lg .ui-run-g i { font-size: var(--fs-head); }
.ui-run.lg .ui-run-g.is-sec b { font-size: var(--fs-head); }
.ui-run.warn .ui-run-g b { color: var(--warning-dark); }
.ui-run.over .ui-run-g b { color: var(--danger-dark); }
.ui-run.over .ui-run-g.is-sec b, .ui-run.warn .ui-run-g.is-sec b { color: var(--text-hint, var(--text-muted)); }

/* the lamp: a light and its words - ok breathes green, warn holds amber, bad blinks red, off is grey */
.ui-lamp { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-label, 11px); font-weight: 700;
           letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); white-space: nowrap; }
.ui-lamp > i { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--border-strong, #cbd5e1);
               box-shadow: inset 0 -1px 2px rgba(0,0,0,.25); }
.ui-lamp.ok > i { background: var(--success); animation: ui-lamp-breathe 2.4s ease-in-out infinite; }
.ui-lamp.warn { color: var(--warning-dark); }
.ui-lamp.warn > i { background: var(--warning); box-shadow: 0 0 8px color-mix(in srgb, var(--warning) 70%, transparent); }
.ui-lamp.bad { color: var(--danger-dark); }
.ui-lamp.bad > i { background: var(--danger); box-shadow: 0 0 9px color-mix(in srgb, var(--danger) 75%, transparent);
                   animation: ui-lamp-blink 1.2s steps(2, start) infinite; }
@keyframes ui-lamp-breathe { 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--success) 55%, transparent); }
                             60% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--success) 0%, transparent); } }
@keyframes ui-lamp-blink { to { opacity: .3; } }
.ui-lamps { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; }

/* the estimate as a track: green up to the estimate, red stripes past it */
.ui-track { display: flex; height: 10px; border-radius: 999px; background: var(--border); overflow: hidden; }
.ui-track > .in { background: var(--success); height: 100%; transition: width .4s ease; }
.ui-track > .near { background: var(--warning); }
.ui-track > .over { flex: 1; height: 100%;
  background: repeating-linear-gradient(135deg, var(--danger) 0 6px, color-mix(in srgb, var(--danger) 65%, #fff) 6px 12px); }
@media (prefers-reduced-motion: reduce) {
  .ui-lamp.ok > i, .ui-lamp.bad > i { animation: none; }
  .ui-track > .in { transition: none; }
}

/* The waiting stage's clock sits under the step, on its own line — a row of digit tiles beside a
   name would fight the name for the eye, and the point of the face is that it is the thing seen. */
.ui-chain-clock { display: flex; align-items: center; gap: 8px; margin-top: 7px; }
.ui-chain-waiting { font-size:11px; font-weight:600;
 color: var(--text-muted); }

/* A two-way answer where one of the two is always true — on foot or in a vehicle, in or out. Big
   enough to hit on a touchscreen at a barrier, and the chosen one is filled rather than outlined so
   the answer is visible from arm's length. */
.gf-yn { font: inherit; font-size:13px; font-weight: 600; padding: 8px 16px; cursor: pointer;
         border: 1px solid var(--border); background: var(--card); color: var(--text);
         border-radius: var(--radius-sm); transition: background .15s, border-color .15s, color .15s; }
.gf-yn:hover { border-color: var(--primary); }
.gf-yn.on { background: linear-gradient(135deg, #3b82f6, #1d4ed8); border-color: transparent; color: #fff; }

/* The module a DOA task belongs to, beside its name. Tokens only, so it re-themes with the page. */
.ta-mod { display:inline-block; margin-left:7px; padding:0 8px; border:1px solid var(--border);
  border-radius:var(--radius-pill); background:var(--bg); color:var(--text-muted); font-size:11px;
  font-weight:600; line-height:1.6; vertical-align:middle; white-space:nowrap; }


/* ── WHERE IT HAS GOT TO ──────────────────────────────────────────────────────────────────────────
   The approval chain with the step it is sitting on. Same shape as the flow in the handbook, so the
   two read as one thing. An open question is drawn as a STOP, not as progress — a request waiting on
   the raiser is not moving, and colouring it "in progress" would say it was. */
/* `:not(.is-trail)` because the VERTICAL trail above carries .ui-chain too, and this rule sets
   properties that one never sets — display, align-items, flex-wrap — so the two composed instead of
   conflicting and the trail's container was laid out as a horizontal row regardless of specificity.
   Excluding it here is the fix; raising specificity there would not have been. */
.ui-chain:not(.is-trail) { display:flex; align-items:center; flex-wrap:wrap; gap:0; margin:10px 0 8px; }
.ui-chain-step { display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:600; }
.ui-chain-dot { display:inline-flex; align-items:center; justify-content:center; width:20px; height:20px;
  border-radius:var(--radius-pill); font-size:11px; border:1px solid var(--border); background:var(--card);
  color:var(--text-muted); flex:none; }
.ui-chain-step.done .ui-chain-dot { background:var(--success-light); border-color:var(--success); color:var(--success-dark,#059669); }
.ui-chain-step.here .ui-chain-dot { background:var(--primary); border-color:var(--primary); color:#fff; }
.ui-chain-step.asked .ui-chain-dot { background:var(--warning-light); border-color:var(--warning); color:#b45309; }
.ui-chain-step.done .ui-chain-label { color:var(--text-muted); }
.ui-chain-step.todo .ui-chain-label { color:var(--text-muted); font-weight:500; }
.ui-chain-line { flex:1; min-width:14px; height:1px; background:var(--border); margin:0 8px; }
.ui-chain-note { border:1px solid var(--border); border-radius:var(--radius); padding:8px 12px;
  background:var(--bg); font-size:13px; display:flex; flex-direction:column; gap:2px; }
.ui-chain-note b { color:var(--text); }
.ui-chain-note span { color:var(--text-muted); }
.ui-chain-note i { color:var(--text-muted); font-size:12px; }
.ui-chain-note.ok { background:var(--success-light); border-color:var(--success); }
.ui-chain-note.asked { background:var(--warning-light); border-color:var(--warning); }
/* the law refusing at a door (S1): a stop or a hold, said in red - the third tone the note needed */
.ui-chain-note.stopped { background:var(--danger-light); border-color:var(--danger); }

/* ── THE ESTATE AS A 3D WIREFRAME ────────────────────────────────────────────────────────────────
   Dark whatever the app theme is: a wireframe reads as light-on-dark, the glow only works against
   dark, and this panel is stared at rather than read through. The rest of the page still follows the
   user's theme. */
.wv-tools { display:flex; align-items:center; gap:6px; padding:0 0 8px; flex-wrap:wrap; }
.wv-btn { width:28px; height:28px; border:1px solid var(--border); background:var(--card); color:var(--text);
          border-radius:var(--radius-sm); font-size:15px; font-weight:600; line-height:1; cursor:pointer; }
.wv-btn.wide { width:auto; padding:0 12px; font-size:12px; font-weight:600; }
.wv-btn:hover { border-color:var(--primary); color:var(--primary-dark); }
.wv-btn.on { background:var(--primary); border-color:var(--primary); color:#fff; }
.wv-zoom { font-size:12px; color:var(--text-muted); font-variant-numeric:tabular-nums; min-width:44px; }
.wv-hint { font-size:12px; color:var(--text-muted); margin-left:auto; }
/* Which way round the estate is entered — the two questions Vijay asked for, side by side and always
   visible, because switching is the point rather than a setting somebody goes looking for. */
.wv-modes { display:inline-flex; border:1px solid var(--border); border-radius:var(--radius-sm); overflow:hidden; margin-right:6px; }
.wv-mode { border:none; background:var(--card); color:var(--text-muted); font:600 .74rem 'Inter',sans-serif;
           padding:6px 12px; cursor:pointer; }
.wv-mode:hover { color:var(--primary-dark); }
.wv-mode.on { background:linear-gradient(135deg,#3b82f6,#1d4ed8); color:#fff; }

.wv-svg { border-radius:var(--radius); background:#060b17; }
.wv-bg  { fill:#060b17; }
/* The floor. Faint on purpose — it is there to say which way is down, not to be looked at. */
.wv-grid { fill:none; stroke:#16233d; stroke-width:.6; opacity:.55; }
/* The planet's horizon — the one line that says "globe" before anything is read. */
.wv-rim { stroke:#2a4a78; stroke-width:1.4; opacity:.85; filter:url(#wv-glow); }
/* The real globe (globe.gl) — the library owns the canvas inside; the frame is ours. */
.wv-globe { border-radius:var(--radius); overflow:hidden; background:#060b17; height:min(62vh,620px); }
.wv-globe canvas { display:block; }

/* ── the wires ───────────────────────────────────────────────────────────────────────────────────
   A spoke says what is inside what. A LINK is a real column joining two modules, and it is the one
   Vijay asked to have back — "the connecting communication data transfer graphics". */
.wv-path { fill:none; stroke:#2a4a78; opacity:.5; stroke-linecap:round; }
.wv-wire.live .wv-path { stroke:#3b82f6; opacity:.5; }
.wv-wire.cold .wv-path { stroke:#334155; opacity:.38; stroke-dasharray:5 6; }
.wv-wire.gone .wv-path { stroke:#7f1d1d; opacity:.5; stroke-dasharray:2 5; }
/* A real column between two modules: teal, so it never reads as "contains". */
.wv-wire.link .wv-path { stroke:#0e7490; opacity:.55; }
.wv-wire.link.live .wv-path { stroke:#22d3ee; opacity:.5; }
/* The stem back up the path you took. Faint, because it is history rather than the question. */
.wv-wire.faint .wv-path { stroke:#1e3357; opacity:.4; stroke-dasharray:3 6; }
/* ★ THE TRAFFIC. Two dashed overlays running at different speeds — one alone reads as a dashed
   border, two read as movement, which is the whole point: the plant working, not a picture of it. */
.wv-flow  { fill:none; stroke:#7dd3fc; stroke-linecap:round; stroke-dasharray:3 16; opacity:.95;
            animation:wvflow 2.6s linear infinite; filter:url(#wv-glow); }
.wv-wire.link .wv-flow  { stroke:#67e8f9; }
/* A branch line says what is inside what — quieter than a wire between two modules, which is a fact
   about the plant rather than about the drawing. */
.wv-wire.branch .wv-path { stroke:#2a4a78; opacity:.45; }
.wv-wire.branch.live .wv-path { stroke:#3b82f6; opacity:.5; }
/* ★ AND SOMETHING CAME DOWN IT TODAY. Brighter than merely carrying records, because "this is busy
   now" and "this holds a lot" are different facts and the eye should be able to tell them apart. */
.wv-wire.today .wv-path { stroke:#60a5fa; opacity:.72; }
.wv-wire.link.today .wv-path { stroke:#22d3ee; opacity:.7; }
.wv-wire.today .wv-flow { opacity:1; stroke:#bae6fd; }
.wv-wire.today .wv-packet { r:3; }
@keyframes wvflow { from { stroke-dashoffset:220; } to { stroke-dashoffset:0; } }
.wv-wire:hover .wv-path { opacity:1; stroke:#93c5fd; stroke-width:3; }

/* ── one thing, standing on the floor ────────────────────────────────────────────────────────────
   A hexagonal pad projected in the ground plane, a post, and the thing's OWN icon on top. The pad is
   FILLED — SVG hit-testing is per shape, so an unfilled outline is only clickable on its lines and
   the middle of every node was a hole clicks fell through. */
.wv-pad { stroke-linejoin:round; filter:url(#wv-glow); }
.wv-post { fill:none; stroke-width:1.1; opacity:.5; }
.wv-badge { fill:#060b17; fill-opacity:.85; stroke-width:1.1; opacity:.9; }
.wv-ico { opacity:.98; }
.wv-drop { fill:none; stroke:#1e3357; stroke-width:.8; stroke-dasharray:2 4; opacity:.7; }
.wv-shadow { fill:#0b1b33; opacity:.55; }
.wv-halo { fill:url(#wv-halo-g); }
/* ★ SCENERY DOES NOT TAKE CLICKS. Every one of these is an SVG shape, and an SVG shape is a hit
   target — a glow, a floor line or a travelling dash sitting over a pad is invisible to the eye and
   solid to the pointer, which is precisely the kind of thing that makes a page feel broken for no
   visible reason. */
.wv-halo, .wv-shadow, .wv-drop, .wv-grid, .wv-flow, .wv-more, .wv-post { pointer-events:none; }
.wv-node { transition:opacity .12s; }
.wv-node:hover .wv-pad { stroke-width:2.6; fill-opacity:.34; }
.wv-node:hover .wv-badge { fill-opacity:1; stroke-width:2; }
.wv-node.core .wv-pad { stroke-width:2.4; }
/* Something came in here today — the pad pulses, so a live corner of the plant is visible before any
   number is read. */
.wv-node.fresh .wv-pad { animation:wvpulse 2.4s ease-in-out infinite; }
@keyframes wvpulse { 0%,100% { fill-opacity:.13; } 50% { fill-opacity:.34; } }
/* Where you came from: still there, still clickable, plainly not the question any more. */
.wv-node.dim { opacity:.45; }
.wv-node.dim:hover { opacity:.9; }
/* Nothing has ever been recorded here. Dimmed and dashed, never hidden — an idle department is the
   most useful thing on a view like this, and hiding it is how it stays idle. */
.wv-node.dead .wv-pad { stroke-dasharray:3 4; fill-opacity:.05; }
.wv-node.dead .wv-badge { opacity:.5; }
.wv-node.hot .wv-pad { stroke-width:2.4; }
/* ★ A THING WITH MORE INSIDE IT SAYS SO — a second hex just outside its own, like a stack of paper.
   Without it there is no telling a department that holds nothing from one nobody has opened yet, and
   the whole tree becomes trial and error. */
.wv-more { fill:none; opacity:.35; stroke-width:1; stroke-dasharray:5 5; }
/* A name that had to step aside keeps a hairline back to the thing it names, so it is never in doubt
   which symbol it belongs to. */
.wv-leader { fill:none; stroke:#334155; stroke-width:.7; opacity:.55; pointer-events:none; }
.wv-node:hover .wv-more { opacity:.8; }
/* What you last chose stays marked, so the panel beside the map and the map agree. */
.wv-node.sel .wv-pad { stroke-width:2.8; }
.wv-node.sel .wv-badge { fill-opacity:1; }
/* ★ THE PACKETS. `linkDirectionalParticles` from 3d-force-graph: discrete things travelling the path,
   which reads as traffic in a way a dashed border never does. */
.wv-packet { fill:#e0f2fe; filter:url(#wv-glow); pointer-events:none; }
.wv-wire.link .wv-packet { fill:#a5f3fc; }

.wv-lbl { fill:#dbe4f0; font-family:'Inter',sans-serif; font-weight:600; letter-spacing:.05em;
          paint-order:stroke; stroke:#060b17; stroke-width:3.5px; }
.wv-val { font-family:'Inter',sans-serif; font-weight:600;
          paint-order:stroke; stroke:#060b17; stroke-width:3.5px; }
.wv-today { fill:#34d399; font-weight:600; }
.wv-tbl { font:500 8px 'Inter',sans-serif; fill:#64748b; opacity:0; }

/* What is legible depends on how close you are, so that is what decides what is drawn. In CSS, so a
   wheel turn costs no extra work beyond the re-projection. */
[data-band="mid"]  .wv-tbl { opacity:.7; }
[data-band="near"] .wv-tbl { opacity:.9; }

/* Full screen: the panel becomes the monitor. */
.wv-fs { background:#060b17; padding:12px 16px; }
.wv-fs .wv-svg { height:calc((100vh - 96px)/var(--ui-zoom,1)) !important; }
/* ★ AND THE GLOBE TOO. Only `.wv-svg` was given a full-screen height, and site mode does not draw an
   SVG — it mounts a canvas — so going full screen left the earth in its small box with the rest of
   the screen black around it: "it shows only half". */
.wv-fs .wv-globe { height:calc((100vh - 96px)/var(--ui-zoom,1)) !important; }
/* ★ `.wv-fs .wv-fs .wv-btn` — a selector that needs a full screen INSIDE a full screen, so it never
   matched anything. The buttons kept `background:var(--card)` (white) on the #060b17 backdrop and
   read as loose pills floating over the page: "they feel out side of ui". */
.wv-fs .wv-hint, .wv-fs .wv-btn, .wv-fs .wv-mode { background:#0d1729; border-color:#1c2c4a; color:var(--border-strong, #cbd5e1); }
.wv-fs .wv-mode.on { background:linear-gradient(135deg,#3b82f6,#1d4ed8); border-color:#3b82f6; color:#fff; }
.wv-fs .wv-btn:hover { border-color:#3b82f6; color:#93c5fd; }
.wv-fs .wv-modes { border-color:#1c2c4a; background:#0d1729; }
/* The toolbar is a BAR, not a row of loose buttons on a black field. */
.wv-fs .wv-tools { background:#0a1220; border:1px solid #17243c; border-radius:var(--radius);
                   padding:8px 8px; margin-bottom:10px; }
.wv-fs .wv-zoom { color:#94a3b8; }

@media (prefers-reduced-motion: reduce) {
  .wv-flow { animation:none; stroke-dasharray:none; opacity:.45; }
  .wv-node.fresh .wv-pad { animation:none; }
  .wv-packet { display:none; }
}

/* the unit tree beside the map */
.lv-scopebar { margin-left:10px; }
.lv-chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:600;
           background:var(--primary-light); color:var(--primary-dark); border-radius:var(--radius-sm); padding:2px 8px; }
.lv-chip.plain { background:var(--bg); color:var(--text-muted); font-weight:500; }
.lv-chip b { cursor:pointer; font-size:14px; line-height:1; opacity:.7; }
.lv-chip b:hover { opacity:1; }
.lv-none { font-size:13px; color:var(--text-muted); padding:8px 0; }

/* what each unit made */
.lv-prow { display:flex; align-items:center; gap:12px; padding:6px 0; border-bottom:1px solid var(--border); }
.lv-prow:last-child { border-bottom:none; }
.lv-prow.quiet { opacity:.62; }
.lv-prow .u { flex:1; min-width:0; font-size:13px; font-weight:600; color:var(--text); letter-spacing:.04em; }
.lv-prow .u i { display:block; font-style:normal; font-size:12px; font-weight:400; color:var(--text-muted);
                overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lv-prow .v { flex:none; width:74px; text-align:right; }
.lv-prow .v b { display:block; font-size:14px; font-weight:600; color:var(--text); font-variant-numeric:tabular-nums; }
.lv-prow .v i { font-style:normal; font-size:11px; color:var(--text-muted); }
.lv-prow .f { flex:none; width:112px; text-align:right; }
.lv-flag { font-size:11px; font-weight:600; border-radius:var(--radius-sm); padding:2px 6px; }
.lv-flag.never { background:var(--tint-warn-bg); color:var(--tint-warn-fg); }
.lv-flag.stale { background:var(--warning-light); color:var(--tint-warn-fg); }
.lv-flag.idle  { background:var(--bg); color:var(--text-muted); }

/* ── the drill-down ──────────────────────────────────────────────────────────────────────────────
   It IS the 3D scene: the crumb line is a read-out of where the camera is standing, not a second
   navigation beside it. */
.lv-crumbs { display:flex; align-items:center; gap:6px; flex-wrap:wrap; font-size:12px; padding-bottom:8px; }
.lv-crumbs .cr { color:var(--primary-dark); cursor:pointer; font-weight:600; }
.lv-crumbs .cr:hover { text-decoration:underline; }
.lv-crumbs .cr.plain { color:var(--text-muted); cursor:default; font-weight:500; }
.lv-crumbs .cr.plain:hover { text-decoration:none; }
.lv-crumbs .sep { color:var(--text-muted); }
/* the bars inside an opened module — on both pages, so the console and the wall read the same */
.lv-bar { display:flex; align-items:center; gap:8px; padding:2px 0; font-size:13px; }
.lv-bar .lab { flex:none; width:150px; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lv-bar .track { flex:1; height:7px; background:var(--bg); border-radius:var(--radius-sm); overflow:hidden; }
.lv-bar .track i { display:block; height:100%; background:linear-gradient(90deg,#3b82f6,#1d4ed8); }
.lv-bar .n { flex:none; width:52px; text-align:right; font-weight:600; font-variant-numeric:tabular-nums;
             color:var(--text-muted); font-size:12px; }
.lv-mdet { font-size:13px; }
.lv-mhead { display:flex; align-items:baseline; gap:8px; padding-bottom:8px; }
.lv-mhead b { font-size:15px; color:var(--text); }
.lv-mhead span { font-size:12px; color:var(--text-muted); }
.lv-mstats { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin-bottom:10px; }
.lv-mstats div { background:var(--bg); border-radius:var(--radius-sm); padding:8px 12px; }
.lv-mstats b { display:block; font-size:21px; font-weight:600; color:var(--text); font-variant-numeric:tabular-nums; }
.lv-mstats span { font-size:11px; color:var(--text-muted); }
.lv-msec { font-size:11px; color:var(--text-muted);
           border-top:1px solid var(--border); padding:8px 0 4px; margin-top:4px; }
.lv-mrow { display:flex; align-items:baseline; gap:8px; padding:4px 0; border-bottom:1px solid var(--border); font-size:13px; }
.lv-mrow:last-child { border-bottom:none; }
.lv-mrow .t { flex:none; width:44px; color:var(--text-muted); font-size:11px; }
.lv-mrow .r { flex:none; width:74px; text-align:right; font-weight:600; font-variant-numeric:tabular-nums; color:var(--text); }
.lv-mrow .l { flex:1; min-width:0; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lv-mrow .w { flex:none; width:96px; text-align:right; color:var(--text-muted); font-size:12px; }

/* ── THIN SCROLLBAR, ANYWHERE ────────────────────────────────────────────────────────────────────
   The sidebar has had one of these since it was built; every other scrolling box in the product got
   the operating system's default, which on a narrow popover is a bar almost as wide as the text it
   sits beside. Add `thin-scroll` to any element that scrolls.

   ★ THE SAME TRAP AS THE SIDEBAR'S, and it is worth repeating rather than rediscovering: CHROME
   IGNORES ::-webkit-scrollbar ENTIRELY WHEN THE STANDARD `scrollbar-width` IS ALSO SET, and falls
   back to its own ~11px "thin" bar. So `scrollbar-width` is given ONLY to Firefox, which has no
   ::-webkit-scrollbar selector, through @supports.

   Unlike the sidebar's, this thumb is visible at rest. A menu you must hover to discover is
   scrollable is a menu whose bottom half nobody finds. */
.thin-scroll::-webkit-scrollbar { width: 5px; height: 5px; }
.thin-scroll::-webkit-scrollbar-track { background: transparent; }
.thin-scroll::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text) 18%, transparent); border-radius: var(--radius-pill);
}
.thin-scroll:hover::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--text) 32%, transparent); }
.thin-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--text) 45%, transparent); }
@supports not (selector(::-webkit-scrollbar)) {
  .thin-scroll { scrollbar-width: thin;
                 scrollbar-color: color-mix(in srgb, var(--text) 18%, transparent) transparent; }
  .thin-scroll:hover { scrollbar-color: color-mix(in srgb, var(--text) 32%, transparent) transparent; }
}

/* ── RING WHOEVER A CHAIN STEP IS WAITING ON ──────────────────────────────────────────────────────
   Vijay: "Give Call Button on every approval step, so that they communicate and complete the chain."
   It sits inside a step's meta line, so it reads as a small affordance and never competes with the
   step's own primary action. */
.gp-callbtn { display:inline-flex; align-items:center; gap:4px; margin-left:8px; padding:2px 8px;
              border:1px solid var(--primary); background:var(--primary-light);
              color:var(--primary-dark); border-radius:var(--radius-lg); font-size:11px; font-weight:600;
              cursor:pointer; vertical-align:middle; line-height:1.6; }
.gp-callbtn:hover { background:var(--primary); color:#fff; }
.gp-callbtn > span, .gp-callbtn svg { width:13px; height:13px; }
/* On the amber "waiting on …" banner the primary-light fill disappears into the background, so the
   button there carries the banner's own amber and reads as part of the sentence it sits in. */
.gp-callbtn.gp-callbtn-warn { border-color:#b45309; background:#fff; color:#b45309; }
.gp-callbtn.gp-callbtn-warn:hover { background:#b45309; color:#fff; }

/* ── ★★ THE LIVE-CALL DOCK ────────────────────────────────────────────────────────────────────────
   A call runs in its own window now, so every OTHER page needs to say so. Top right: the messenger
   and help bubbles own the bottom right, and a dock that covered them would take two things away to
   give one.
   ★ Every colour is a token. A dock hardcoded to #3b82f6 would be the one element on the page that
   ignored the user's chosen theme, and it is the element that is on EVERY page. */
.cs-dock {
  position: fixed; right: 20px; top: 74px; z-index: 2900; width: 268px;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--el-3);
  overflow: hidden; animation: cs-dock-in .18s ease-out;
}
@keyframes cs-dock-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .cs-dock { animation: none; } }
.cs-dock-bar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; }
.cs-dock-ic { display: inline-flex; width: 30px; height: 30px; flex: 0 0 30px; border-radius: 50%;
              align-items: center; justify-content: center;
              background: var(--success-light); color: var(--success-dark, #059669); }
.cs-dock-ic > span, .cs-dock-ic svg { width: 16px; height: 16px; }
/* Muted is a state somebody must be able to see without reading — the ring goes amber. */
.cs-dock.muted .cs-dock-ic { background: var(--warning-light); color: #b45309; }
.cs-dock-txt { min-width: 0; flex: 1; }
.cs-dock-txt b { display: block; font-size:13px; color: var(--text);
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cs-dock-txt span { display: block; font-size:11px; color: var(--text-muted);
                    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cs-dock-time { font-size:12px; font-weight:600; color: var(--text);
                font-variant-numeric: tabular-nums; }
.cs-dock-acts { display: flex; border-top: 1px solid var(--border); }
.cs-dock-btn { flex: 1; padding: 8px 4px; border: 0; background: transparent; cursor: pointer;
               font-size:12px; font-weight:600; color: var(--primary-dark);
               font-family: inherit; }
.cs-dock-btn + .cs-dock-btn { border-left: 1px solid var(--border); }
.cs-dock-btn:hover { background: var(--primary-light); }
.cs-dock-btn.danger { color: var(--danger); }
.cs-dock-btn.danger:hover { background: var(--danger); color: #fff; }
@media (max-width: 640px) { .cs-dock { right: 10px; left: 10px; width: auto; top: 66px; } }

/* ── ★★ THE WEIGHMENT SCREEN — from the approved preview (/dev/weighbridge-preview · 2 · Weighment)
   The live page had this as a MODAL with two buttons. The design is a page: the paperwork typed on
   the left, the instruments read on the right, so the operator never scrolls away from the live view
   to finish a field and the weight is taken while the vehicle is still visibly on the platform. */
/* ★ The rail's width is a VARIABLE, set from the camera count — the approved preview's own contract.
   Hardcoding the column here meant the wall could grow and the rail could not follow it. */
.wb-2col { display:grid; grid-template-columns:minmax(0,1fr) var(--rail,420px); gap:16px; align-items:start; }
/* The form column is the one that gives — the instruments do not shrink to fit a wider form. */
.wb-2col > :first-child { min-width:0; }
.wb-rail { position:sticky; top:64px; }
@media (max-width:1100px){ .wb-2col { grid-template-columns:1fr; } .wb-rail { position:static; } }

/* Two answers, two big targets. It is the FIRST thing decided and everything below changes with it,
   so the one not chosen drops back to almost nothing — a gloved hand should never be in doubt. */
.wb-dirpick { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.wb-dirbtn { display:flex; align-items:center; gap:12px; text-align:left; cursor:pointer;
             padding:16px 16px; border-radius:var(--radius); border:2px solid var(--border);
             background:var(--card); color:var(--text-muted); font-family:inherit;
             transition:border-color .15s ease, background .15s ease, color .15s ease, opacity .15s ease; }
.wb-dirbtn span { display:flex; flex-direction:column; line-height:1.25; }
.wb-dirbtn b { font-size:17px; font-weight:600; }
.wb-dirbtn small { font-size:12px; opacity:.9; }
.wb-dirbtn:not(.on) { opacity:.5; }
.wb-dirbtn:not(.on):hover { opacity:.8; border-color:var(--text-muted); }
.wb-dirbtn.in.on { border-color:var(--success); background:var(--success-light); color:#047857; }
.wb-dirbtn.out.on { border-color:var(--primary); background:var(--primary-light); color:var(--primary-dark); }
@media (max-width:520px){ .wb-dirpick { grid-template-columns:1fr; } }

/* The scale reading — the one number the operator is looking for. */
/* ★ THE LIVE WEIGHT IS INK ON THE DARK RAIL, NOT A HAIRLINE. This read `color:#e2e8f0` and a
   sweep of mine turned it into `var(--border, #e2e8f0)` because that hex happened to equal
   --border's default. The VALUE matched; the MEANING did not. --border is the hairline token
   and it goes dark on any theme that lights the rail, so the weighbridge's live reading fell to
   1.18:1 on white, 1.22 on flat, 1.23 on console-day and 1.27 on console — invisible, on the one
   number a weighbridge operator is watching. --sidebar-active is the token that means
   "legible against the rail" and every theme that repaints the rail declares it. */
.wb-scale2 { background:var(--sidebar-bg); color:var(--sidebar-active, #e2e8f0); border-radius:var(--radius); padding:16px 16px;
             display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
.wb-scale2 .rd { font-size:38px; font-weight:600; font-variant-numeric:tabular-nums; letter-spacing:.01em; }
.wb-scale2 .rl { font-size:11px; opacity:.7; }
.wb-livelbl { display:inline-flex; align-items:center; gap:6px; font-size:11px; opacity:.85; }
.wb-dot { width:7px; height:7px; border-radius:50%; background:var(--success, #4ade80); }
.wb-dot.off { background:#94a3b8; }

/* ★ A frame that came back LATE is not the same evidence as one taken with the weight — amber says
   so without pretending it is missing, and red is a camera that never answered at all. */
.wb-shots { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; }
.wb-shot { aspect-ratio:4/3; border:1px dashed var(--border); border-radius:var(--radius-sm); background:var(--bg);
           display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px;
           color:var(--text-muted); font-size:12px; text-align:center; padding:6px; overflow:hidden; }
.wb-shot.has { border-style:solid; border-color:var(--success); background:var(--success-light); color:#047857; }
.wb-shot.late { border-style:solid; border-color:var(--warning); background:var(--warning-light); color:#b45309; }
.wb-shot.bad { border-style:solid; border-color:var(--danger); background:var(--danger-light, #fef2f2); color:#b91c1c; }
.wb-shot img { width:100%; height:100%; object-fit:cover; border-radius:var(--radius-sm); }

/* A row the operator has CHOSEN must look chosen — the selection is the whole point of the list. */
.wb-pick { border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--card);
           padding:12px 12px; margin-bottom:8px; cursor:pointer; display:flex; align-items:center; gap:12px;
           transition:border-color .13s, background .13s, box-shadow .13s; }
.wb-pick:hover { border-color:var(--primary); }
.wb-pick.sel { border-color:var(--primary); background:var(--primary-light); box-shadow:0 0 0 1px var(--primary) inset; }
.wb-pick-main { flex:1 1 auto; min-width:0; }
.wb-pick-t { font-weight:600; font-size:14px; }
.wb-pick.sel .wb-pick-t { color:var(--primary-dark); }
.wb-pick-s { font-size:12px; color:var(--text-muted); margin-top:2px; }
/* ★ The tick is the selection, and it is drawn even when empty so the row reads as choosable. The
   first cut of this invented a second radio-button class beside .wb-pick — which the approved
   preview had already defined — and the two fought: the older rule's align-items:center made every
   row shrink to its own text, so three orders rendered as three different widths. */
.wb-pick-tick { width:20px; height:20px; border-radius:50%; border:2px solid var(--border);
                flex:none; display:flex; align-items:center; justify-content:center; color:transparent; }
.wb-pick.sel .wb-pick-tick { border-color:var(--primary); background:var(--primary); color:#fff; }
.wb-none { padding:16px; text-align:center; color:var(--text-muted); font-size:13px;
           border:1px dashed var(--border); border-radius:var(--radius-sm); }
.wb-hint2 { font-size:12px; color:var(--text-muted); line-height:1.5; margin-bottom:8px; }

/* ── ★★ THE CAMERA WALL ON THE WEIGHMENT SCREEN (approved preview · 2 · Weighment) ───────────────
   I built this as ONE tile saying "No frames yet". The design is a tile PER CAMERA on the bridge,
   laid out in a grid whose shape follows the count — so a plant with three cameras is never shown a
   fourth empty box, and each tile is a live monitor before the weight and the frame afterwards. */
.wb-frame { position:relative; aspect-ratio:16/9; border-radius:var(--radius-sm); overflow:hidden;
            background:
              repeating-linear-gradient(0deg, rgba(255,255,255,.045) 0 1px, transparent 1px 34px),
              repeating-linear-gradient(90deg, rgba(255,255,255,.045) 0 1px, transparent 1px 34px),
              linear-gradient(160deg,#334155,#0f172a 70%);
            border:1px solid var(--border); }
/* A dead camera is still a DARK camera. A white sheet reads as a blank page rather than as a frame
   that never arrived, and at full size it is blinding. */
.wb-frame.miss { background:linear-gradient(160deg,#4c1d1d,#1a0808 70%); border-color:var(--danger); }
.wb-frame.miss .mid { color:#fca5a5; }
/* Nothing pulled yet — an empty SLOT, not a dimmed picture. A greyed dark frame reads as a camera
   that is on and showing nothing, which is a different and much worse thing. */
.wb-frame.idle { background:var(--bg); border-style:dashed; }
.wb-frame.idle .mid { color:var(--text-muted); }
.wb-frame .mid { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center;
                 justify-content:center; gap:4px; color:#cbd5e1; font-size:11px; }
.wb-frame img { width:100%; height:100%; object-fit:cover; }
.wb-frame .lbl, .wb-frame .stamp { position:absolute; font-size:11px; font-weight:600;
            letter-spacing:.03em; color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.7);
            font-variant-numeric:tabular-nums; }
/* On a 130px rail tile "CAM 4 · DRIVER CABIN" runs off the edge — bounded and clipped rather than
   allowed to spill over the picture. */
.wb-frame .lbl { top:7px; left:8px; right:8px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.wb-frame .stamp { bottom:7px; right:8px; font-weight:600; opacity:.9; display:flex; align-items:center; gap:4px; }
.wb-fr-tag { position:absolute; bottom:6px; left:8px; font-size:9px; font-weight:600;
             padding:2px 6px; border-radius:var(--radius-sm); letter-spacing:.03em; }
.wb-fr-tag.late { background:var(--warning); color:#fff; }
/* A thumbnail on a rail is too small to see a number plate in — it is a way IN, not the evidence
   itself. So every tile opens. */
.wb-frame.tap { cursor:pointer; transition:box-shadow .15s ease, transform .15s ease; }
.wb-frame.tap:hover { box-shadow:0 0 0 2px var(--primary); transform:translateY(-1px); }
.wb-frame.tap:focus-visible { outline:none; box-shadow:0 0 0 3px var(--primary-light),0 0 0 5px var(--primary); }
/* A picture viewer needs the room dark. The app's 50% scrim is right for a form on top of a page;
   here the page must go away, or white caption text lands on white cards behind it. */
#wm-viewer { position:fixed; inset:0; z-index:3200; background:rgba(2,6,23,.92);
             display:flex; align-items:center; justify-content:center; padding:24px; }
#wm-viewer .vw { max-width:1000px; width:100%; }
#wm-viewer .vw .wb-frame { aspect-ratio:16/9; }
#wm-viewer .cap { color:#e2e8f0; font-size:13px; display:flex; align-items:center;
                  justify-content:space-between; gap:12px; margin-top:10px; flex-wrap:wrap; }
#wm-viewer .cap button { background:transparent; border:1px solid #475569; color:var(--border, #e2e8f0);
                         border-radius:var(--radius-sm); padding:4px 12px; font-family:inherit; cursor:pointer; }

/* ★ A DISABLED BUTTON MUST LOOK DISABLED. `.btn-primary` carries a gradient, which overrides any
   background a disabled rule sets — so a button the app had switched OFF still read as the loudest
   thing on the panel, and an operator pressing it got nothing with no explanation. Seen first on the
   weighment screen, where "Close Slip And Print" is deliberately dead until both weights are in. */
.btn:disabled, .btn[disabled] { opacity:.5; cursor:not-allowed; box-shadow:none; transform:none; }
.btn:disabled:hover, .btn[disabled]:hover { box-shadow:none; transform:none; }




/* ── ★★ THE APP'S ONE LOADING STATE ────────────────────────────────────────────────────────────
   Every request in this product goes through apiFetch, so the waiting is shown from there rather
   than by writing a skeleton into each of the eighty-six pages that build themselves from it. Two
   pixels across the top of the window, and the control that started it turns while it waits.
   ★ It is deliberately NOT a spinner over the page: the operator can keep reading what is already
   on screen, which on a weighbridge or a gate is usually the thing they are being asked about. */
#app-progress { position:fixed; top:0; left:0; height:2px; width:0; z-index:100000;
  background:linear-gradient(90deg,var(--primary),var(--primary-dark)); opacity:0;
  pointer-events:none; transition:width .25s ease, opacity .2s ease; }
#app-progress.on   { opacity:1; width:75%; transition:width 3.2s cubic-bezier(.05,.8,.2,1), opacity .12s; }
#app-progress.done { width:100%; opacity:0; transition:width .16s ease, opacity .26s ease .1s; }

/* The control that asked keeps its shape and its label — only the icon turns, so nothing jumps. */
.is-busy { position:relative; cursor:progress !important; }
.is-busy > span > svg, .is-busy svg { animation:app-spin .7s linear infinite; transform-origin:50% 50%; }
.is-busy:disabled, .is-busy[disabled] { opacity:.75; }
@keyframes app-spin { to { transform:rotate(360deg); } }

/* A page that asks for it can put the same turn on any icon: <i class="ti ti-refresh spin"> */
.spin, .spin > span > svg, .spin svg { animation:app-spin .7s linear infinite; }

@media (prefers-reduced-motion: reduce) {
  #app-progress { transition:opacity .1s; }
  #app-progress.on { width:75%; transition:opacity .1s; }
  .is-busy svg, .is-busy > span > svg, .spin svg, .spin > span > svg { animation:none; }
}


/* ── The review before a weighing is recorded (approved preview · 2 · Weighment) ────────────────
   Copied from the design rather than approximated: a cell that failed is outlined in danger and a
   cell taken again after the weight is outlined in warning, so the two are told apart at a glance
   from across a weighbridge cabin. */
#wm-review .modal-dialog { max-width: 880px; }
.wb-rev-grid { display:grid; gap:8px; }
.wb-rev-cell { border:1px solid var(--border); border-radius:var(--radius-sm); padding:8px; background:var(--bg); }
.wb-rev-cell.late { border-color:var(--warning); background:var(--warning-light); }
.wb-rev-cell.miss { border-color:var(--danger); background:var(--danger-light, #fef2f2); }
.wb-rev-cap { display:flex; align-items:center; justify-content:space-between; gap:8px;
              margin-top:7px; font-size:12px; font-weight:600; }

/* ============================================================================
   WORK ORDERS — THE REGISTER LAYOUT
   ----------------------------------------------------------------------------
   ★ EVERY RULE HERE IS SCOPED UNDER #wo-root, ON PURPOSE.
   These same component classes (.ox-row, .ox-rowmain, .ox-rowright, .ox-b) are
   rendered by 21 other views, and an unscoped change to them was measured across
   12 pages / 6 page shapes: it flattened the header band on Gate, Weighbridge,
   Recruitment and Expatriate, and it turned Muster's STATE column into an outline
   chip — i.e. demoted the one fact that page exists to show. So the register
   layout lives here, behind one id, and is rolled out to another module only
   after that module has been photographed before and after.

   The list stops being a stack of cards and becomes a column grid: the heading
   strip and the row read the SAME widths from the custom properties below, so a
   heading cannot drift off its values (measured 0px on all four columns).
   ========================================================================== */
#wo-root {
  --wo-ref: 104px;      /* the work-order number, in tabular mono */
  --wo-prio: 82px;      /* priority as a word, never colour alone */
  --wo-stat: 106px;     /* status as a lamp + word */
  --wo-gap: 14px;
}

/* ---- the heading strip: same padding and gap as a row, so columns cannot drift -- */
#wo-root .wo-colhead {
  display: flex; align-items: center; gap: var(--wo-gap);
  padding: 8px 16px; border-bottom: 1px solid var(--border);
  background: var(--bg);
  font-size:11px; font-weight:600;
  color: var(--text-hint, var(--text-muted));
}
#wo-root .wo-colhead .h-pad  { flex: none; width: 16px; }         /* over the checkbox */
#wo-root .wo-colhead .h-ic   { flex: none; width: 38px; }         /* over the type icon */
#wo-root .wo-colhead .h-ref  { flex: none; width: var(--wo-ref); }
#wo-root .wo-colhead .h-main { flex: 1; min-width: 0; }
#wo-root .wo-colhead .h-prio { flex: none; width: var(--wo-prio); }
#wo-root .wo-colhead .h-stat { flex: none; width: var(--wo-stat); }
#wo-root .wo-colhead .h-open { flex: none; width: 36px; }

/* ---- the row: 93px of stacked card becomes a 52px line ---- */
#wo-root .ox-row { gap: var(--wo-gap); padding: 8px 16px; position: relative; }
#wo-root .ox-row > input[type=checkbox] { margin: 0; flex: none; }

/* the severity rail — priority read before anything is read */
#wo-root .ox-row::before {
  content: ''; position: absolute; left: 0; top: 3px; bottom: 4px; width: 4px;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0; background: color-mix(in srgb, var(--text-muted) 18%, transparent);
}
#wo-root .ox-row[data-prio="critical"]::before { background: var(--danger); }
#wo-root .ox-row[data-prio="high"]::before     { background: var(--tint-hot-dot); }
#wo-root .ox-row[data-prio="medium"]::before   { background: var(--warning); }
#wo-root .ox-row[data-prio="critical"] { background: color-mix(in srgb, var(--danger) 4%, transparent); }

/* the work-order number: an identifier is read character by character */
#wo-root .wo-ref {
  flex: none; width: var(--wo-ref);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size:11px; color: var(--text-muted); white-space: nowrap;
}
/* the job itself takes the slack; the meta line ellipsises rather than wrapping */
#wo-root .ox-rowmain { display: flex; align-items: center; gap: 8px; }
#wo-root .ox-rowmain > .wo-lines { flex: 1; min-width: 0; }
#wo-root .ox-rowtop  { gap: 8px; flex-wrap: nowrap; min-width: 0; }
#wo-root .ox-rowtop b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#wo-root .ox-rowmeta { margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* flags sit at the right edge of the job column, so they form their own line */
#wo-root .ox-badges { margin: 0 0 0 auto; flex: none; gap: 4px; }

/* priority: a word in the ramp colour, so it survives a colour-blind reader */
#wo-root .wo-prio {
  flex: none; width: var(--wo-prio);
  /* ★ capitalize, NOT uppercase. This cell prints a raw data value — `critical`, `high`,
     `medium` — so the CSS had been doing the capitalising the data should carry. Stripping
     uppercase in the label pass turned it into "critical", breaking the Title Case rule.
     `capitalize` is the honest fix at this layer: the value keeps its own case elsewhere
     (filters, exports, the API) and reads correctly here. The same applied to five more rules
     that dressed values rather than labels — badges, severity chips and the stat eyebrow. */
  text-transform: capitalize;
  font-size:11px; font-weight:600;
  color: color-mix(in srgb, var(--text-muted) 76%, transparent);
}
#wo-root .wo-prio[data-p="critical"] { color: var(--danger-dark); }   /* the ink token: --danger measured 3.6:1 as 11px bold on white */
#wo-root .wo-prio[data-p="high"]     { color: var(--tint-hot-fg); }
#wo-root .wo-prio[data-p="medium"]   { color: var(--tint-warn-fg); }

/* status: a lamp and a word, not a third filled pill competing with the flags */
#wo-root .wo-stat {
  flex: none; width: var(--wo-stat); display: inline-flex; align-items: center; gap: 6px;
  font-size:12px; font-weight: 600; color: var(--text);
}
#wo-root .wo-stat i { font-style: normal; width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--text-muted); }
#wo-root .wo-stat[data-s="in_progress"] { color: var(--primary-dark); }
#wo-root .wo-stat[data-s="in_progress"] i { background: var(--primary); }
#wo-root .wo-stat[data-s="on_hold"] i { background: var(--warning); }

/* a child job indents INSIDE the job column — never in the row's padding, which
   would push every column right of it out of alignment with its heading */
#wo-root .ox-row.ox-row-kid { padding-left: 16px; background: color-mix(in srgb, var(--primary) 3%, transparent); }
#wo-root .ox-row.ox-row-kid .wo-lines { padding-left: 20px; }

/* the open control is what clicking the row already does — it waits to be wanted */
#wo-root .ox-row .ox-icobtn { opacity: 0; transition: opacity .12s; }
#wo-root .ox-row:hover .ox-icobtn,
#wo-root .ox-row .ox-icobtn:focus-visible { opacity: 1; }
@media (hover: none) { #wo-root .ox-row .ox-icobtn { opacity: 1; } }

/* ---- the summary rail: one strip, tinted by MEANING not by field ----
   A zero in Overdue is not an alarm. Tone follows the number: nothing overdue
   reads calm, and only a real count turns it warn/bad. */
#wo-root .wo-calm  { color: var(--tint-ok-fg); }
#wo-root .wo-calm-note { color: var(--tint-ok-fg); font-weight: 600; }

@media (max-width: 1100px) {
  /* below this the four fixed columns crowd; fall back to the stacked card */
  #wo-root .wo-colhead { display: none; }
  #wo-root .ox-row { flex-wrap: wrap; padding: 12px 16px; }
  #wo-root .wo-ref, #wo-root .wo-prio, #wo-root .wo-stat { width: auto; }
  #wo-root .ox-rowmain { flex-basis: 100%; }
}

/* ---- the chrome above the list, still inside #wo-root ----
   The page stacked four bands before a single work order. These do NOT go global: the same rules
   measured across 12 pages flattened the header zone on Gate, Weighbridge, Recruitment and
   Expatriate, where the wash is what groups the title, actions and tabs into one block. */

/* the wash is removed HERE, where the stat cards sit on top of its yellow lobe and come out cream */
#wo-root { --wo-unwash: 1; }
.content-area:has(#wo-root) { background-image: none; }

/* tabs sized to their words, not stretched edge to edge with a filled slab */
#wo-root .ox-tabs2 { grid-auto-columns: max-content; justify-content: start; padding: 0 8px; }
#wo-root .ox-tabs2 button { padding: 12px 16px; font-size:13px; }
#wo-root .ox-tabs2 button.on { background: transparent; }

/* the search band: 44px of control inside 18px of card padding is a slab for one input.
   ★ The Search and Filters buttons carry style="height:44px" INLINE from ui.js, and an inline
   style beats a stylesheet rule — so shrinking .ox-search alone leaves the field 8px shorter than
   the buttons beside it and produces a visible stepped edge. They must be brought down together. */
#wo-root .ox-search { height: 36px; padding: 0 12px; border-radius: var(--radius-sm); }
#wo-root .ox-search input { font-size:14px; }
#wo-root .ox-searchband .ox-hero,
#wo-root .ox-searchband .ox-chip { height: 36px !important; font-size:13px; }

/* (the split-flap board - .ui-clock.lg - and its dimmed tiles were here; the job clock replaced every clock face on
   14-10, see .ui-run beside .ui-chain-clock. Nothing draws .ui-clock any more.) */

/* ---- the work-order readiness banner ----
   The page's one alert region. What is holding the job sits on the left in words; while the job is
   running, how long it has been running sits on the right as the flip board. They were four cards
   apart before, so "the permit expired and this job is still running" and the number saying how
   long had to be joined up by the reader. */
.wod-banner { display: flex; gap: 12px; align-items: flex-start;
              border-radius: var(--radius); padding: 12px 16px; margin-top: 16px; }
.wod-banner-ic { flex: none; margin-top: 1px; }
.wod-banner-say { flex: 1; min-width: 0; }
.wod-banner-say b { font-size:15px; display: block; }
.wod-banner-say ul { margin: 5px 0 0; padding-left: 16px; font-size:13px; line-height: 1.6; }

/* the clock rail — pinned right, and never squeezed into wrapping the board.
   ★ THE JOB CLOCK'S PANEL (14-10, design A, approved): a card of its own inside the tinted banner - the page's own
   ink on the page's own surface, so the figure reads at a distance whatever the banner's tone - holding the label,
   the figure, the estimate as a track (.ui-track), its scale and the note. The lamps sit under the title (.wod-lamps). */
.wod-runbox { flex: none; width: 360px; max-width: 100%; display: flex; flex-direction: column; gap: 10px;
              align-self: stretch; justify-content: center;
              background: var(--card); color: var(--text); border-radius: var(--radius-sm);
              border: 1px solid color-mix(in srgb, currentColor 18%, transparent); padding: 14px 16px; margin: -2px -6px -2px 4px; }
.wod-runbox > * { max-width: 100%; }
.wod-runlab .ui-lamp { color: var(--text-muted); }
.wod-runscale { display: flex; justify-content: space-between; gap: 10px; font-size: var(--fs-aux); color: var(--text-muted); }
.wod-runscale b { color: var(--text); }
.wod-runnote { display: flex; align-items: center; gap: 6px; font-size: var(--fs-body); font-weight: 700; line-height: 1.4; }
.wod-runnote.bad { color: var(--danger-dark); }
.wod-runnote.warn { color: var(--warning-dark); }
.wod-runnote.ok { color: var(--success-dark); }
.wod-lamps { margin-top: 10px; }
.wod-lamps .ui-lamp.ok { color: inherit; }

/* Under 900px the board would squeeze the sentence to a column of single words, so the rail moves
   below the text and the divider becomes a rule across the banner. */
@media (max-width: 900px) {
  .wod-banner { flex-wrap: wrap; }
  .wod-runbox { flex-basis: 100%; width: 100%; margin: 4px 0 0; }
}

/* ============================================================================
   [data-theme="soft"] — NEUMORPHISM, AS A CHOICE
   ----------------------------------------------------------------------------
   Soft UI: nothing sits ON the page, everything is pressed OUT of it. One
   material, one colour, and depth carried entirely by a pair of shadows — light
   from the top-left, shade to the bottom-right. Raised for what you press,
   debossed for what you type into.

   ★ OFFERED, NOT IMPOSED — and this one has real costs, so they are worth
   writing down rather than discovering:
     · it is monochrome by nature, and this app runs on semantic colour (red =
       permit expired, amber = overrun, green = cleared). So the status tints
       are KEPT, softened onto the grey rather than flattened into it. That is
       the compromise the look makes here, and it is the first thing to look at.
     · shadows need room, so rows and cards breathe wider than the default
       theme — a dense register shows fewer lines per screen.
     · it depends on a mid-tone ground. It cannot be combined with a white or
       black page, which is why 'plain' background and high contrast override
       it below rather than fighting it.
   ========================================================================== */
[data-theme="soft"] {
  /* ★ THE MATERIAL IS THE USER'S. --soft-base is written onto <html> from their saved preference;
     with none set it falls back to the slate this theme shipped with, so an account that never
     opens the picker sees exactly what it saw before.
     The light and the shade are DERIVED here rather than stored. Neumorphism only reads as depth
     when all three are one colour at different luminance — store them separately and any base the
     user picks would keep somebody else's highlight, and the extrusion would flatten into blur. */
  --soft-base:#e4e9f0;
  /* ★ --border IS NOT DECORATION HERE. Soft UI drops the border from a card or a button because the
     shadow already says where it ends — so this token was set to `transparent`. But 182 rules in
     this stylesheet read var(--border), and most of them are not cards: they are TABLE GRID LINES,
     chip edges and section rules, where the line IS the structure. The DOA matrix lost every cell
     boundary and became ten grades of floating text; the org tree's department chips lost their
     edges. So the token stays a real hairline — quiet, mixed from the material itself so it belongs
     to the theme — and the handful of things that genuinely should have no border say `border:none`
     for themselves below. */
  --bg:var(--soft-base); --card:var(--soft-base);
  /* Reverted: a visible hairline again. 182 rules read this token, so this is the single line that
     decides whether the product shows section rules, dividers, card headers and tab underlines at
     all. Mixed from the material so it belongs to whatever colour is chosen. */
  --border:color-mix(in srgb, var(--soft-base) 80%, #5b6b80);
  --border-strong:color-mix(in srgb, var(--soft-base) 62%, #5b6b80);
  /* --text-muted was #7b8798 = 2.96:1 on this theme's own card — below the floor, so every
     secondary label in the app was unreadable under Soft. #5c6778 measures 4.66:1 and still sits
     half-way to --text, so the hierarchy the theme depends on survives. */
  --text:#3d4757; --text-muted:#5c6778; --text-hint:#5c6778;
  --radius:16px; --radius-sm:12px;
  /* ★ THE LIGHT MUST GO ALL THE WAY. The first build set these literally — #ffffff and #bcc6d4 —
     and when the base became the user's I replaced them with a mix that only travelled 58% toward
     white. The highlight fell from 255 to ~244 and the extrusion went flat: the shade was still
     right, so the surface looked lit from nowhere. These are a FALLBACK; the real derivation is
     the @supports block below. */
  /* ★ THE DEFAULT STATE IS THE FIRST BUILD, VERBATIM — the literal pair the theme was first shown
     with. Everything that came after (a derived pair, tints mixed into the material) exists to make
     a CHOSEN colour work, and applies only under [data-soft-tint]. Choosing no colour therefore
     gives back exactly the surface that was approved, not an approximation of it. */
  --soft-lo:#ffffff;
  --soft-hi:#bcc6d4;
  --soft-up:-5px -5px 10px var(--soft-lo), 5px 5px 10px var(--soft-hi);
  --soft-up-sm:-3px -3px 6px var(--soft-lo), 3px 3px 6px var(--soft-hi);
  --soft-in:inset -3px -3px 7px var(--soft-lo), inset 3px 3px 7px var(--soft-hi);
}
[data-theme="soft"] body { background:var(--bg); background-image:none; }

/* ---- THE HEADER BAND, IN THIS THEME'S OWN TERMS ----
   Every other theme opens a page with a tinted band behind the title — a blue-to-cream wash. Under
   soft that wash was simply switched off, which left the top of the page flat and, worse, left a
   visible vertical edge where the band used to stop short of the content's full width.
   A soft band is not a different COLOUR laid over the paper; it is the same material catching more
   light near the top, exactly as a real surface does. So: one gradient, the base lightened a little
   at the very top and falling back to the base by 340px — full width, no edges to see, and it
   follows whatever colour the user picked because it is derived from it. */
[data-theme="soft"] .content-area {
  background-color: var(--soft-base);
  /* The band: the same material catching more light near the top, falling back to the base by
     340px. Full width, so it has no side edges to see, and derived from the base so it follows
     whatever colour is chosen. */
  background-image: linear-gradient(180deg,
      color-mix(in srgb, var(--soft-base) 88%, #ffffff) 0,
      var(--soft-base) 340px);
  background-repeat: no-repeat;
  background-attachment: local;
}
@supports (color: hsl(from white h s calc(l + 1))) {
  [data-theme="soft"] .content-area {
    background-image: linear-gradient(180deg,
        hsl(from var(--soft-base) h s calc(l + 3.5)) 0,
        var(--soft-base) 340px);
  }
}


/* raised surfaces */
[data-theme="soft"] .card,
[data-theme="soft"] .ox-stat,
[data-theme="soft"] .ox-card { background:var(--bg); border:none; border-radius:var(--radius);
                               box-shadow:var(--soft-up); }
[data-theme="soft"] .card-header,
[data-theme="soft"] .card-body { border-color:transparent; background:transparent; }

/* controls: raised at rest, pressed when touched */
[data-theme="soft"] .ox-chip,
[data-theme="soft"] .btn,
[data-theme="soft"] .btn-secondary,
[data-theme="soft"] .ox-icobtn { background:var(--bg); border:none; border-radius:var(--radius-sm);
                                 box-shadow:var(--soft-up-sm); color:var(--text); }
[data-theme="soft"] .ox-chip:hover,
[data-theme="soft"] .btn:hover,
[data-theme="soft"] .ox-icobtn:hover,
[data-theme="soft"] .ox-chip:active,
[data-theme="soft"] .btn:active { box-shadow:var(--soft-in); transform:none; }

/* the one accent left: a soft UI with no accent has no verb */
[data-theme="soft"] .ox-hero,
[data-theme="soft"] .btn-primary,
[data-theme="soft"] .ds-btn { background:linear-gradient(135deg,var(--primary),var(--primary-dark));
  color:#fff; border:none; border-radius:var(--radius-sm);
  box-shadow:-4px -4px 9px var(--soft-lo), var(--el-2); }
[data-theme="soft"] .ox-hero:hover,
[data-theme="soft"] .btn-primary:hover { transform:none;
  box-shadow:inset -3px -3px 7px rgba(255,255,255,.30), inset 3px 3px 7px rgba(0,0,0,.20); }

/* ---- inputs are pressed INTO the page ----
   ★ ONCE, THOUGH. .ox-search is a WRAPPER with an <input> inside it, so a blanket `input` rule
   pressed a well into the wrapper AND another into the field sitting in it — a box inside a box,
   with the typing area squeezed to 19px inside a 41px field. Depth is a property of the CONTROL, not
   of every element that happens to be one; anything already inside a pressed surface goes flat.
   Checkboxes and radios are excluded outright: the browser draws those, and a shadow around a 16px
   tick reads as damage. */
[data-theme="soft"] .form-control,
[data-theme="soft"] .ox-search,
/* :where() and not :not() — a :not() chain takes the specificity of its arguments, so three of them
   lifted this blanket rule to (0,4,1) and it beat the very override below meant to tame it. :where()
   weighs nothing, so the exclusion costs no specificity and the wrapper rule wins as intended. */
[data-theme="soft"] input:not(:where([type=checkbox],[type=radio],[type=range])),
[data-theme="soft"] select,
[data-theme="soft"] textarea {
  background:var(--bg); border:none; border-radius:var(--radius-sm);
  box-shadow:var(--soft-in); color:var(--text); }

/* inside an already-pressed wrapper: no second well, and fill the height you were given */
[data-theme="soft"] .ox-search input,
[data-theme="soft"] .ox-search select,
[data-theme="soft"] .ox-searchband .ox-search input {
  background:transparent; box-shadow:none; border:none; border-radius:0;
  height:100%; padding:0; flex:1; min-width:0; }
[data-theme="soft"] input[type=checkbox],
[data-theme="soft"] input[type=radio] { box-shadow:none; background:initial; }

[data-theme="soft"] .form-control:focus,
[data-theme="soft"] .ox-search:focus-within { box-shadow:var(--soft-in), 0 0 0 2px var(--primary); }
[data-theme="soft"] .ox-search input:focus { box-shadow:none; }

/* rows lose their rules — separation comes from the light */
[data-theme="soft"] .ox-row { border-bottom:none; border-radius:var(--radius-sm);
                              margin:6px 8px; box-shadow:var(--soft-up-sm); }
[data-theme="soft"] .ox-row:hover { box-shadow:var(--soft-in); background:var(--bg); }
[data-theme="soft"] .ox-ic,
[data-theme="soft"] .ox-rowic { background:var(--bg); box-shadow:var(--soft-in);
                                color:var(--primary-dark); border-radius:var(--radius-sm); }

/* ★ STATUS SURVIVES. Softened onto the grey, never flattened into it. */
[data-theme="soft"] .ox-b,
[data-theme="soft"] .badge { border:none; border-radius:var(--radius-sm); box-shadow:var(--soft-up-sm); }
[data-theme="soft"] .tint-bad  { background:#ecdfe2; box-shadow:var(--soft-up); }
[data-theme="soft"] .tint-warn { background:#ece5d8; box-shadow:var(--soft-up); }
[data-theme="soft"] .tint-ok   { background:#dde8e2; box-shadow:var(--soft-up); }
[data-theme="soft"] .wod-banner { border-radius:var(--radius); box-shadow:var(--soft-up); }


/* the dark bars are the one thing soft UI cannot absorb — brought into the material */
/* ★ THE SIDEBAR IS THE SAME MATERIAL. These colours were hardcoded, so the bar stayed slate-blue
   while the page went sand — the one part of the screen that did not follow the tint, which reads
   as the theme not applying at all. Everything here is now mixed from --soft-base, so the whole
   window is one material whatever colour is chosen. The dark bar is the only surface soft UI cannot
   keep: an extruded panel needs light falling on it, and nothing is extruded out of black. */
[data-theme="soft"] .sidebar {
  background: color-mix(in srgb, var(--soft-base) 92%, #5b6b80);
  box-shadow: 6px 0 14px color-mix(in srgb, var(--soft-hi) 70%, transparent);
  border-right: none;
}
[data-theme="soft"] .sidebar .nav-item,
[data-theme="soft"] .sidebar a,
[data-theme="soft"] .sidebar .nav-section { color: color-mix(in srgb, var(--text) 88%, transparent); }
[data-theme="soft"] .sidebar .nav-item:hover,
[data-theme="soft"] .sidebar .nav-group:hover {
  background: color-mix(in srgb, var(--soft-base) 80%, #5b6b80); border-radius: var(--radius-sm); }
[data-theme="soft"] .nav-item.active {
  background: var(--soft-base); box-shadow: var(--soft-in);
  border-radius: var(--radius-sm); color: var(--primary-dark); }
[data-theme="soft"] .sidebar-brand,
[data-theme="soft"] .brand { color: var(--text); }
/* ★ THE BAR IS PART OF THE BAND, NOT A THING SITTING ON IT. The topbar was flat base colour with a
   drop shadow, and the band beneath it starts lighter — so the eye met three tones inside eleven
   pixels (bar, its shadow, band top) and read the whole thing as a drawn line. That is the seam.
   In one material the bar shares the band's top tone and casts nothing onto it; the only separation
   is a hairline, which is a border and reads as one. */
[data-theme="soft"] .topbar {
  background: color-mix(in srgb, var(--soft-base) 88%, #ffffff);
  border: none;
  box-shadow: 0 14px 22px -18px color-mix(in srgb, var(--soft-hi) 85%, transparent); }


/* ★ THE OVERRIDES THAT MUST WIN. Soft UI needs a mid-tone ground and low-contrast edges — both of
   which are the opposite of what these settings were chosen for. Somebody who asked for a plain
   white page, or for high contrast, asked for legibility over texture, and gets it. */
[data-theme="soft"][data-bg="plain"] { --bg:#f4f6f9; --card:#ffffff; --border:#e2e8f0; }
[data-theme="soft"][data-bg="plain"] .card,
[data-theme="soft"][data-bg="plain"] .ox-stat { background:var(--stat-tint,var(--card)); border:1px solid var(--border);
                                                box-shadow:var(--el-1); }
[data-theme="soft"][data-contrast="high"],
[data-theme="soft"][data-contrast="xhigh"] { --text:#1e293b; --text-muted:#475569; --border:#94a3b8; }
[data-theme="soft"][data-contrast="high"] .card,
[data-theme="soft"][data-contrast="xhigh"] .card,
[data-theme="soft"][data-contrast="high"] .ox-chip,
[data-theme="soft"][data-contrast="xhigh"] .ox-chip { border:1px solid var(--border); }

/* ★ SOFT AND GLASS CANNOT BOTH BE TRUE. Glass says a surface is translucent and floating; soft says
   it is solid and extruded from the page. Glass is otherwise orthogonal to theme and composes with
   all nine other looks, so rather than removing the setting this neutralises it for this one theme
   — scoped to BOTH attributes, the same way [data-theme="black"][data-glass="full"] already is, so
   it outranks the single-attribute glass rules instead of fighting them with !important. */
[data-theme="soft"][data-glass] .card,
[data-theme="soft"][data-glass] .panel,
[data-theme="soft"][data-glass] .stat,
[data-theme="soft"][data-glass] .ox-stat,
[data-theme="soft"][data-glass] .ox-card,
[data-theme="soft"][data-glass] .ovl > .card {
  background: var(--stat-tint, var(--bg));
  border: none;
  box-shadow: var(--soft-up);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* ★ AND THIS ONE OUTRANKS THE TOPBAR RULE FURTHER DOWN. Written earlier to neutralise glass, it is
   scoped to BOTH attributes — so it kept winning and holding the bar at flat base colour while the
   band beneath it started lighter, which is the seam. It carries the band's own top tone now, so
   the bar and the band are one surface. */
[data-theme="soft"][data-glass] .topbar {
  background: color-mix(in srgb, var(--soft-base) 88%, #ffffff);
  border: none;
  /* No hairline. A 1px rule between two surfaces of the SAME colour is read as a drawn line, which
     is exactly the seam being complained about — the bar and the band are one material, so what
     separates them is depth, not ink. A wide, shallow falloff gives the bar somewhere to be when
     content scrolls under it without ever resolving into an edge. */
  border-bottom: none;
  box-shadow: 0 14px 22px -18px color-mix(in srgb, var(--soft-hi) 85%, transparent);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}


/* ---- SOFT: things that FLOAT above the material ----
   ★ ONE MATERIAL HAS A COST. This theme sets --card to --bg on purpose: in soft UI nothing sits on
   the page, it is pressed out of it. But every popover in the app separates itself from the page by
   being --card ON --bg, and when those are the same colour the separation disappears:
     · .ds-pop painted #e4e9f0 on a #e4e9f0 page — the dropdown was there, invisible, and only the
       caret gave it away. With glass on it was 58% of that same colour, i.e. worse.
     · .ds-opt:hover sets background:var(--bg) — the colour it already was, so hovering did nothing.
   A real flip-side: a panel that has LEFT the surface catches more light than the surface it left.
   So floating things get a lighter material and a lift, and a hovered row is PRESSED instead of
   recoloured — which is the theme's own vocabulary rather than a borrowed highlight. */
[data-theme="soft"] {
  --soft-raised:color-mix(in srgb, var(--soft-base) 72%, #ffffff);
  --soft-sunk:color-mix(in srgb, var(--soft-base) 88%, #5b6b80);
}

[data-theme="soft"] .ds-pop,
[data-theme="soft"] .nav-fly,
[data-theme="soft"] .ui-expmenu,
[data-theme="soft"] .cacct-pop,
[data-theme="soft"] .ox-cardpop,
[data-theme="soft"] .ch-emoji,
[data-theme="soft"][data-glass] .ds-pop,
[data-theme="soft"][data-glass] .nav-fly,
[data-theme="soft"][data-glass] .ui-expmenu,
[data-theme="soft"][data-glass] .cacct-pop,
[data-theme="soft"][data-glass] .ox-cardpop {
  /* STEP 2 — no outline. A panel that has left the surface is told apart by being lighter than it
     and by the shadow it throws, which is the whole vocabulary of this theme. An outline on top of
     both is a third statement saying the same thing, and it is the one that reads as a drawn line.
     The lift is deepened slightly to carry the separation on its own. */
  background: var(--soft-raised);
  border: none;
  box-shadow: -7px -7px 16px var(--soft-lo), 10px 12px 30px color-mix(in srgb, var(--soft-hi) 100%, transparent);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* a hovered option is pressed into the panel; the selected one stays pressed and takes the accent */
[data-theme="soft"] .ds-opt:hover,
[data-theme="soft"] .nav-fly .pit:hover,
[data-theme="soft"] .ui-expmenu button:hover {
  background: var(--soft-sunk);
  box-shadow: inset -2px -2px 5px var(--soft-lo), inset 2px 2px 5px var(--soft-hi);
}
[data-theme="soft"] .ds-opt.on,
[data-theme="soft"] .ds-opt[aria-selected="true"] {
  background: var(--primary-light);
  color: var(--primary-dark);
  box-shadow: inset -2px -2px 5px rgba(255,255,255,.55), inset 2px 2px 5px color-mix(in srgb, var(--primary) 22%, transparent);
}
/* the search box inside a dropdown sits in the panel, so it is pressed into the PANEL's colour */
[data-theme="soft"] .ds-search input { background: var(--soft-sunk); box-shadow: var(--soft-in); }

/* ---- SOFT: fields must still look like fields ----
   An input with no border and only an inset shadow reads fine at form size, on a card, with space
   around it. In a DENSE TABLE it does not: the DOA matrix puts a select and an amount in every cell
   of a ten-row grid, and at that size the shadow is too quiet to say "this is where you type". So a
   control keeps a hairline as well as its well — the well says pressed, the line says edge. */
/* ★ SCOPED TO WHERE STRUCTURE ACTUALLY NEEDS IT. The first version of this put a hairline on EVERY
   field, chip and badge in the product because the DOA matrix — a ten-row grid of selects — was
   unreadable without one. That fixed the matrix and cost the theme its character everywhere else:
   soft UI is borderless by definition, and a page of outlined boxes with a shadow behind them is
   just the ordinary UI wearing a texture.
   A field is given an edge only where it sits in a DENSE CONTEXT and the well alone cannot carry
   it — inside a table, and inside a form grid. Standing on its own, on a card, it stays borderless
   the way the theme was first shown. */
[data-theme="soft"] table .form-control,
[data-theme="soft"] table .ds-sel,
[data-theme="soft"] table input:not(:where([type=checkbox],[type=radio],[type=range])),
[data-theme="soft"] table select,
[data-theme="soft"] .form-grid-2 .form-control,
[data-theme="soft"] .ds-fp-grid .form-control,
[data-theme="soft"] .ds-fp-grid .ds-sel {
  border: 1px solid color-mix(in srgb, var(--soft-base) 74%, #5b6b80);
}
/* the inner field of a wrapper keeps none — the wrapper already carries the edge */
[data-theme="soft"] .ox-search input { border: none; }

/* a table under soft: hairlines from the material, and a header that reads as a header */
[data-theme="soft"] table th,
[data-theme="soft"] table td { border-color: var(--border); }
[data-theme="soft"] table th { background: color-mix(in srgb, var(--soft-base) 88%, #ffffff); }

/* chips and tags in the org tree keep their outline — they are objects in a list, not surfaces */
/* Badges keep their tint and their small lift; the outline added while chasing the DOA matrix made
   every pill on every page look boxed, which is not this theme. */
[data-theme="soft"] .ox-b,
[data-theme="soft"] .badge,
[data-theme="soft"] .chip,
[data-theme="soft"] .ox-groupn { border: none; }

/* ---- SOFT: the light and the shade, derived the way the eye expects ----
   A mix toward white cannot serve every base. On a light material it must reach nearly pure white
   or the surface looks lit from nowhere; on a dark one, a near-white highlight is not a highlight
   at all, it is a stripe. What is actually wanted is the SAME colour, lighter and darker — which is
   a move along lightness, not a blend with white.
   Relative colour syntax does exactly that, so where the browser has it the pair is computed from
   the base's own hue and saturation. On #e4e9f0 this returns #ffffff and a shade at 79% lightness —
   i.e. the literal values the theme was first built with — so the original look is reproduced
   exactly, and every other colour now gets its own correct pair instead of a borrowed one. */
/* ★ THE CHANNELS ARE BARE NUMBERS, NOT PERCENTAGES. calc(l + 9%) parses as invalid inside
   hsl(from …) — and an invalid custom property is not ignored, it resolves to nothing, so the
   highlight and the shade both vanished and every shadow went transparent. Worse than the value
   being wrong. The @supports test must exercise the SAME form that is used, or it green-lights a
   syntax the browser then rejects. */
@supports (color: hsl(from white h s calc(l + 1))) {
  [data-theme="soft"][data-soft-tint] {
    --soft-lo: hsl(from var(--soft-base) h s calc(l + 9));
    --soft-hi: hsl(from var(--soft-base) h calc(s + 2) calc(l - 13));
    --soft-raised: hsl(from var(--soft-base) h s calc(l + 4));
    --soft-sunk:   hsl(from var(--soft-base) h s calc(l - 4));
  }
}

/* The default stat-tile colour, previously written inline by ui.js where no stylesheet could reach
   it. As a class it themes like everything else. */
.ox-ic-plain { background: var(--primary-light); color: var(--primary-dark); }

/* ---- SOFT: one material, no ambient wash ----
   [data-glass="full"] paints three wide colour blooms across .app-layout so a frosted panel has
   something to refract. Soft has no frosted panels — it has a solid material — so the blooms only
   showed as a tonal drift across the page, lighter at the edges than in the middle. That drift is
   the "colour difference" between the middle and the sides. Scoped to both attributes so it beats
   the glass rule without !important. */
/* Reverted: the ambient blooms stay. [data-glass="full"] paints three wide, low-opacity --primary
   glows across .app-layout so a frosted panel has something to refract. They were cleared here
   while hunting a seam, on the assumption they were causing tonal drift across the page — measuring
   the first build showed variation 0 across the content area, because .content-area is opaque and
   covers .app-layout entirely. They were never the drift, so they go back. */
/* the tile follows the material rather than staying cold blue on a warm page */
[data-theme="soft"] .ox-ic-plain,
[data-theme="soft"] .ox-ic,
[data-theme="soft"] .ox-rowic {
  background: var(--soft-sunk);
  color: var(--primary-dark);
  box-shadow: var(--soft-in);
}

/* ============================================================================
   SOFT — THE TINT LAYER
   ----------------------------------------------------------------------------
   ★ THE AUDIT'S REAL FINDING. Six defects had been fixed one surface at a time; running the theme
   under two very different bases and diffing the computed styles showed the pattern underneath:
   every status tint, and --primary-light with its 34 consumers, was still a cold literal from the
   default palette. On a warm material the icon tiles, the view switcher, the stage pills and the
   help button all stayed blue-grey — not broken, but plainly not part of the page.
   A tint is an accent MIXED INTO THE MATERIAL, not a colour laid on top of it. Each one keeps its
   hue — and therefore its meaning, which is the thing that must not move — while sitting in the
   surface the user chose. Red still reads red on sand; it just reads like it belongs there.
   ========================================================================== */
[data-theme="soft"][data-soft-tint] {
  --primary-light:   color-mix(in srgb, var(--primary) 17%, var(--soft-base));
  --nav-active-bg:   color-mix(in srgb, var(--primary) 11%, var(--soft-base));
  --tint-neutral-bg: color-mix(in srgb, var(--soft-base) 90%, #5b6b80);
  --tint-info-bg:    color-mix(in srgb, var(--primary) 13%, var(--soft-base));
  --tint-warn-bg:    color-mix(in srgb, var(--warning) 22%, var(--soft-base));
  --tint-hot-bg:     color-mix(in srgb, var(--tint-hot-dot) 20%, var(--soft-base));
  --tint-danger-bg:  color-mix(in srgb, var(--danger) 16%, var(--soft-base));
  --tint-ok-bg:      color-mix(in srgb, var(--success) 20%, var(--soft-base));
  --success-light:   color-mix(in srgb, var(--success) 20%, var(--soft-base));
  --warning-light:   color-mix(in srgb, var(--warning) 22%, var(--soft-base));
  --th-bg:           color-mix(in srgb, var(--soft-base) 90%, #ffffff);
}

/* Two literals the audit caught that no token covers: the sidebar collapse button was pure white on
   a sand bar, and the secondary button carried the DEFAULT theme's #f8fafc rather than this one's
   paper — both are surfaces, so both take the material and its lift. */
[data-theme="soft"] .side-toggle,
[data-theme="soft"] .btn-secondary {
  background: var(--soft-base);
  border: none;
  color: var(--text);
  box-shadow: var(--soft-up-sm);
}
[data-theme="soft"] .side-toggle:hover,
[data-theme="soft"] .btn-secondary:hover { box-shadow: var(--soft-in); }

/* The view switcher sat on the old neutral literal; it is a pressed track with raised keys. */
[data-theme="soft"] .ox-views { background: var(--tint-neutral-bg); box-shadow: var(--soft-in); }
[data-theme="soft"] .ox-views button.on { background: var(--soft-base); box-shadow: var(--soft-up-sm); }

/* The bar takes the band's own top tone — measured, these met with a 33-unit step when they did not,
   and that step is what reads as a drawn line between them. */
@supports (color: hsl(from white h s calc(l + 1))) {
  [data-theme="soft"] .topbar,
  [data-theme="soft"][data-glass] .topbar { background: hsl(from var(--soft-base) h s calc(l + 3.5)); }
}

/* ---- SOFT: the two controls the audit caught still wearing another theme's colours ----
   "How this works" sets its own tinted border in form-docs.ejs, so under this theme it was the one
   outlined button on every page while its neighbours had none. The sidebar collapse button reads
   var(--card), which [data-bg="plain"] pins to #ffffff — a white chip on a coloured bar. Both are
   surfaces: they take the material and the theme's own lift. */
[data-theme="soft"] .khtfw-tab {
  background: var(--soft-base);
  border: none;
  color: var(--primary-dark);
  box-shadow: var(--soft-up-sm);
}
[data-theme="soft"] .khtfw-tab:hover { background: var(--soft-base); box-shadow: var(--soft-in); }
[data-theme="soft"] .side-toggle {
  background: var(--soft-base);
  border: none;
  box-shadow: var(--soft-up-sm);
}

/* ---- SOFT: the last three the audit and the eye caught ----
   1. WORK ORDERS HAS NO BAND, ON PURPOSE. 123 roles hold this module and 112 of them create jobs on
      it — it is the page rows were tightened from 93px to 54px for, so 340px of decorative tint at
      the top works against the only thing that page is for. But the topbar was still carrying the
      band's lighter tone, so it met flat paper with a 12-unit step: a line under the bar on one page
      and none anywhere else. The bar goes flat here too, and the exception becomes deliberate.
   2. .btn-sm carried the DEFAULT theme's #f8fafc rather than this theme's paper.
   3. A field with neither border nor shadow is not a field; the well is the minimum. */
.content-area:has(#wo-root) ~ .topbar,
[data-theme="soft"] .content-area:has(#wo-root),
[data-theme="soft"]:has(#wo-root) .topbar,
[data-theme="soft"][data-glass]:has(#wo-root) .topbar {
  background: var(--soft-base);
  background-image: none;
  box-shadow: 0 10px 18px -16px color-mix(in srgb, var(--soft-hi) 80%, transparent);
}
/* ★ A SIZE CLASS MUST NOT SET A COLOUR.
   This read `[data-theme="soft"] .btn-sm { background: var(--soft-base); color: var(--text) }` — the
   same specificity as the `.btn-primary` rule 360 lines above it, but LATER, so it won. `.btn-sm`
   says how BIG a button is; it was also deciding what it MEANS. Under this theme every small button
   in the app came out the same putty colour whatever it did, so on Recruitment "Make An Offer" and
   "Not Taken Forward" were the same button — measured identical at rgb(228,233,240) — and the row
   offered no clue which one commits and which one ends a candidacy.
   `:not()` on the two roles that carry their own colour, so the size rule dresses only the buttons
   that have no colour of their own. Small primaries keep the accent the theme deliberately kept:
   "a soft UI with no accent has no verb". */
[data-theme="soft"] .btn-sm:not(.btn-primary):not(.btn-danger) {
  background: var(--soft-base); border: none; color: var(--text); box-shadow: var(--soft-up-sm);
}
[data-theme="soft"] .btn-sm.btn-primary { box-shadow: var(--soft-up-sm); }
[data-theme="soft"] input:not(:where([type=checkbox],[type=radio],[type=range],[type=color])),
[data-theme="soft"] select,
[data-theme="soft"] textarea { box-shadow: var(--soft-in); }


/* ── WHAT TO LOOK AT ── the findings block a module page hosts (ui.findings). Tokens only, so it
   re-themes with everything else; severity carries its own edge so the loud ones read at a glance. */
.ui-find { margin: 14px 0; }
.ui-find-h { display:flex; align-items:center; gap:8px; font-size:13px; font-weight:600;
 color:var(--text-muted); margin-bottom:8px; }
.ui-find-n { font-size:11px; font-weight:600; padding:0 6px; border-radius:var(--radius-pill);
  background:var(--primary-light); color:var(--primary-dark); }
.ui-find-more { margin-left:auto; font-size:12px; font-weight:400; text-transform:none;
  letter-spacing:0; color:var(--primary-dark); }
.ui-find-none { border:1px dashed var(--border); border-radius:var(--radius); padding:12px 12px;
  font-size:13px; color:var(--text-muted); background:var(--bg); }
.ui-find-c { border:1px solid var(--border); border-left:3px solid var(--border);
  border-radius:var(--radius); background:var(--card); padding:12px 12px; margin-bottom:8px; }
.ui-find-c.act   { border-left-color:var(--danger); }
.ui-find-c.warn  { border-left-color:var(--warning); }
.ui-find-c.watch { border-left-color:var(--text-muted); }
.ui-find-t { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
.ui-find-t b { font-size:15px; color:var(--text); }
.ui-find-sev {
  text-transform: capitalize; font-size:11px; font-weight:600;
  padding:2px 8px; border-radius:var(--radius-pill); white-space:nowrap; }
.ui-find-sev.act   { color:var(--danger); background:var(--danger-light, #fee2e2); }
.ui-find-sev.warn  { color:var(--warning-dark); background:var(--warning-light); }
.ui-find-sev.watch { color:var(--text-muted); background:var(--bg); }
.ui-find-dom { font-size:11px; padding:2px 8px; border-radius:var(--radius-pill);
  background:var(--bg); color:var(--text-muted); white-space:nowrap; }
.ui-find-evs { margin-left:auto; display:flex; gap:6px; flex-wrap:wrap; }
.ui-find-ev { font-size:11px; font-variant-numeric:tabular-nums; padding:2px 6px; border-radius:var(--radius-sm);
  background:var(--bg); color:var(--text-muted); white-space:nowrap; }
.ui-find-b { font-size:14px; color:var(--text); margin-top:6px; line-height:1.5; }
.ui-find-l { margin-top:8px; }
.ui-find-l > span { font-size:11px; font-weight:600;
  color:var(--text-muted); }
.ui-find-l ul { margin:3px 0 0; padding-left:20px; font-size:13px; }
.ui-find-l li { margin-bottom:2px; }
.ui-find-src { font-size:11px; color:var(--text-muted); margin-top:8px; }

/* ================================================================================================
   CONTROL ROOM  —  the plant as an instrument, not a document.
   Two grounds, one language: `console` is the night surface, `console-day` the same thing on paper.

   ★ WHAT MAKES IT AN INSTRUMENT is not the dark ground — that is a preference, and the day variant
   proves it by changing only token VALUES. It is these, and they hold on both:
     · SANS FOR LANGUAGE, MONO FOR ANYTHING MEASURED — a value, a reference, a timestamp, a
       micro-label. That split is the signature and it is why a number reads as a reading.
     · IF A PIXEL IS COLOURED IT IS STATING A CONDITION. No blue buttons, no blue links, no blue
       active states, no blue icons. Chrome is the five-step grey ramp below; red, amber and green
       mean state and nothing else.
     · A READING CARRIES ITS LIMIT. Handled by the pages, not here, but the theme leaves room for it.
     · EVERY STRUCTURE IS A HAIRLINE GRID — 1px of --ln showing between cells, never a box.

   ★ AND EVERY SIZE COMES FROM THE SCALE. No raw px font-size or padding appears below; they read
   --fs-* and --space-* from :root. `theme-raw-px` in db/design-audit.js fails the build otherwise,
   which is the whole reason this is the second Console and not the third: the first two set sizes by
   hand, one of them moved the rem basis underneath its own numbers, and buttons ended up smaller
   than the nav items beside them.

   ★ IT OVERRIDES, ON PURPOSE: the Claude.ai sidebar pass (which makes .sidebar transparent and
   colours .nav-item by color-mix off --text — unreadable on a dark rail), the glass pass on cards
   and the top bar, and the four .ox-stat.tint-* fills, which are re-pointed through --stat-tint
   rather than fought over. Placed last in the file so it wins those ties on source order.
   ================================================================================================ */

/* ---- THE NIGHT GROUND -------------------------------------------------------------------------
   Never pure black: pure black on an LCD crushes the hairlines that carry all the structure. */
/* ★ THE CONTRAST AXIS DID NOTHING ON THESE TWO THEMES. `[data-contrast="high"]` and `…xhigh` are
   (0,1,0) and sit at line ~2010; `[data-theme="console"]` is also (0,1,0) and sits 3,400 lines later,
   so it won on source order and the person who picked High Contrast for accessibility got the default
   ink with no error. warm, black and soft all carry explicit pairings; the two newest themes were
   never given one. These are (0,2,0) and settle it regardless of order. */
[data-theme="console"][data-contrast="high"]  { --text:#f4f7fa; --text-muted:#b3bdc9; --border:#2b333d; }
[data-theme="console"][data-contrast="xhigh"] { --text:#ffffff; --text-muted:#ccd5df; --border:#3a444f; }
[data-theme="console-day"][data-contrast="high"]  { --text:#0a0f18; --text-muted:#374151; --border:#c8d0da; }
[data-theme="console-day"][data-contrast="xhigh"] { --text:#000000; --text-muted:#1f2937; --border:#9aa7b6; }

[data-theme="console"] {
  --cr-g0:#08090b;  /* the rail — the deepest thing on screen */
  --cr-g1:#0c0f13;  /* the canvas */
  --cr-g2:#11151a;  /* a panel: a card, a reading, a field */
  --cr-g3:#161b21;  /* raised: a header cell, a neutral button */
  --cr-ln:#1e242c;  /* the hairline that draws every grid */
  --cr-ln2:#2a323c; /* a border that must be seen */
  --cr-t0:#ffffff;  /* brightest text: brand, active nav, page title */

  /* ★ THE THREE THAT MUST NEVER BE LITERALS. Building the day variant caught the brand, the active
     nav row and the page title still hardcoded #fff — each went white-on-white the instant the
     ground flipped, and none of it is visible while you are in the dark one. Same defect as the
     company name vanishing on the navy rail. A colour whose only definition is a literal cannot
     follow its theme. */
  --primary:#96a0ad; --primary-dark:#c8d0da; --primary-light:#1a2028; --nav-active-bg:#11151a;
  --bg:var(--cr-g1); --card:var(--cr-g2);
  --text:#e6eaef; --text-muted:#96a0ad; --text-secondary:#c2c9d2;
  --border:var(--cr-ln); --border-strong:var(--cr-ln2);
  --sidebar-bg:var(--cr-g0); --sidebar-hover:var(--cr-g2);
  --sidebar-text:#96a0ad; --sidebar-active:#ffffff;
  --th-bg:var(--cr-g0);

  /* state — the only saturated colour permitted anywhere in this theme */
  --success:#3ecf8e; --success-light:#0d1c16; --success-dark:#3ecf8e;
  --warning:#ffb454; --warning-light:#1d1710; --warning-dark:#ffb454;
  --danger:#ff5d5d;  --danger-light:#1e1013;  --danger-dark:#ff5d5d;
  --tint-ok-bg:#0d1c16;     --tint-ok-fg:#3ecf8e;
  --tint-warn-bg:#1d1710;   --tint-warn-fg:#ffb454;
  --tint-danger-bg:#1e1013; --tint-danger-fg:#ff5d5d;
  --tint-hot-bg:#1f1410;    --tint-hot-fg:#ff9d5c;    --tint-hot-dot:#ff8a3d;
  --tint-info-bg:#141920;   --tint-info-fg:#96a0ad;
  --tint-neutral-bg:#141920; --tint-neutral-fg:#96a0ad;

  --radius:6px; --radius-sm:4px;
  --shadow:0 1px 2px rgba(0,0,0,.5);
  --shadow-md:0 2px 6px rgba(0,0,0,.55);
  --shadow-lg:0 10px 26px -8px rgba(0,0,0,.7);
  --sidebar-width:216px;
  --topbar-height:44px;
  color-scheme: dark;
}

/* ---- THE DAY GROUND ---------------------------------------------------------------------------
   The same theme with different token values and NOT ONE selector redefined below this block. The
   states are darker and less saturated than the night set, because the night reds and ambers glare
   on paper and two of them fail contrast outright. */
/* ★ THE DAY VARIANT IS NOT THE NIGHT ONE WITH THE VALUES FLIPPED, and the first version was exactly
   that. It swapped 26 tokens, proved the swap worked mechanically, and shipped — which is how a
   theme ends up dull rather than quiet. Measured, the reason was arithmetic and not taste:
     · the chrome accent was #5a6472, a grey at 21% SATURATION, so every button, every active nav
       row and every interactive thing on the page was grey;
     · the state tints sat at 1.07–1.09 contrast against white, which is to say invisible — the
       alarm strip, the pills and the amber note were all functionally plain white;
     · the canvas was 1.06 from the cards, so nothing separated from anything.
   The night rule "chrome is grey, colour means a condition" works on a dark ground because the
   states GLOW against it. Nothing glows on paper. So the day variant keeps colour reserved for
   state, and gives the chrome ONE accent of its own — a deep petrol that reads as instrument ink
   rather than as a highlight, and is deliberately nowhere near the app's #3b82f6 so the two themes
   are never mistaken for each other. */
[data-theme="console-day"] {
  /* ★ PURE WHITE, AND COLOURFUL. The opposite of the night theme, not a lit version of it.
     Two earlier attempts got this wrong the same way: the first was grey chrome on off-white and
     read dull; the second answered "dull" with a deep petrol accent, which is a colour I chose and
     which reads green. Neither was asked for. So: the paper is #ffffff — cards, rail and canvas all
     white, separated by a hairline and nothing else — and the accent is the PRODUCT'S OWN BLUE, not
     a hue invented for this theme. The states are the app's own full-strength palette rather than
     the muted set I mixed for the first pass. Colour here comes from the design system, which is
     where it should have come from at the start. */
  --cr-g0:#ffffff; --cr-g1:#ffffff; --cr-g2:#ffffff; --cr-g3:#f4f7fb;
  --cr-ln:#e3e8ef; --cr-ln2:#cbd5e1; --cr-t0:#0b1220;

  --primary:#3b82f6; --primary-dark:#1d4ed8; --primary-light:#dbeafe; --nav-active-bg:#eff6ff;
  --bg:var(--cr-g1); --card:var(--cr-g2);
  --text:#0f172a; --text-muted:#475569; --text-secondary:#1e293b;
  --border:var(--cr-ln); --border-strong:var(--cr-ln2);
  --sidebar-bg:#ffffff; --sidebar-hover:#f1f5f9;
  --sidebar-text:#475569; --sidebar-active:#0f172a;
  --th-bg:#f8fafc;

  /* the design system's own states, at full strength */
  --success:#10b981; --success-dark:#047857;
  --warning:#f59e0b; --warning-dark:#b45309;
  --danger:#ef4444;  --danger-dark:#b91c1c;
  --tint-ok-bg:#d1fae5;     --tint-ok-fg:#047857;
  --tint-warn-bg:#fef3c7;   --tint-warn-fg:#b45309;
  --tint-danger-bg:#fee2e2; --tint-danger-fg:#b91c1c;
  --tint-hot-bg:#ffedd5;    --tint-hot-fg:#c2410c;   --tint-hot-dot:#f97316;
  --tint-info-bg:#dbeafe;   --tint-info-fg:#1d4ed8;
  --tint-neutral-bg:#f1f5f9; --tint-neutral-fg:#475569;
  --success-light:#d1fae5; --warning-light:#fef3c7; --danger-light:#fee2e2;

  --radius:6px; --radius-sm:4px;
  --shadow:0 1px 2px rgba(15,23,42,.06);
  --shadow-md:0 2px 6px rgba(15,23,42,.08);
  --shadow-lg:0 10px 26px -10px rgba(15,23,42,.16);
  --sidebar-width:216px;
  --topbar-height:44px;
}

/* ================================================================================================
   ONE SET OF RULES, BOTH GROUNDS. Everything below is scoped to both keys and reads only tokens —
   which is what makes the day variant twenty-six values rather than a second theme.
   ================================================================================================ */

/* ---- canvas: no ambient wash. The gradient on .content-area varies down the page and behind a
   hairline grid it reads as a rendering artifact rather than a surface. -------------------------- */
[data-theme="console"] .app-layout,
[data-theme="console-day"] .app-layout { background:var(--bg); }
[data-theme="console"] .content-area,
[data-theme="console-day"] .content-area {
  background-image:none; padding:var(--space-4) var(--space-5); max-width:1560px;
}

/* ---- the rail. OVERRIDES the Claude.ai sidebar pass, which sets .sidebar transparent and derives
   every nav colour from color-mix() on --text. On a dark ground that is slate on slate. ---------- */
[data-theme="console"] .sidebar,
[data-theme="console-day"] .sidebar {
  background:var(--sidebar-bg); border-right:1px solid var(--cr-ln);
  -webkit-backdrop-filter:none; backdrop-filter:none;
}
[data-theme="console"] .sidebar-brand,
[data-theme="console-day"] .sidebar-brand {
  border-bottom:1px solid var(--cr-ln); padding:var(--space-3) var(--space-4) var(--space-2);
}
[data-theme="console"] .sidebar-brand h2,
[data-theme="console-day"] .sidebar-brand h2 { color:var(--cr-t0); font-size:var(--fs-head); }
[data-theme="console"] .sidebar-brand .brand-sub,
[data-theme="console-day"] .sidebar-brand .brand-sub {
  color:var(--text-muted); font-family:var(--font-mono); font-size:var(--fs-label); letter-spacing:.12em;
}
[data-theme="console"] .nav-item,
[data-theme="console-day"] .nav-item {
  color:var(--sidebar-text); font-size:var(--fs-body); font-weight:500;
  padding:var(--space-1) var(--space-3); gap:var(--space-2); border-radius:var(--radius-sm);
}
[data-theme="console"] .nav-sub .nav-item,
[data-theme="console-day"] .nav-sub .nav-item { font-size:var(--fs-aux); }
/* a group heading is a micro-label, and it is mono like every other one in this theme */
[data-theme="console"] .nav-group,
[data-theme="console-day"] .nav-group {
  font-family:var(--font-mono); font-size:var(--fs-label); letter-spacing:.13em;
  color:var(--text-muted); padding:var(--space-3) var(--space-3) var(--space-1);
  margin:var(--space-1) 0 0;
}
[data-theme="console"] .nav-item:hover,
[data-theme="console-day"] .nav-item:hover { background:var(--sidebar-hover); color:var(--sidebar-active); }
[data-theme="console"] .nav-group:hover,
[data-theme="console-day"] .nav-group:hover { color:var(--sidebar-active); }
[data-theme="console"] .nav-section.is-open,
[data-theme="console-day"] .nav-section.is-open { background:none; }
/* the active row is an EDGE, not a fill — on a dark rail a fill is a smudge, an edge is a position */
[data-theme="console"] .nav-item.active, [data-theme="console"] .nav-item.nav-parent.expanded {
  background:var(--sidebar-hover); color:var(--cr-t0); font-weight:600;
  box-shadow:inset 2px 0 0 var(--text-muted);
}
/* On paper the active row takes the accent — a grey edge on a white rail is not a cue, it is a
   smudge, and this is the one place the day variant genuinely needs to shout a little. */
[data-theme="console-day"] .nav-item.active, [data-theme="console-day"] .nav-item.nav-parent.expanded {
  background:var(--nav-active-bg); color:var(--primary-dark); font-weight:600;
  box-shadow:inset 2px 0 0 var(--primary);
}
/* everything is white, so the only thing separating a surface from the page is its edge */
[data-theme="console-day"] .sidebar { border-right:1px solid var(--cr-ln); }
[data-theme="console-day"] .card, [data-theme="console-day"] .panel,
[data-theme="console-day"] .ox-stat, [data-theme="console-day"] .ds-card {
  border:1px solid var(--cr-ln); box-shadow:none;
}
[data-theme="console-day"] .nav-item.active .nav-icon { color:var(--primary); }
[data-theme="console"] .nav-item .nav-icon,
[data-theme="console-day"] .nav-item .nav-icon { color:var(--text-muted); }
[data-theme="console"] .nav-item.active .nav-icon, [data-theme="console"] .nav-item:hover .nav-icon,
[data-theme="console-day"] .nav-item.active .nav-icon, [data-theme="console-day"] .nav-item:hover .nav-icon {
  color:var(--text);
}
/* icons.js writes width/height as ATTRIBUTES, so CSS reaches them; 16 beside 13px text. */
[data-theme="console"] .nav-item .nav-icon svg,
[data-theme="console-day"] .nav-item .nav-icon svg { width:16px; height:16px; }
/* ★ the "View only" disc. It paints var(--card) with a ring of the same, which is invisible on a
   white rail — the point — and a bright sticker repeated down a dozen rows on a dark one. */
[data-theme="console"] .nav-ro,
[data-theme="console-day"] .nav-ro {
  background:var(--sidebar-hover); color:var(--text-muted); box-shadow:0 0 0 1.5px var(--sidebar-bg);
}
[data-theme="console"] .cacct .cnm,
[data-theme="console-day"] .cacct .cnm { color:var(--text); font-size:var(--fs-body); }
[data-theme="console"] .cacct .crl, [data-theme="console"] .cacct .cdl,
[data-theme="console-day"] .cacct .crl, [data-theme="console-day"] .cacct .cdl { color:var(--text-muted); }
[data-theme="console"] .cacct .cav,
[data-theme="console-day"] .cacct .cav { background:var(--cr-g3); color:var(--text); }
[data-theme="console"] .cacct:hover,
[data-theme="console-day"] .cacct:hover { background:var(--sidebar-hover); }
[data-theme="console"] .side-foot,
[data-theme="console-day"] .side-foot { border-top:1px solid var(--cr-ln); }

/* ---- top bar. OVERRIDES the glass bar; at 44px a blur has nothing to refract. ----------------- */
[data-theme="console"] .topbar,
[data-theme="console-day"] .topbar {
  background:var(--cr-g0); border-bottom:1px solid var(--cr-ln); box-shadow:none;
  -webkit-backdrop-filter:none; backdrop-filter:none;
}
[data-theme="console"] .topbar-title,
[data-theme="console-day"] .topbar-title { font-size:var(--fs-head); font-weight:600; color:var(--cr-t0); }

/* ---- surfaces. OVERRIDES the glass pass. .card itself gets NO padding: the base .card in this
   file carries none, so adding some invents a gutter on every full-bleed card. ------------------ */
[data-theme="console"] .card, [data-theme="console"] .panel, [data-theme="console"] .stat,
[data-theme="console"] .kpi-card, [data-theme="console"] .ds-card, [data-theme="console"] .ox-stat,
[data-theme="console-day"] .card, [data-theme="console-day"] .panel, [data-theme="console-day"] .stat,
[data-theme="console-day"] .kpi-card, [data-theme="console-day"] .ds-card, [data-theme="console-day"] .ox-stat {
  background:var(--stat-tint,var(--card));
  border:1px solid var(--border); box-shadow:var(--shadow);
  -webkit-backdrop-filter:none; backdrop-filter:none;
}
[data-theme="console"] .card-header, [data-theme="console-day"] .card-header {
  padding:var(--space-3) var(--space-4); background:var(--cr-g0);
}
[data-theme="console"] .card-header h3, [data-theme="console-day"] .card-header h3 { font-size:var(--fs-head); }
[data-theme="console"] .card-body,   [data-theme="console-day"] .card-body   { padding:var(--space-4); }
[data-theme="console"] .card-footer, [data-theme="console-day"] .card-footer { padding:var(--space-3) var(--space-4); }

/* ---- the header band ------------------------------------------------------------------------- */
[data-theme="console"] .ox-head, [data-theme="console-day"] .ox-head {
  gap:var(--space-3); margin-bottom:var(--space-3);
}
[data-theme="console"] .ox-title, [data-theme="console-day"] .ox-title {
  font-size:var(--fs-title); font-weight:600; letter-spacing:-.025em; color:var(--cr-t0);
  margin-bottom:var(--space-1);
}
[data-theme="console"] .ox-sub, [data-theme="console-day"] .ox-sub { font-size:var(--fs-aux); }
[data-theme="console"] .ox-chip, [data-theme="console"] .ox-hero,
[data-theme="console-day"] .ox-chip, [data-theme="console-day"] .ox-hero {
  height:30px; padding:0 var(--space-3); font-size:var(--fs-aux); border-radius:var(--radius);
}
/* ★ NO GRADIENT AND NO BLUE. The app's primary button is a blue gradient; in this theme the only
   coloured pixels are states, so the one filled control on a page is a plain light-on-dark slab. */
[data-theme="console"] .ox-hero, [data-theme="console"] .btn-primary {
  background-color:var(--primary-dark); background-image:none;
  color:var(--cr-g0); box-shadow:none; font-weight:600;
}
/* background-COLOR then background-IMAGE, never the `background:` shorthand — the shorthand resets
   background-color to transparent, which is exactly how this button became white-on-white. */
[data-theme="console-day"] .ox-hero, [data-theme="console-day"] .btn-primary {
  background-color:#1d4ed8;
  background-image:linear-gradient(135deg,#3b82f6,#1d4ed8);
  color:#ffffff; font-weight:600;
}
[data-theme="console"] .ox-hero:hover, [data-theme="console"] .btn-primary:hover,
[data-theme="console-day"] .ox-hero:hover, [data-theme="console-day"] .btn-primary:hover {
  box-shadow:none; transform:none; opacity:.88;
}
[data-theme="console"] .ox-chip:hover, [data-theme="console-day"] .ox-chip:hover {
  border-color:var(--border-strong); color:var(--text);
}

/* ---- severity as an EDGE, not a wash. Through --stat-tint, the custom property the four
   .ox-stat.tint-* rules already yield to, so nothing is fought over and no page knows. ----------- */
[data-theme="console"] .ox-stat, [data-theme="console-day"] .ox-stat {
  --stat-tint:var(--card);
  position:relative; overflow:hidden; gap:var(--space-3);
  padding:var(--space-3) var(--space-4) var(--space-3) var(--space-4);
}
[data-theme="console"] .ox-stat::after, [data-theme="console-day"] .ox-stat::after {
  content:''; position:absolute; top:0; bottom:0; left:0; width:3px; background:var(--border);
}
[data-theme="console"] .ox-stat.tint-bad::after,  [data-theme="console-day"] .ox-stat.tint-bad::after  { background:var(--danger); }
[data-theme="console"] .ox-stat.tint-warn::after, [data-theme="console-day"] .ox-stat.tint-warn::after { background:var(--warning); }
[data-theme="console"] .ox-stat.tint-ok::after,   [data-theme="console-day"] .ox-stat.tint-ok::after   { background:var(--success); }
[data-theme="console"] .ox-stat.tint-info::after, [data-theme="console-day"] .ox-stat.tint-info::after { background:var(--text-muted); }
/* ★ MONO ON THE FIGURE. The signature of the theme: a number set in the measured face reads as a
   reading rather than as decoration. */
[data-theme="console"] .ox-stat b, [data-theme="console-day"] .ox-stat b {
  font-family:var(--font-mono); font-size:var(--fs-value); font-weight:600; letter-spacing:-.04em;
}
/* and small caps on the label beneath it */
[data-theme="console"] .ox-stat small, [data-theme="console-day"] .ox-stat small {
  font-family:var(--font-mono); font-size:var(--fs-label); font-weight:600;
 margin-top:var(--space-1);
}
[data-theme="console"] .ox-stat .ox-ic, [data-theme="console-day"] .ox-stat .ox-ic {
  background:transparent; width:30px; height:30px; border-radius:var(--radius-sm);
}
[data-theme="console"] .ox-stats, [data-theme="console-day"] .ox-stats {
  gap:var(--space-2); margin-bottom:var(--space-4);
  grid-template-columns:repeat(auto-fit,minmax(160px,1fr));
}
[data-theme="console"] .ox-stat:hover, [data-theme="console-day"] .ox-stat:hover {
  transform:none; border-color:var(--border-strong);
}

/* ---- tables: a hairline grid, small caps heads, mono figures --------------------------------- */
[data-theme="console"] .data-table th, [data-theme="console"] .admin-table th, [data-theme="console"] .form-table th,
[data-theme="console-day"] .data-table th, [data-theme="console-day"] .admin-table th, [data-theme="console-day"] .form-table th {
  font-family:var(--font-mono); font-size:var(--fs-label);
  font-weight:600; color:var(--text-muted); padding:var(--space-2) var(--space-3); background:var(--th-bg);
}
[data-theme="console"] .data-table td, [data-theme="console"] .admin-table td, [data-theme="console"] .form-table td,
[data-theme="console-day"] .data-table td, [data-theme="console-day"] .admin-table td, [data-theme="console-day"] .form-table td {
  font-size:var(--fs-body); padding:var(--space-2) var(--space-3); font-variant-numeric:tabular-nums;
}

/* ---- controls. Inputs are RECESSED — darker than the panel they sit on — so an editable field is
   visibly different from a printed one, which matters far more on a dark ground. ---------------- */
[data-theme="console"] .btn, [data-theme="console-day"] .btn {
  padding:var(--space-2) var(--space-3); font-size:var(--fs-aux);
}
[data-theme="console"] .btn-sm, [data-theme="console-day"] .btn-sm {
  padding:var(--space-1) var(--space-2); font-size:var(--fs-meta);
}
[data-theme="console"] .btn-secondary, [data-theme="console-day"] .btn-secondary {
  background:var(--cr-g3); border:1px solid var(--border-strong); color:var(--text-muted);
}
[data-theme="console"] .form-control, [data-theme="console"] .ds-sel,
[data-theme="console"] input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=file]):not([type=color]),
[data-theme="console"] select, [data-theme="console"] textarea,
[data-theme="console-day"] .form-control, [data-theme="console-day"] .ds-sel,
[data-theme="console-day"] input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=file]):not([type=color]),
[data-theme="console-day"] select, [data-theme="console-day"] textarea {
  background:var(--cr-g0); border:1px solid var(--border-strong); color:var(--text);
  padding:var(--space-2) var(--space-3); font-size:var(--fs-body);
}
[data-theme="console"] input::placeholder, [data-theme="console"] textarea::placeholder,
[data-theme="console-day"] input::placeholder, [data-theme="console-day"] textarea::placeholder {
  color:var(--text-muted);
}
[data-theme="console"] select option { background:var(--cr-g2); color:var(--text); }
[data-theme="console"] .form-label-sm, [data-theme="console-day"] .form-label-sm {
  font-family:var(--font-mono); font-size:var(--fs-label);
  color:var(--text-muted);
}
/* a badge is a state, so it is mono and small caps like every other state marker here */
[data-theme="console"] .badge, [data-theme="console-day"] .badge {
  font-family:var(--font-mono); font-size:var(--fs-label);
  padding:var(--space-1) var(--space-2); border-radius:var(--radius-sm);
}
[data-theme="console"] .ui-empty, [data-theme="console-day"] .ui-empty { padding:var(--space-6) var(--space-4); }
[data-theme="console"] .ui-empty-title, [data-theme="console-day"] .ui-empty-title { font-size:var(--fs-ui); }
[data-theme="console"] .ui-empty-hint, [data-theme="console-day"] .ui-empty-hint { font-size:var(--fs-aux); }
[data-theme="console"] .ui-empty-ico, [data-theme="console-day"] .ui-empty-ico {
  width:36px; height:36px; border-radius:var(--radius); margin-bottom:var(--space-2);
}

/* ---- DAY: NOT A PRINTED FORM ---------------------------------------------------------------
   ★ The night theme sets every micro-label in mono, uppercase, at .11em tracking. On a dark ground
   that reads as an instrument face. On white it reads as LETTERPRESS — a printed works order, not
   software — which is a mannerism I chose and nobody asked for. So the day variant drops it:
   labels go back to Inter, sentence case, no tracking, at a size you can actually read.
   ★ The FIGURES stay mono. That is the one place the mono is doing work rather than decorating —
   it is what makes a number read as a measurement, and it aligns columns of digits. */
[data-theme="console-day"] .ox-stat small,
[data-theme="console-day"] .nav-group,
[data-theme="console-day"] .data-table th, [data-theme="console-day"] .admin-table th,
[data-theme="console-day"] .form-table th,
[data-theme="console-day"] .form-label-sm,
[data-theme="console-day"] .badge,
[data-theme="console-day"] .sidebar-brand .brand-sub {
  font-family:inherit; text-transform:none; letter-spacing:0;
  font-size:var(--fs-aux); font-weight:600; color:var(--text-muted);
}
[data-theme="console-day"] .nav-group { font-size:var(--fs-meta); font-weight:600; }
[data-theme="console-day"] .ox-stat small { font-weight:500; }
/* the page's own micro-labels, same reasoning */
[data-theme="console-day"] .op-k,
[data-theme="console-day"] .op-mh span,
[data-theme="console-day"] .op-lbl {
  font-family:inherit; text-transform:none; letter-spacing:0;
  font-size:var(--fs-aux); font-weight:600;
}
[data-theme="console-day"] .op-v b { font-family:var(--font-mono); }

/* ---- THE VERB PILLS ON A DARK GROUND -----------------------------------------------------------
   Same three verbs, inverted: a deep tinted bed with a bright fore, the way every other state tint
   on these themes is built. Without this the DOA page shows three pale pastel chips on near-black,
   which is what the leak sweep found on /admin/doa. */
[data-theme="console"], [data-theme="black"] {
  --verb-rec:#c4b5fd;  --verb-rec-bg:#241a3a;
  --verb-con:#67e8f9;  --verb-con-bg:#0c2a31;
  --verb-inf:#fda4af;  --verb-inf-bg:#2e1119;
}

/* ============================================================================
   THE CHART LAYER (ui.chart) — see public/js/core/ui.js for the drawing rules.
   Only geometry and neutrals live here; every chart COLOUR is passed inline by
   the caller from a token, because a chart's colour carries meaning (on target /
   off target) and meaning is the page's to decide, not the stylesheet's.
   ============================================================================ */
.ui-spark { display:block; width:100%; height:38px; overflow:visible; }
.ui-ring, .ui-donut { display:block; flex:none; }

/* ★ THE S-CURVE (ui.chart.scurve, V2 part P3) - one scale. A series' KIND is geometry (dashed, solid, thin, dotted, a
   stepped area) and its TONE the caller's word - ink · ok · bad · warn · info · muted - each read from a theme token, so
   the meaning stays the page's and the colour stays the theme's. */
.ui-scwrap { width:100%; min-width:0; }
.ui-scurve { display:block; width:100%; height:auto; overflow:visible; }
.ui-scurve .sc-grid { stroke:var(--border); stroke-width:1; }
.ui-scurve .sc-base, .ui-scurve .sc-tick { stroke:var(--text-muted); stroke-width:1; }
.ui-scurve .sc-ax { fill:var(--text-muted); font-size:11px; font-variant-numeric:tabular-nums; }
.ui-scurve .sc-dash { stroke-width:2; stroke-dasharray:7 5; }
.ui-scurve .sc-solid { stroke-width:2.6; stroke-linejoin:round; stroke-linecap:round; }
.ui-scurve .sc-thin { stroke-width:1.4; stroke-linejoin:round; }
.ui-scurve .sc-dot { stroke-width:2.2; stroke-dasharray:1 6; stroke-linecap:round; }
.ui-scurve .sc-ink { stroke:var(--text); }
.ui-scurve .sc-ok { stroke:var(--success); }
.ui-scurve .sc-bad { stroke:var(--danger); }
.ui-scurve .sc-warn { stroke:var(--warning); }
.ui-scurve .sc-info { stroke:var(--primary); }
.ui-scurve .sc-muted { stroke:var(--text-muted); }
.ui-scurve path.sc-step { stroke:none; fill:var(--warning); fill-opacity:.16; }
.ui-scurve circle.sc-end { stroke:var(--card); stroke-width:2; }
.ui-scurve circle.sc-end.sc-ok { fill:var(--success); }
.ui-scurve circle.sc-end.sc-bad { fill:var(--danger); }
.ui-scurve circle.sc-end.sc-ink { fill:var(--text); }
.ui-scurve circle.sc-end.sc-info { fill:var(--primary); }
.ui-scurve .sc-today { stroke:var(--text); stroke-width:1; stroke-dasharray:3 3; opacity:.6; }
.ui-scurve .sc-todayl { fill:var(--text); font-weight:600; }
.ui-scurve .sc-bracket line { stroke:var(--danger); stroke-width:1.6; }
.ui-scurve .sc-bracket text { fill:var(--danger); font-size:11.5px; font-weight:700; }
.ui-scurve .sc-bracket.sc-ok line { stroke:var(--success-dark); }
.ui-scurve .sc-bracket.sc-ok text { fill:var(--success-dark); }
.ui-sclegend { display:flex; flex-wrap:wrap; gap:6px 16px; margin-top:8px; font-size:12px; color:var(--text-muted); }
.ui-sclegend span { display:inline-flex; align-items:center; gap:6px; }
.ui-sclegend .sw { display:inline-block; width:22px; height:0; border-top:2.6px solid currentColor; }
.ui-sclegend .sw.sc-dash { border-top:2px dashed currentColor; }
.ui-sclegend .sw.sc-thin { border-top-width:1.4px; }
.ui-sclegend .sw.sc-dot { border-top:2.2px dotted currentColor; }
.ui-sclegend .sw.sc-step { height:10px; border:0; border-radius:2px; background:var(--warning); opacity:.35; }
.ui-sclegend .sw.sc-ink { color:var(--text); }
.ui-sclegend .sw.sc-ok { color:var(--success); }
.ui-sclegend .sw.sc-bad { color:var(--danger); }
.ui-sclegend .sw.sc-info { color:var(--primary); }
.ui-sclegend .sw.sc-muted { color:var(--text-muted); }

/* The scale: fill is what was achieved, the tick is what is due by now. The tick is INK, not an
   accent — it is a reference mark, and colouring it would put it in competition with the fill,
   which is the thing that actually carries state. */
.ui-scale { margin-top:10px; }
.ui-scale-tr { position:relative; height:7px; border-radius:var(--radius-sm);
               background:var(--tint-neutral-bg, #f1f5f9); }
.ui-scale-fl { position:absolute; top:0; bottom:0; left:0; border-radius:var(--radius-sm);
               transition:width .3s ease; }
.ui-scale-tk { position:absolute; top:-4px; bottom:-4px; width:2px; border-radius:var(--radius-sm);
               background:var(--text, #1e293b); }
.ui-scale-cp { display:flex; justify-content:space-between; margin-top:6px;
               font-size:var(--fs-meta, 11px); color:var(--text-muted, #64748b);
               font-variant-numeric:tabular-nums; }
.ui-scale-cp span:nth-child(2) { color:var(--text-secondary, #334155); }
/* Overrides the space-between placement above ON PURPOSE, for the one caption that names the tick:
   the middle span leaves the flex flow and stands at the tick's own percentage (see ui.chart.scale).
   The two end spans keep space-between, so `lo` and `hi` still sit on the bar's ends. */
.ui-scale-cp.at { position:relative; }
.ui-scale-cp.at > .mid { position:absolute; top:0; white-space:nowrap; }
@media (prefers-reduced-motion: reduce) { .ui-scale-fl { transition:none; } }

/* The Queue floorplan's "Owed by you" line — one decision that has stopped on this reader. The
   count is the subject, so it leads; the verb closes it. Same row rhythm as .ox-row so the block
   sits with the list underneath it rather than looking bolted on. */
.wo-q { display:grid; grid-template-columns:auto 1fr auto; gap:0 12px; align-items:center;
        padding:12px 16px; border-top:1px solid var(--border); border-left:3px solid var(--st, var(--warning)); }
.wo-q:first-of-type { border-top:none; }
.wo-qn { min-width:34px; height:26px; padding:0 8px; border-radius:var(--radius-pill); display:inline-flex;
         align-items:center; justify-content:center; font-size:var(--fs-body, 13px); font-weight:600;
         font-variant-numeric:tabular-nums; }
.wo-qt { font-size:var(--fs-ui, 14px); font-weight:500; color:var(--text); }
.wo-qm { font-size:var(--fs-aux, 12px); color:var(--text-muted); margin-top:3px; line-height:1.5; }

/* ============================================================================
   GLASS — a full theme, not the [data-glass] axis.

   ★ THE AXIS FROSTS SURFACES ON WHATEVER THEME YOU ARE ALREADY ON, AND ON A
   WHITE PAGE WITH A WHITE SIDEBAR THAT IS INVISIBLE. Frost over white is white.
   Glass is not a surface treatment — it is a RELATIONSHIP between a surface and
   what is behind it, so the ground has to be worth seeing through to. This theme
   supplies that ground first and makes every surface translucent second.

   Light, not dark. The reference everybody means by "glassmorphism" is macOS and
   iOS: a soft coloured wallpaper, white chrome floating on it at 60-75% opacity,
   hairlines made of light rather than grey, and shadows that read as distance
   rather than as a drawn edge.
   ============================================================================ */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) {
  /* ★ THIS BLOCK SERVES BOTH GROUNDS; THE DARK BLOCK OVERRIDES ONLY WHAT DIFFERS. The first
     attempt kept it light-only "so dark could override it", which is backwards: these are
     SIBLING selectors, not nested, so glass-dark inherited none of the ~40 tokens declared here
     and fell through to the [data-glass] axis defaults. Measured: a production card came out
     WHITE at .66 on the dark theme. A variant must inherit the base it varies from.
     ★ THE FILM LADDER — SEVEN STEPS, AND EVERY TRANSLUCENT WHITE IN THIS THEME IS ONE OF THEM.
     Before this there were 116 hardcoded rgba(255,255,255,A) declarations across 44 DISTINCT
     alpha values — .3 and .30 and .34 and .36 and .38 all within a few percent of each other,
     which is drift, not intent. Two things were wrong with leaving them literal:
       · nothing could be adjusted once. Making panes denser meant finding 116 numbers.
       · A DARK VARIANT WAS IMPOSSIBLE. Glass on a dark ground is the identical recipe with a
         far THINNER film — .11 where this has .55 — so the literals pinned the theme to one
         ground. The ladder is what makes :is([data-theme="glass-dark"],[data-theme="liquid-dark"]) a token swap instead of a
         second 400-line copy that drifts away from this one by the third edit.
     f1 is a hairline, f4 is a pane, f7 is an overlay that must not be read through. */
  --gl-f1:rgba(255,255,255,.16); --gl-f2:rgba(255,255,255,.30);
  --gl-f3:rgba(255,255,255,.46); --gl-f4:rgba(255,255,255,.58);
  --gl-f5:rgba(255,255,255,.72); --gl-f6:rgba(255,255,255,.86);
  --gl-f7:rgba(255,255,255,.95);
  /* ★ THE LIP IS NOT A STEP ON THE FILL LADDER — a lesson taken from a real glass theme
     (cedmillion/heimdall-obsidian-glass), whose panes are DARK fills `rgba(10,10,10,.65)` while
     their border stays LIGHT `rgba(255,255,255,.12)` with a brighter top edge at `.25`. Fill and
     lip are two different families, and I had them sharing one ladder: on light that works
     because both are white, and on dark it cannot, because the fill must go dark while the lip
     must stay light. 25 inset highlights were riding the fill steps. */
  --gl-lip:rgba(255,255,255,.80);
  --gl-lip-side:rgba(255,255,255,.46);
  --primary:#3b82f6; --primary-dark:#1d4ed8;
  /* ★ THE FOUR "-light" TOKENS WERE THE BIGGEST SOURCE OF OPAQUE SLABS ON THIS THEME.
     --primary-light was re-declared here as the opaque #dbeafe, and --success-light,
     --warning-light and --danger-light were NOT DECLARED AT ALL — so they fell through to
     :root's opaque pastels. Anything painting with one became a printed sticker: measured, a
     769x52 card-header at #fee2e2 on /admin/account, nine #dbeafe labels across three admin
     pages, .mtag, .sld-dept, #env-badge, the form-guide totals. Same shape as the --tint-*
     miss and as --success-dark in CLAUDE.md: a theme that declares three of four token
     families has silently opted the fourth out of theming. */
  --primary-light:rgba(59,130,246,.20);
  --success-light:rgba(16,185,129,.20);
  --warning-light:rgba(245,158,11,.22);
  --danger-light:rgba(239,68,68,.20);
  /* ★ THE INK HAS TO CLEAR THE GROUND, NOT THE BASE COLOUR. Measured across 27 routes:
     --text-muted at #5f6775 came out at 2.21:1 on 345 elements — every tagline, scope line,
     counter and empty-state hint — because those sit DIRECTLY ON the ground, and the ground is
     not the flat #e7ebf7 the value was chosen against: its real luminance runs 0.17 to 0.63.
     The same probe on the `white` theme returns ZERO failures, so this is glass's own defect.
     Darkened until it clears the ground's LIGHTEST regions (~0.38 luminance): #3a3f4a gives
     about 4.6:1 there, and more everywhere else. It reads heavier than the light-theme grey and
     that is the correct trade — a tagline nobody can read is not subtle, it is missing. */
  --text:#141821; --text-secondary:#2b313c; --text-muted:#3a3f4a;
  /* The card is TRANSLUCENT WHITE, not white. Everything downstream that reads
     --card gets glass for free, which is why this is a theme and not a patch. */
  --card:var(--gl-f4);
  /* ★ --bg IS A PAINT COLOUR, NOT A PAGE GROUND. 73 rules read it as a fill — table zebra
     striping, row hover, .btn-secondary's face, the guidance panel. Setting it `transparent`
     did not 'let the gradient show through', it emptied all 73: .btn-secondary computed to
     rgba(0,0,0,0) and became an invisible button. A translucent white is the correct value —
     the ground still reads through it, and everything that wanted a neutral fill gets one. */
  --bg:var(--gl-f2);
  --border:var(--gl-f4);         /* a hairline of LIGHT, not of grey */
  --border-strong:var(--gl-f5);
  --th-bg:var(--gl-f3);
  --tint-neutral-bg:var(--gl-f3);
  /* ★ THE TINT FAMILY, TRANSLUCIFIED — AND SCOPED TO THIS THEME, WHICH IS THE POINT.
     These were never declared here, so every surface reading one got :root's OPAQUE hex and
     became a printed sticker on a pane of glass: measured rgb(254,243,199) at alpha 1 on the
     Operations status pills, a 1132x202 `.sd-mine` panel at rgb(239,246,255) on Shutdowns, and
     `.ui-find-sev.warn` across four module pages.
     ★ AND THE FIRST ATTEMPT AT THIS FIX LANDED IN THE BASE `:root` BY MISTAKE — anchored on the
     first `--tint-neutral-bg` in the file without checking it was unique, and the first one is
     the base's. That silently retinted warning, danger and success for ALL THIRTEEN THEMES: on
     white/flat/black a warning badge became a translucent wash instead of its solid pastel.
     Caught by a page sweep, not by reading the diff. The lesson is the same one the
     `--primary:#3b82f6 … --primary-light:#dbeafe` line taught an hour earlier — that line also
     appears twice, in two different themes. In a 7,000-line stylesheet with 13 themes, NEVER
     anchor an edit on a declaration without proving there is only one of it. */
  --tint-warn-bg:rgba(245,158,11,.18); --tint-danger-bg:rgba(239,68,68,.17);
  --tint-ok-bg:rgba(16,185,129,.17);   --tint-info-bg:rgba(59,130,246,.16);
  --tint-hot-bg:rgba(249,115,22,.20);
  /* the foreground half, so the family is complete on this ground too — these tints are
     translucent here, so the ink sits on the page's own light ground rather than on a pastel */
  /* ★ AND THESE SIT ON THE GROUND TOO, because the tint fills are translucent on this theme —
     a .18 wash does not isolate its ink from what is behind it. Measured: tint-ok 2.63,
     tint-warn 2.87, tint-info 3.85, tint-danger 3.28 across 36 instances on 13 routes. Taken
     down to values that clear the ground rather than values that clear a pastel. */
  --tint-neutral-fg:#2f353f; --tint-info-fg:#0b2f6b;
  --tint-ok-fg:#064e3b;      --tint-warn-fg:#6b3a04;
  --tint-danger-fg:#7f1616;  --tint-hot-fg:#6e2c08;
  --sidebar-bg:var(--gl-f2);
  --sidebar-hover:var(--gl-f5);
  --sidebar-text:#4a5260; --sidebar-active:#0f1420;
  --nav-active-bg:rgba(59,130,246,.14);
  --radius:16px; --radius-sm:11px;
  /* ★ .55 WAS TOO DENSE ONCE THE CANVAS LAYER WENT. A pane only reads as glass if the ground
     survives it; measured, a card at .55 over this ground carried std(R) of about 1 in its body
     and looked like white card stock. .44 lets the colour through and still holds text at well
     over the contrast floor, because the ink in this theme is opaque (#161a22), not alpha. */
  --glass-bg:var(--gl-f3); --glass-strong:var(--gl-f5);
  /* ★ A FLOATING OVERLAY IS NOT A DENSE PANE — IT NEEDS ITS OWN DENSITY. --glass-strong is .72,
     which is right for an in-page surface sitting on the ground, and wrong for a menu hanging
     over LIVE CONTENT: at .72 the rows underneath stay legible through it and the two layers
     merge, which is what Vijay reported for the notification panel and the popovers. An overlay
     has to win against whatever it covers, so it is a separate token at .93 — still glass
     (you can see the blur move under it), but no longer transparent enough to read through.
     --over-bg / --over-blur are the CHANNEL for surfaces styled INLINE and therefore
     unreachable from any stylesheet: #notif-panel in views/layouts/main.ejs is one, the same
     way the toasts in public/js/core/api.js were. The fallbacks keep the other 12 themes
     byte-identical. */
  --glass-over:var(--gl-f7);
  /* ★ A MENU NESTED INSIDE A backdrop-filtered ANCESTOR CANNOT HIDE THAT ANCESTOR'S CONTENT.
     `.cacct-pop` and `.nav-fly` live inside `.sidebar`, which has a backdrop-filter and is
     therefore a BACKDROP ROOT: a filter on a descendant samples what is behind the ROOT — the
     page behind the rail — never the nav rows painted inside it. Measured: the rail's own labels
     ("Production", "Operations", "STORES") stayed legible straight through a surface computing
     at 93% white, and forcing the fill opaque stopped it dead. So these two get no translucency;
     they keep the glass EDGE (hairline, lip, shadow), which is what makes them belong to the
     theme. A menu covering a menu should not be see-through anyway. */
  --menu-bg:#fbfcff;
  --over-bg:var(--glass-over); --over-blur:saturate(1.3) blur(26px);
  /* ★ THIS TOKEN IS A WHOLE FILTER VALUE, NOT A LENGTH. Ten shared rules elsewhere in this file
     read it as `backdrop-filter: var(--glass-blur)`, so `22px` substituted to
     `backdrop-filter: 22px` — invalid, and the browser drops the declaration silently. Every
     surface the theme did not name itself was therefore NOT frosted at all. */
  /* ★ NOT `saturate()`. MEASURED: with saturate(1.5) the blue channel inside a .card was
     mean 255.0, std 0.00, and 100% OF PIXELS SAT AT B=255 — while the raw ground's own blue
     std is 8.42. The two dominant pools are rgba(56,189,248) and rgba(99,102,241): under a
     34–55% white pane the blue is already 236–250, so any saturate(k>1.05) overshoots 255 and
     CLIPS. Clipping is irreversible, so every pane was pinned to one flat pale-cyan wash
     no matter what was behind it — a coloured panel, which is exactly what Vijay kept saying.
     contrast() lifts vibrancy by pushing away from mid-grey instead of scaling toward the
     channel ceiling, so it keeps the variation the blur then has something to spread.
     ★ ACCEPTANCE TEST, not an opinion: inside a card and inside the rail, share(B==255) must
     be 0% and std(B) must exceed 0.5. Re-measure before changing these two numbers. */
  --glass-blur:contrast(1.07) saturate(1.1) blur(30px); --glass-brd:var(--gl-f4);
  /* ★ THE SWATCH CANNOT BE MEASURED FOR THIS THEME. The picker builds each tile by probing the
     theme's own tokens, and glass keeps --bg `transparent` because its ground is a four-stop
     radial wash on .app-layout. A probe would read blank. So the theme states what its tile
     should show, and the picker reads THAT — the description stays with the thing it describes
     rather than becoming a literal in a view that can never re-theme. */
  --swatch-bg:linear-gradient(135deg,#dfe4fb 0%,#dbeafe 45%,#f6dcec 100%);
  --swatch-card:var(--gl-f5);
  --shadow:0 2px 10px rgba(23,33,61,.06);
  --shadow-md:0 8px 26px rgba(23,33,61,.10);
  --shadow-lg:0 18px 48px rgba(23,33,61,.14);
}

/* ★ THE GROUND. Four soft radial washes, not one linear gradient — a single
   diagonal ramp reads as a cheap header banner, while overlapping pools of
   colour give the blur something that CHANGES across the screen, which is what
   makes glass look like glass when you scroll. Fixed so it does not travel. */
/* ★ THE GROUND IS A FIXED LAYER, NOT A BACKGROUND ON BODY. `background-attachment: fixed` paints
   a viewport-sized image and then does not repeat down a long document — so on the Operations page
   Kiln 1 sat in colour and Kiln 3 sat on bare white, because the gradient had simply run out. It is
   also the property the review flagged as expensive and badly behaved on iOS Safari.
   A `position: fixed` pseudo-element is the correct instrument: it is always exactly the viewport,
   whatever the page height, so every screenful gets the whole wash — which is how macOS and iOS
   behave when you scroll a window over a wallpaper. */
/* ★ THE GROUND WAS NEVER ON SCREEN. `.app-layout` carried this as an OPAQUE colour while the
   ground lives on `.app-layout::before` at `z-index:-1` — and a negative-z-index pseudo-element
   paints BEHIND its own parent's background. So the flat #e7ebf7 covered all nine gradients, every
   pane in the app was blurring a UNIFORM FIELD, and a blur of a uniform field returns that same
   field. Measured: the rail computed to white however its opacity was tuned, which is why three
   passes at the sidebar changed nothing. Same class of fault as the smooth-gradient blur earlier —
   a backdrop-filter with nothing behind it to filter.
   The base colour stays on `body` (so there is never a white flash before paint) and the layout
   itself is transparent, which is what lets the ::before through. */
/* ★ THE GROUND HANGS OFF body, NOT .app-layout — BECAUSE 32 OF THE 44 PAGES THAT EMIT THEIR
   OWN <html> HAVE NO .app-layout AT ALL. Measured: /group/login, /group/request, /group/help
   and /platform/wiring/live reported "(no .app-layout)", so the entire ground — all sixteen
   gradients — could never exist on them, and their frosted panes were paying for a blur while
   blurring a UNIFORM FIELD. That is the same no-op this theme was already repaired for twice.
   ★ AND THE BASE COLOUR MOVES UP TO <html> FOR THE SAME REASON IT LEFT .app-layout: a
   z-index:-1 pseudo-element paints BEHIND ITS OWN PARENT'S BACKGROUND, so an opaque body
   would occlude body::before exactly as the opaque .app-layout occluded its own. html holds
   the colour, body is transparent, the pseudo-element is visible. */
/* ★ `[data-theme=…] html` NEVER MATCHED — html cannot be a descendant of itself. The attribute
   sits ON the html element, so the selector must BE it. Written during the move of the ground
   onto body::before, so this base colour has in fact been absent the whole time; nothing showed
   because body::before covers the viewport. It matters the moment that layer is switched off —
   print, reduced-transparency, or Glass effects → Off. */
html[data-theme="glass"] { background: #e7ebf7; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) body { background: transparent; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout { background: transparent; }


/* Every surface that used to be a white box is now a pane. `saturate` is not
   decoration: blurring alone greys the colour underneath, and lifting saturation
   back is what stops frosted panes looking dirty. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .card,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .topbar,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .modal-dialog,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-cardpop,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .apx-modal,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .wl-fig,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .mini {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--shadow-md), inset 0 1px 0 var(--gl-lip);
}
/* ★ THE INSET HIGHLIGHT IS THE WHOLE TRICK. One pixel of white along the top
   edge is light catching the lip of a pane of glass. Without it a translucent
   box reads as a faded card; with it, it reads as a physical object. */

/* ★ A BLUR NEEDS EDGES. This is the finding that mattered most and I had it exactly wrong: a
   Gaussian blur of a smooth gradient returns THE SAME GRADIENT — convolving a symmetric kernel with
   a linear function is an identity operation. Measured before this existed: the pane at the centre
   of the viewport computed to #f2f6ff with backdrop-filter ON and #f2f6ff with it OFF. Every pane
   was paying a full-screen compositing pass for nothing, and the "frost" was doing no work at all;
   the 55% white tint was the entire effect.
   Apple blurs a PHOTOGRAPH. iOS Control Centre blurs the home-screen ICON GRID. Windows Acrylic
   samples the desktop. All three are full of hard edges at 20–200px — which is the only frequency
   band a 30px blur can act on. So the ground gets edges of its own: hard-stopped discs, `0 46%,
   transparent 47%`, and that 1% cliff is the whole point of the layer. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) body::before {
  /* ★ z-index -1, NOT 0, AND NO RULE ON THE CHILDREN. The first version paired this with
     `.app-layout > * { position:relative; z-index:1 }` to lift content above it — and that turned
     the FIXED sidebar into a flow element, so it took 260px of space while .main-content still
     carried its 260px margin-left. The content was pushed twice and a band of bare gradient opened
     between the rail and the page. A fixed pseudo-element at -1 sits behind everything already;
     nothing else has to move. */
  content:''; position:fixed; inset:0; z-index:-1; pointer-events:none;
  background:
    /* the hard-edged discs the blur actually eats — listed FIRST so they sit above the wash.
       ★ SMALL AND STRONG, NOT BIG AND FAINT. Once the canvas blur layer came out (it was
       pre-flattening the ground and leaving every card's own blur nothing to work on), these
       became directly visible in the gutters BETWEEN cards and read as flat circles on the
       page. Halving the radii and raising the alpha keeps the edge a blur can bite on while
       making each disc small enough to sit under a card rather than in a 20px gap. A disc is
       an instrument, not decoration: it should only ever be seen THROUGH something. */
    /* ★ NO MORE DISCS ON THE LIGHT GROUND EITHER. This is the same correction the dark ground
       already carries, and the reason is written out at its own body::before: hard-stopped
       circles read as "four coloured circles parked on the page" the moment one lands in a
       GUTTER between cards instead of under one. Vijay said it about dark and then said it
       about light — pink at the top, blue mid-right, peach bottom-right, all plainly visible
       between the panes. A disc is an instrument, not decoration: it should only ever be seen
       THROUGH something, and nothing on this page guarantees that.
       ★ THE REQUIREMENT IS AN EDGE, NOT A CIRCLE. A blur of a smooth gradient is the identity
       operation, so the ground has to carry a step change the kernel can spread — but raked
       DIAGONAL BANDS satisfy that everywhere at once instead of only where a disc happens to
       sit, and they read as light across a surface, which is what glass actually does. Two band
       sets at different angles and periods so the pattern never repeats visibly, plus a fine
       grid for the high-frequency detail the blur turns into sheen.
       ★ AND THEY ARE STRONGER THAN DARK'S. Dark uses rgba(150,160,255,.05); the same alpha over
       a #e4e9f7 ground is below the threshold measured earlier on the rail discs — 10 units of
       255, which left the rail rendering IDENTICALLY at 0px, 40px and 90px of blur. On a light
       ground the band has to be DARKER than what it crosses to be a step at all. */
    repeating-linear-gradient(115deg,
      rgba(79,70,229,.075) 0 2px, transparent 2px 190px),
    repeating-linear-gradient(64deg,
      rgba(14,165,233,.065) 0 2px, transparent 2px 260px),
    repeating-linear-gradient(0deg,
      rgba(70,80,140,.026) 0 1px, transparent 1px 84px),
    repeating-linear-gradient(90deg,
      rgba(70,80,140,.026) 0 1px, transparent 1px 84px),
    /* ★ THREE DISCS IN THE RAIL'S OWN COLUMN — AND THE COLUMN IS 18%, NOT 15%.
       `--sidebar-width` is 260px of 1440 = 18.1%. The first placement of these three used a
       215px/15% figure and put all three in the rail's left half, leaving x≈180–260 — the strip
       nearest the seam, which is the part you actually read — covered only by one clipped arc.
       Measured disc coverage of the rail band was 3.76% of its area.
       ★ AND THEIR AMPLITUDE WAS BELOW THRESHOLD. Measured on the raw ground, disc 1's cliff
       stepped (147,157,232) → (157,170,230): 10 R / 13 G units out of 255. Consequence: the rail
       rendered IDENTICALLY at blur 0px, 40px and 90px — 0.6 RGB apart — so the blur term was
       inert and every pass that tuned it was tuning nothing. A blur needs edges that are
       actually edges, so these carry roughly double the alpha of the content-area discs. The five above are at x = 18%, 47%, 72%, 31%, 88%;
       the rail is 0 → 216px of 1440, i.e. 0 → 15%. EVERY EXISTING DISC IS OUTSIDE THE RAIL, so
       behind it there were only the two soft pools at -6% and -4% — smooth, and a blur of a smooth
       gradient is the identity operation. The rail's backdrop-filter has therefore been doing
       nothing at ANY opacity, which is why three passes at its transparency changed nothing.
       Measured after the occlusion fix: rail rgb(192,216,255) vs bare ground rgb(193,216,255) one
       pixel to its right — one unit. A blur needs edges, and the edges have to be where the glass is. */
    /* ★ THESE FOUR STAY, AND THE FIVE CONTENT DISCS DID NOT — THE DIFFERENCE IS WHETHER A DISC
       CAN EVER BE SEEN. They sit at x = 2%, 5%, 15% and 16% of 1440, i.e. inside 230px, and the
       rail is 260px wide and always drawn: nothing here can ever surface in a gutter between
       cards, which is the whole reason the other five had to go. A disc is an instrument, not
       decoration — and these are the only ones on the page guaranteed to be seen THROUGH
       something.
       ★ AND REMOVING THEM MEASURABLY KILLS THE RAIL'S BLUR. Taking them out alongside the
       content discs looked tidy and was wrong: with only the bands behind it the rail's column
       carried 10.3 RGB units of horizontal amplitude — the same figure recorded above as the
       threshold at which nothing happens — and the rail then rendered 0.34 / 0.40 / 0.63 units
       apart at blur 14px / 40px / 90px. That is the inert state this comment was written about,
       reintroduced. The bands are viewport-wide and help everywhere, but they are 2px lines at
       .075 alpha; the rail needs a cliff, not a hairline. */
    /* ★ AND THEY HAVE TO FIT INSIDE THE RAIL, WHICH IS A STATEMENT ABOUT RADIUS, NOT POSITION.
       Two of these were at 15% and 16% of 1440 — 216px and 230px, comfortably inside the 260px
       rail as CENTRES, which is what was checked. But the colour stop sits at 46% of the ending
       shape, so a `210px` disc paints a visible radius of ~97px and reaches 313px; the one at
       16% reached 322px. Both put a crescent 50-60px into the content gutter, right against the
       seam — a blue one beside Trade Library and a magenta one beside Assets, exactly the flat
       circles the other five were removed for. Vijay spotted them in the first screenshot he
       took of the finished theme.
       Centre plus 0.46 x radius must stay under ~255px. It does now, for all four. */
    radial-gradient(190px 190px at  5% 14%, rgba(79, 70,229,.62) 0 46%, transparent 47%),
    radial-gradient(210px 210px at 11% 38%, rgba(14,165,233,.54) 0 46%, transparent 47%),
    radial-gradient(170px 170px at  2% 63%, rgba(139,92,246,.52) 0 46%, transparent 47%),
    radial-gradient(200px 200px at 10% 86%, rgba(219,39,119,.48) 0 46%, transparent 47%),
    /* …and the four soft pools beneath them, pulled inward so the middle of the screen is not dead */
    radial-gradient(1200px 900px at 22%  4%, rgba(99,102,241,.44), transparent 100%),
    radial-gradient(1000px 820px at 84% 14%, rgba(56,189,248,.38), transparent 100%),
    radial-gradient(1000px 900px at 66% 90%, rgba(236,72,153,.34), transparent 100%),
    radial-gradient( 950px 800px at 12% 82%, rgba(45,212,191,.34), transparent 100%),
    /* ★ AND A POOL ANCHORED OFF THE LEFT EDGE. The rail is the leftmost 215px — about 15% of the
       screen — and every pool above starts at 12% or further in, so behind the sidebar the ground
       was almost bare linear wash. Frosted white over near-white IS white: that is why the menu
       read as a plain panel while the content area looked like glass. Glass needs something to
       refract, so the colour has to be put where the glass is. */
    radial-gradient(760px 1100px at -6% 26%, rgba(99,102,241,.58), transparent 100%),
    radial-gradient(620px  900px at -4% 88%, rgba(56,189,248,.44), transparent 100%),
    linear-gradient(160deg,#e4e9f7 0%,#dde6f6 55%,#e6e1f4 100%);
}

/* ★ AND THE TINT STAYS. The blanket rule above is (0,2,0), the same as `.ox-stat.tint-bad`, and it
   sits 3,192 lines later — so it won, and every summary tile became one neutral pane. On the
   permits page the "8 Expired" tile rendered identical to "14 Total permits" beside it. Colour is
   the one thing this product is asked for and the theme deleted it. `--stat-tint` is the escape
   hatch the tint rules already yield to, so the theme re-points rather than fights, and the tints
   are restated as translucent vibrancy — an opaque #fee2e2 on glass reads as a sticker. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat { background: var(--stat-tint, var(--glass-bg));
                                border-color: var(--stat-brd, var(--glass-brd)); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat.tint-info { --stat-tint:rgba(59,130,246,.22); --stat-brd:rgba(59,130,246,.44); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat.tint-warn { --stat-tint:rgba(245,158,11,.24); --stat-brd:rgba(245,158,11,.48); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat.tint-bad  { --stat-tint:rgba(239,68,68,.22);  --stat-brd:rgba(239,68,68,.46); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat.tint-ok   { --stat-tint:rgba(16,185,129,.22); --stat-brd:rgba(16,185,129,.44); }

/* ★ 71 RULES READ `background: var(--card)` AND ONLY NINE WERE FROSTED. Making --card translucent
   theme-wide meant every dropdown, flyout, skeleton and the whole Operations module became 58%
   see-through with UNBLURRED page content behind it — a legibility bug, not a look. Measured on the
   Operations page: --text-muted on the raw ground came out at 3.31:1, under the 4.5:1 floor. */
/* ★ #notif-panel AND #comms-menu WERE NOT ON THIS LIST. The bell panel is styled inline, so it
   also needs the --over-bg channel opened above; adding it here gives it the border, the shadow
   and the lip. Everything that FLOATS now takes --glass-over, not --glass-strong. */
/* ★ AND `.op-machine`, `.ui-skelcard` AND `#comms-menu` ARE NOT OVERLAYS — REMOVED FROM THIS
   LIST. This is the fault Vijay was pointing at when he said the production cards are plain
   white slabs damaging the theme: `.op-machine` was in this list, so when overlays were given
   their own density (.93, correct for a menu floating over live content) EVERY PRODUCTION CARD
   became 93% opaque white. Measured var(--gl-f7) on the Kiln cards. They are in-page
   panes and belong to the `--glass-bg` recipe, which they were already in — this rule sits
   later and was quietly overriding it.
   `#comms-menu` is the "why is there a card behind the buttons" — it is a CONTAINER of buttons,
   so by §26 it takes a boundary and no fill; the `.comms-item` pills are the objects.
   The list that remains is only things that genuinely float over live content. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ds-pop, .ui-expmenu,
                         .mobile-menu-fab, .help-panel, #help-panel,
                         #notif-panel, #notif-list) {
  background: var(--glass-over);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--shadow-lg), inset 0 1px 0 var(--gl-lip);
}

/* ★ A SEPARATOR IS A BORDER, NOT A FILL. This read --gl-f3 — the CARD step of the film ladder —
   so the line between two rows was the same colour family as the pane it sits on. Measured against
   the row body either side: 1.07:1 on light and 1.002:1 on dark, the latter a difference of 2/255
   in one channel, against the `white` theme's 1.178:1 for the identical markup. The list rendered
   as one unbroken block of text. The ladder has a border family (--glass-brd) for exactly this. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-row { background: transparent; border-color: var(--glass-brd, var(--border)); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-row:hover { background: var(--gl-f3); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-tabs2 { background: var(--gl-f2); border-bottom-color: var(--gl-f4); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .form-control,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-search input {
  background: var(--gl-f5); border-color: var(--gl-f6);
}
/* The one thing that must NOT be glass. A primary action people press has to be
   solid — a button you can see through is a button nobody trusts. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .btn-primary,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-hero {
  background-color: var(--primary-dark);
  /* ★ THE GRADIENT'S LIGHT END CANNOT CARRY WHITE. Measured on every primary button and hero in
     the app: 2.54:1 on dark (--primary #60a5fa) and 4.35:1 on light (#3b82f6) — the Appearance
     Save button among them, on all 22-27 routes. White needs 4.5, and white on --primary alone
     is 3.68 even at its best. Capping the light end at 55% primary mixed toward primary-dark
     puts the worst pixel at 4.79:1 and the dark end at 6.70:1 — the same cap the sidebar
     selection already uses, so the two agree. Do NOT lighten this back. */
  background-image: linear-gradient(135deg,
      color-mix(in srgb, var(--primary) 55%, var(--primary-dark)), var(--primary-dark));
  box-shadow: 0 6px 18px rgba(59,130,246,.34);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
/* Every other place white sits on a flat --primary fill: same 3.68 failure, same answer. */
/* ★ NOT --primary-dark. MEASURED ON GLASS DARK: rgb(59,130,246) under white ink = 3.68:1 — the
   very failure the line above says it is answering, because on a DARK theme the primary ladder
   flips and --primary-dark becomes the LIGHT theme's --primary (#1d4ed8 -> #3b82f6). The token
   means "the far stop of the accent gradient", not "a fill dark enough to carry white ink", and
   the two part company the moment a theme inverts. --accent-solid is the second thing, declared
   per theme and NEVER derived from the ladder: #1d4ed8 on the Glass pair (6.70:1 in both modes),
   #0e7490 on the Liquid pair (5.36:1).
   ★ It is NOT --accent-ink. That token already exists and means the OPPOSITE — ink laid ON a dark
   ground (#a8c8ff on Glass Dark, #67e8f9 on Liquid Dark, used as `color:` at §30/§30b). Reusing
   the name put a pale blue fill under white text and measured 1.4:1, worse than the defect. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.skip-link, .brand-icon, .ox-page,
      .apx-seg > button.on, .rec-stage.joined, .cal-num.on, .cacct .cav) {
  background-color: var(--accent-solid);
}
/* ★ A DISABLED PRIMARY IS opacity:.5 OVER AN OPAQUE GRADIENT — measured 2.06 on "Upload" and
   2.31 on "Shift handover". Halving the opacity of a saturated fill halves the CONTRAST too, so
   a disabled control becomes unreadable rather than merely quiet. It gets a flat quiet surface
   and quiet-but-legible ink instead of a faded loud one. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.btn-primary, .ox-hero):disabled,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.btn-primary, .ox-hero)[disabled] {
  opacity: 1;
  background-color: var(--gl-f5); background-image: none;
  color: var(--text-muted); box-shadow: none;
  border: 1px solid var(--border);
}
/* ★ AND IT MUST NOT EAT THE SELECTED STATE. This rule is (0,2,0); every page paints its active
   chip with a plain class — `.pr-chip-on { background: var(--primary); color:#fff }` at (0,1,0),
   and 14 more like it across app.css and the views. So this won, the accent was replaced by 62%
   white, the page's `color:#fff` stayed, and the selected chip became WHITE ON WHITE: measured
   1.00:1 on Production's K1 machine selector — invisible, not just low contrast.
   ★ THIS IS THE THIRD TIME THE SAME MISTAKE HAS SHIPPED IN THIS THEME: a broad glass rule at
   (0,2,0) silently beating a semantic state at (0,1,0). It ate the Appearance Save button, then
   the sidebar's severity tints, now every segmented control in the app. A neutral rule has to
   say which elements are neutral. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-chip:not(.on):not(.active):not(.is-on):not(.sel):not([class*="-on"]):not([aria-selected="true"]) {
  background: var(--gl-f4); border-color: var(--gl-f5);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
}
/* ★ THE SELECTED CHIP IS FROSTED ACCENT — THE SAME THING VIJAY APPROVED FOR THE SIDEBAR (S2),
   NOT SOLID BLUE. My first fix here made it solid --primary-dark, which contradicted a choice he
   had already made an hour earlier: shown both, he picked the frosted variant and said "not
   blue". I reached for solid because the pages pin `color:#fff` on these chips and white cannot
   sit on a pale frosted fill — but S2's answer to that is to change the INK, not the fill, and
   that is exactly what the sidebar rule does. One selection language for the whole app.
   ★ THE INK OVERRIDE IS LOAD-BEARING. White on this fill measures about 1.4:1; --text measures
   ~10:1. Never restore `color:#fff` here without also restoring a solid fill. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ox-chip, .oxseg > button, .ox-views > button):is(.on, .active, .is-on, .sel, [class*="-on"], [aria-selected="true"]) {
  background-color: transparent;
  background-image: linear-gradient(180deg, rgba(59,130,246,.36), rgba(99,102,241,.26));
  -webkit-backdrop-filter: saturate(1.4) blur(12px); backdrop-filter: saturate(1.4) blur(12px);
  color: var(--text);
  font-weight: 600;
  border: 1px solid rgba(59,130,246,.52);
  box-shadow: inset 0 1px 0 var(--gl-lip), var(--el-1);
}

/* ★ AND "How this works" IS NOW IN THE ACTION ROW, SO IT MUST BE THE SAME SIZE AS THE ROW.
   `.khtfw-tab` is `padding:7px 13px; font-size:.82rem` — it was authored to stand alone above
   the title, where nothing sat beside it. `.ox-chip` is `height:40px; padding:0 15px;
   font-size:14px; border-radius:10px`. Seated together they were visibly two different
   controls, which is what Vijay meant by the button sizes not being consistent. It takes the
   row's geometry and keeps its own tint. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-acts .khtfw-tab,
.ox-acts .khtfw-tab {
  height: 40px; padding: 0 16px; border-radius: var(--radius-sm);
  font-size: 14px; line-height: 1; gap: 6px;
}

/* ★ OFF MUST MEAN OFF. The theme block is appended AFTER the [data-glass] axis at equal
   specificity, so `Appearance → Glass effects → Off` did nothing on this theme — verified: the
   tokens came back byte-identical. Somebody who cannot tolerate transparency had no way out of the
   one theme built entirely from it. These are (0,2,0) and settle it whatever the order. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] {
  --card:#ffffff; --bg:#f4f6fb; --border:#dfe3ee; --border-strong:#c9cfe0;
  --th-bg:#eef1f8; --tint-neutral-bg:#eef1f8;
  --sidebar-bg:#ffffff; --sidebar-hover:#eef1f8;
  --glass-bg:#ffffff; --glass-strong:#ffffff; --glass-brd:#dfe3ee;
  --glass-blur:none;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] body,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] .app-layout { background:#f4f6fb; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] .app-layout::before { display:none; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] :is(.card,.sidebar,.topbar,.modal-dialog,.ox-cardpop,
  .apx-modal,.ox-stat,.wl-fig,.mini,.ds-pop,.ui-expmenu,.nav-fly,.cacct-pop,.side-user-menu,
  .ui-skelcard,.op-machine,.ox-drawer){ -webkit-backdrop-filter:none; backdrop-filter:none; }

/* ★ AND THE OPERATING SYSTEM'S OWN ANSWER OUTRANKS THE THEME. A person who has asked their machine
   to reduce transparency has asked every application, not just the ones that remember to check. */
@media (prefers-reduced-transparency: reduce) {
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) {
    --card:#ffffff; --bg:#f4f6fb; --border:#dfe3ee; --glass-bg:#ffffff;
    --glass-strong:#ffffff; --glass-brd:#dfe3ee; --glass-blur:none;
  }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) body, :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout { background:#f4f6fb; }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) body::before { display:none; }
}

/* ★ HIGH CONTRAST WAS BYTE-IDENTICAL TO NORMAL ON THIS THEME — measured, the tokens did not move,
   because [data-contrast] is (0,1,0) and the theme is too, and the theme is later in the file. The
   axis exists for people who need it and this theme silently switched it off. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-contrast="high"] {
  --card:var(--gl-f6); --glass-bg:var(--gl-f6);
  --glass-strong:var(--gl-f7); --glass-brd:rgba(80,90,120,.34);
  --text:#0a0d14; --text-secondary:#242a36; --text-muted:#3d4450;
  --border:rgba(80,90,120,.34); --border-strong:rgba(80,90,120,.5);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-contrast="xhigh"] {
  --card:var(--gl-f7); --glass-bg:var(--gl-f7);
  --glass-strong:#ffffff; --glass-brd:#5a6478;
  --text:#000000; --text-secondary:#151a22; --text-muted:#2b313c;
  --border:#5a6478; --border-strong:#3d4450;
}

/* ★ A HAIRLINE OF WHITE ON A 96%-WHITE PANE IS NOT A HAIRLINE. Measured at 1.20:1 against the pane
   it was meant to divide, so every border in the app vanished. Glass edges are light AND shadow: a
   white lip catching the top, and a faint dark line everywhere else so the shape still has an edge
   where the light does not fall. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) { --border:rgba(90,100,130,.20); --border-strong:rgba(90,100,130,.32); }
/* A fill that carries WHITE ink, in either mode. Fixed on purpose rather than derived from
   --primary/--primary-dark: those two swap places between the light and dark halves of a theme,
   and white ink does not. 6.70:1. (Liquid Glass overrides it to its own teal below.) */
:is([data-theme="glass"],[data-theme="glass-dark"]) { --accent-solid:#1d4ed8; }
/* On a dark theme --primary-dark flips BRIGHT (it is the ink), so it cannot be the fill under white
   text: Black's #3b82f6 measured 3.7:1, Console's #c8d0da 1.7:1. Each names a real solid. */
[data-theme="black"] { --accent-solid:#1d4ed8; }
[data-theme="console"] { --accent-solid:#3a4656; }
/* the primary-button gradient starts on the solid where --primary is too pale under white text */
:is([data-theme="forest"],[data-theme="black"],[data-theme="console"],[data-theme="glass-dark"],[data-theme="liquid-dark"]) { --btn-grad-from: var(--accent-solid); }
[data-theme="console"] .sidebar-brand .brand-icon { background: var(--cr-g3); color: var(--text); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) { --accent-solid:#0e7490; }
/* see --primary-ink in :root — this theme's --primary-dark is an indigo gradient end, not an ink. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) { --primary-ink: var(--primary); }
/* ★ ON THE LIGHT HALF THE ACCENT IS NOT ENOUGH FOR INK. --primary (#0e7490) is 5.36:1 on a white
   card, but almost nothing on this theme IS a white card — a link inside a translucent chip sits on
   a composite of the three blurred orbs, measured rgb(169,209,218) for "Open all"/"Open my
   department" on /home: 3.27:1. One token, and every one of the ~100 --primary-ink sites follows.
   Dark half untouched: there --primary is a bright cyan on a dark pane. */
[data-theme="liquid"] { --primary-ink: var(--accent-ink-deep); }

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* ★ WITHOUT THE BLUR, TRANSLUCENCY IS JUST NOISE UNDER TEXT. A browser that cannot frost must get
     opaque panes, not faint ones — otherwise the page is unreadable rather than merely plainer. */
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) { --card:#ffffff; --glass-bg:#ffffff; --glass-strong:#ffffff;
                         --bg:#f4f6fb; --sidebar-bg:#ffffff; }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) body::before { display:none; }
}

/* ============================================================================
   GLASS — COMPLETING THE THEME. The first pass dressed nine surfaces; a coverage
   sweep found thirty-five more, and an undressed surface on this theme is not
   "plainer" — it is an opaque white slab floating in a translucent world, which
   is exactly how these themes fall apart in practice.
   ============================================================================ */

/* ── 1 · SCRIMS. Every dialog was filtering a hard navy scrim — rgba(15,23,42,.5)
      with no blur — on a pastel theme. The scrim IS the backdrop a modal frosts,
      so it has to be light and it has to blur, or the dialog has nothing to sit on. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.modal-overlay, .apx-overlay, .ovl, .khtfw-overlay, .ox-drawer-ov) {
  background: rgba(120,130,180,.26);
  -webkit-backdrop-filter: saturate(1.2) blur(10px); backdrop-filter: saturate(1.2) blur(10px);
}

/* ── 2 · THE SURFACES THE FIRST PASS NAMED BY CLASS AND MISSED BY NAME. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.panel, .stat, .kpi-card, .ds-card, .ox-card, .op-machine,
                         .khtfw-modal, .side-usermenu, .ds-pop) {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--shadow-md), inset 0 1px 0 var(--gl-lip);
}

/* ── 3 · THE OPERATIONS MODULE. `.op-machine` read var(--card) with no filter, so
      the whole page was text on raw gradient — measured at 3.31:1, under the floor. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .op-rest { border-top-color: var(--glass-brd); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .op-rest > div { border-right-color: var(--glass-brd); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .op-g { border-top-color: var(--glass-brd); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.op-risk, .op-note) {
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}

/* ── 4 · TABS. `.ox-tabs2` got a wash but the SELECTED tab took no accent, so on the
      permits page "Open (9)" and "Closed (5)" were indistinguishable. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-tabs2 button.on,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-tab.active {
  background: var(--gl-f5);
  box-shadow: inset 0 -2px 0 var(--primary);
}

/* ── 5 · TABLES. A sticky header at .42 white over a .55 pane is one step apart, so
      rows scrolled visibly THROUGH the column titles. Header goes opaque-ish and
      keeps its own blur; zebra and hover come off --bg, which is a colour again. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.data-table thead th, .form-table th) {
  background: var(--gl-f6);
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  border-bottom-color: var(--border-strong);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .data-table tbody tr:nth-child(even) { background: var(--gl-f1); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .data-table tbody tr:hover { background: var(--gl-f3); }

/* ── 6 · BADGES AND CHIPS were opaque --tint-* literals: printed labels stuck on
      glass. Restated as vibrancy — the fill is the hue at low alpha, the ink stays. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-b.info,  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .badge-info    { background:rgba(59,130,246,.18);  border:1px solid rgba(59,130,246,.34); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-b.warn,  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .badge-warning { background:rgba(245,158,11,.20);  border:1px solid rgba(245,158,11,.38); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-b.bad,   :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .badge-danger  { background:rgba(239,68,68,.18);   border:1px solid rgba(239,68,68,.34); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-b.plain, :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .badge         { background:var(--gl-f4); border:1px solid var(--glass-brd); }
/* ★ .ox-b.ok WAS THE ONE TONE LEFT OUT OF THE BLOCK ABOVE — info, warn, bad and plain were all
   restated and `ok` was not, so a green badge stayed the opaque #d1fae5 from --success-light
   while its four siblings became vibrancy fills. Measured on Parts Inventory and Shutdowns. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-b.ok,    :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .badge-success  { background:rgba(16,185,129,.20);  border:1px solid rgba(16,185,129,.36); }
/* ★ AND NONE OF THE FIVE HAD A BLUR. They were given a translucent fill and a border and no
   backdrop-filter, which is the worst combination: 50 `.ox-b.plain` chips across five module
   pages with unblurred page content crawling under them. A tint is not glass by itself. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ox-b, [class*="badge-"], .ui-pill) {
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}
/* The shared finding/empty components read --card and --bg straight, so they had glass colour
   and no material — `.ui-find-c` panels up to 1132x274 on four module pages. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ui-find-c, .ui-empty-ico, .ui-find-ev, .ui-find-sev) {
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-color: var(--glass-brd);
}

/* ── 26 · ★ A CONTAINER OF CARDS IS NEVER A FILL. THE RULE, WRITTEN DOWN, BECAUSE I HAVE NOW
       MADE THIS MISTAKE THREE TIMES IN ONE SESSION.
   Measured on Parts Inventory: `.ui-find` is a filled .55 pane 1132x2390, and the `.ui-find-c`
   cards inside it are .58 — a THREE PER CENT difference. The whole "What To Look At" region
   therefore read as one solid white slab with faint seams where the cards were, which is what
   Vijay was pointing at. Identical arithmetic to the two before it: the sidebar sections at .42
   on a .55 rail, and `.pg-board` at .55 holding cards pushed to .72.
   The pattern is always the same and so is the fix. If a surface EXISTS TO HOLD other surfaces,
   it gets a boundary — border, corner, spacing, maybe a lip — and NO fill. Two translucent
   panes of similar density stacked do not read as two objects; they read as one, and the inner
   one disappears. Density separates a pane from the GROUND, never from another pane. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ui-find, .ui-findwrap) {
  background: transparent;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
/* …and the store bar was translucent with no blur at all — flagged by the page sweep, missed by
   my own list. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .pt-storebar {
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  border-color: var(--glass-brd);
}

/* ── 7 · THE PAGE'S OWN CHROME — the strip of controls between the header and the
      list. Every one of these read var(--card) at .58 with no blur. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ox-selbar, .ox-group, .ox-views, .ox-pager, .ox-icobtn, .ds-sel) {
  background: var(--gl-f4);
  -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
  border-color: var(--glass-brd);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ox-views button.on, .ds-sel:focus-within) { background: var(--gl-f6); }
/* the search input was dressed and its rounded wrapper was not, so the field sat in a paler box */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-search { background: var(--gl-f4); border-color: var(--glass-brd); }

/* ── 8 · NEUTRAL BUTTONS. `.btn` measured .58 white with NO blur — a see-through
      button with page text crawling under its label. */
/* ★ AND IT MUST NOT EAT THE SEMANTIC BUTTONS. `:is(.btn, …)` is (0,2,0) with the theme attribute —
   the same as the `.btn-primary` rule above — and it sits LATER in the file, so it won outright and
   the Appearance dialog's Save button rendered white-on-white. That is the exact bug Vijay reported
   three times in another form, reintroduced by me while completing this theme; caught by opening the
   dialog and looking at it. A neutral-button rule has to say which buttons are neutral. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.btn, .ghost-btn, .btn-secondary, .btn-sm):not(.btn-primary):not(.ox-hero):not(.btn-success):not(.btn-danger) {
  background: var(--gl-f5);
  -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
  border-color: var(--glass-brd);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.btn, .ghost-btn, .btn-secondary):not(.btn-primary):not(.ox-hero):not(.btn-success):not(.btn-danger):hover { background: var(--gl-f6); }

/* ── 9 · SKELETONS. 57 views paint these first, so they are the FIRST thing a
      person sees on this theme, and they were opaque cards with an unthemed
      #f1f5f9 → #e2e8f0 shimmer. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ui-skelcard, .ui-skelrow, .ui-skelcirc) {
  background: var(--gl-f3);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border-color: var(--glass-brd);
}

/* ── 10 · THE APPEARANCE DIALOG ITSELF — the one screen a person is on while
       CHOOSING this theme. Head and foot were opaque bands across a glass pane. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.apx-head, .apx-foot) { background: var(--gl-f3); border-color: var(--glass-brd); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .apx-seg { background: var(--gl-f3); border-color: var(--glass-brd); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .apx-seg button.on { background: var(--primary); color:#fff; }
/* and the "How this works" guide, which every module page can open */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.khtfw-head, .khtfw-who, .khtfw-sec) { background: var(--gl-f3); border-color: var(--glass-brd); }

/* ── 11 · THE QUEUE BLOCK on Work Orders — the first content on the page. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .wo-q { border-top-color: var(--glass-brd); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .wo-qn { -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }

/* ── 12 · THE RAIL'S OWN SUB-SURFACES, which sat opaque inside a translucent rail. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.sidebar-brand, .cacct) { background: var(--gl-f2); }
/* ★ NOT A FILL ANY MORE — see --menu-bg in the token block. This rule, the overlay list and the
   shared pane recipe all painted `.cacct-pop`, which is why three separate attempts to override
   it from later in the theme all lost: there was no single owner. It now sets only the EDGE, and
   the surface comes from the channel the base rule reads. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.cacct-pop, .nav-fly) {
  border: 1px solid var(--glass-brd);
  box-shadow: var(--shadow-lg), inset 0 1px 0 #ffffff;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .cacct-sep { background: var(--glass-brd); }

/* ── 13 · THE FLOATING BUTTONS. Two solid circles were the least glassy thing on
       every screenshot, and they sit on top of everything. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(#comms-fab, #help-fab, #urs-fab) {
  -webkit-backdrop-filter: blur(14px) saturate(1.4); backdrop-filter: blur(14px) saturate(1.4);
  box-shadow: var(--el-3), inset 0 1px 0 var(--gl-lip);
}
/* The tray is a container of buttons: boundary, no fill (§26). The pills are the objects. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) #comms-menu {
  background: transparent; border: none; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .comms-item {
  background: var(--glass-over);
  -webkit-backdrop-filter: var(--over-blur); backdrop-filter: var(--over-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--el-3), inset 0 1px 0 var(--gl-lip);
}

/* ── 14 · FOCUS. A 15%-blue ring on a translucent field is not a focus indicator.
       This is the one thing on the list that is an accessibility requirement, not a look. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .form-control:focus,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-search input:focus {
  background: var(--gl-f7);
  box-shadow: 0 0 0 3px rgba(59,130,246,.45);
  border-color: var(--primary);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :focus-visible { outline: 2px solid var(--primary-dark); outline-offset: 2px; }

/* ── 15b · THE RAIL IS GLASS, NOT A WHITE PANEL. It was in the pane list above at
       `--glass-bg` (55% white) and Vijay said three times that the side menu was not
       glassmorphism — he was right and the measurement agrees: 55% white over the near-bare
       left edge of the ground computes to #f4f6fa, which is white. Two things were wrong and
       both had to change: the ground had no colour under the rail (fixed above), and the rail
       was far too opaque to show it. 28% with a cool bias lets the pool through.
       ★ THIS OVERRIDES the shared pane recipe for `.sidebar` only — the rail is now listed
       here instead of there; every other surface keeps the shared recipe. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar {
  background: linear-gradient(180deg, var(--gl-f2), rgba(244,247,255,.24));
  -webkit-backdrop-filter: saturate(1.7) blur(26px); backdrop-filter: saturate(1.7) blur(26px);
  border: none;
  border-right: 1px solid var(--gl-f4);
  box-shadow: inset -1px 0 0 rgba(120,140,200,.12), var(--el-3);
}
/* The brand block and the account block are the two lit ends of the rail. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-brand,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .cacct {
  background: var(--gl-f1);
  border-color: var(--gl-f3);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}

/* ── 16 · THE SIDEBAR SECTIONS ARE PANES TOO. The sidebar was one big frosted slab with the
       sections drawn on it as an ink wash, and an ink wash on frosted glass is invisible: the
       base rules paint an open `.nav-section` with `color-mix(--text 4.5%)` and the active item
       with 9%, both of which read as nothing once the surface behind them is already translucent.
       ★ THIS OVERRIDES `.nav-section.is-open` (line ~599) and `.nav-item.active` (line ~2297) —
       background only, on both; the geometry, weight and ink stay theirs.

       An open section becomes its own slab with its own lip of light, so the menu reads as a
       stack of glass cards rather than one sheet with headings printed on it. */
/* ★ SEPARATE BY TINT, NOT BY MORE WHITE. The first attempt painted each open section
   `var(--gl-f3)` on top of a rail that was already 55% white — the two composited to
   about 76% and turned the whole menu into the plain white panel Vijay reported. A pane sitting
   ON glass reads by being slightly cooler and slightly brighter at its lip, never whiter.
   ★ Scoped through `.sidebar` on purpose: (0,4,0) puts this out of reach of the base
   `.nav-section.is-open` and of the `[data-bg]` and per-theme rules, so it cannot be silently
   outranked the way the .42 version was. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open {
  background: linear-gradient(180deg, var(--gl-f2), rgba(140,165,255,.10));
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border: 1px solid var(--gl-f3);
  box-shadow: inset 0 1px 0 var(--gl-lip), var(--el-1);
}
/* The heading is the lit edge of that slab, not a label floating above it. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open > .nav-group {
  color: var(--primary-dark);
  text-shadow: 0 1px 0 var(--gl-f5);
}
/* A non-collapsible section title gets the same lift so the two kinds match. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .nav-section-title { text-shadow: 0 1px 0 var(--gl-f5); }

/* Where you ARE has to survive the translucency — a 9% grey pill does not. Tinted glass in the
   brand hue plus the accent rail, which is how every other theme says "this one". */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.active,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.nav-parent.expanded {
  background: linear-gradient(90deg, rgba(59,130,246,.20), rgba(99,102,241,.11));
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: var(--primary-dark);
  box-shadow: inset 3px 0 0 var(--primary), inset 0 1px 0 var(--gl-lip);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item:hover:not(.active) { background: var(--gl-f2); }

/* ── 17 · THE LAST THREE SURFACES A THEME COULD NOT REACH — "glassmorphism everywhere"
       means the ones written outside the stylesheet too, not only the ones that were easy. */

/* Toasts. Answered through the vars just opened in public/js/core/api.js; the tone bar is
   untouched, so an error is still unmistakably an error. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) {
  --toast-bg: var(--gl-f5);
  --toast-fg: var(--text);
  --toast-blur: saturate(1.5) blur(22px);
}

/* Scrollbars. A solid grey thumb on a translucent rail is the one piece of furniture that
   gives away that the pane is a div. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) ::-webkit-scrollbar-thumb {
  background: rgba(120,140,200,.34); border: 2px solid transparent; background-clip: padding-box;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) ::-webkit-scrollbar-thumb:hover { background: rgba(99,102,241,.48); background-clip: padding-box; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) ::-webkit-scrollbar-track { background: transparent; }

/* The search field carried a hardcoded #fff, so it was the one opaque white rectangle left
   sitting on a frosted header. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.search-input, .ox-search input, .form-control, .ds-sel, select, textarea) {
  background: var(--gl-f3);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-color: var(--glass-brd);
}

/* The voice assistant and its mic were written for a dark surface. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(#urs-fab, .urs-mic, .urs-panel) {
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-brd);
  color: var(--text);
}

/* ============================================================================
   GLASS · §18 — THE RAIL, REBUILT AS A MATERIAL

   ★ WHY THIS EXISTS. Vijay said four times that the side menu was not glassmorphism.
   Measured on /plant/operations at 1440×1000: the rail's interior computed to
   rgb(192,216,255) and the bare page ground ONE PIXEL to its right computed to
   rgb(193,216,255). Scanning x from 200 to 234 the value never moved by more than 1.
   There was no rail — an unpainted region of the wallpaper with menu text lying on it.
   Three earlier passes tuned its OPACITY, and opacity was never the variable: every
   hard-edged disc on the ground sat outside the rail's column, so its blur was
   convolving a smooth ramp and returning it unchanged. Discs added to the ground above.

   ★ WHAT THIS OVERRIDES, DELIBERATELY, BY SOURCE ORDER (layering, not silently):
       §15b  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar
             → surface, edges, shadow, and .sidebar-brand / .cacct's extra fills
       §16   :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open
             → the group CONTAINER is removed outright, not restyled
       §16   :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.active
             → 20% accent wash + 3px tick  →  one solid accent lozenge
       §16   :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open > .nav-group
             → accent ink + white text-shadow → secondary ink, no shadow (4.49:1 → 7.20:1)
   Nothing else in §15b or §16 is touched. The rail is token-driven, which is what lets
   all six escape hatches at the bottom reach it by restating values.
   ============================================================================ */

/* ── §18.1 · THE MATERIAL, AS TOKENS ──────────────────────────────────────── */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) {
  --rail-a: var(--gl-f3);      /* lit end (top). macOS .sidebar material is 40–55% white;
                                           .24 had no material, .55 was the white panel rejected */
  --rail-b: var(--gl-f2);      /* shaded end. Light comes from above, so the rail darkens
                                           DOWN — a vertical ramp is a pane, a flat fill is paint */
  --rail-spec: var(--gl-f1);   /* specular on the leading 44px — vibrancy brightens
                                           toward the light. ★ .30 re-created the clip this
                                           theme was just repaired for, locally: the acceptance
                                           test came back share(B==255) = 31.9% in the left
                                           gutter and 0% everywhere else, i.e. the highlight
                                           itself was pushing that strip to the ceiling. A
                                           specular has to stay under the ceiling to read as one. */
  --rail-blur: contrast(1.06) saturate(1.12) blur(34px);
                                        /* Same clip, worse: saturate(1.9) put 79–88% of the
                                           rail's pixels at B=255. And the comment that used to
                                           sit here claimed "Apple's stack in order: blur,
                                           saturation, luminance" while the value listed saturate
                                           FIRST — measured, Chrome is order-independent here
                                           (Δ ≤ 0.2 RGB either way), so the claim was inert and
                                           wrong at the same time. brightness(1.05) is gone: it
                                           moved the mean 2.4 R and left the clip at 81%. */
  --rail-seam: rgba(23,33,61,.11);      /* THE CRACK. Dark, not white — macOS separatorColor is
                                           rgba(0,0,0,.10). A white hairline between two light
                                           materials measured 1.0:1 against both, i.e. absent */
  --rail-lip: var(--gl-f6);    /* THE LIP. One pixel of light on the edge the light hits.
                                           This file already calls it "the whole trick" — and the
                                           one surface being rejected was the only one without it */
  --rail-cast: 10px 0 34px -14px rgba(23,33,61,.26);  /* the rail OCCLUDES the page. 6% alpha over
                                           a coloured ground was nothing.
                                           ★ Never set this to `none`: a bare `none` inside a
                                           comma-separated box-shadow list invalidates the WHOLE
                                           declaration and the rail loses its lip too. The
                                           off-switches below zero it as `0 0 0 0 transparent` */
  --rail-hover: var(--gl-f4);  /* the material densifying under the cursor — one
                                           perceptible step above .48, which is what NSTableView does */
  --rail-ink: #39404d;                  /* ★ OPAQUE — see §18.4 */
  --rail-ink-dim: #4b5361;              /* replaces --text-muted inside the rail: 3.82:1 → 5.35:1 */
  --rail-ico: #5a6270;                  /* an icon carries meaning, so it needs 3:1, not 40% alpha */
}

/* ── §18.2 · THE RAIL SURFACE ─────────────────────────────────────────────── */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar {
  /* THREE LAYERS, NOT ONE FILL. A pane varies across BOTH axes: a specular band down the
     leading edge and a top-lit body ramp. One flat rgba is how you get paint. */
  background:
    linear-gradient(90deg,  var(--rail-spec) 0, transparent 44px),
    linear-gradient(180deg, var(--rail-a), var(--rail-b));
  -webkit-backdrop-filter: var(--rail-blur);
          backdrop-filter: var(--rail-blur);
  border: none;
  border-right: 1px solid var(--rail-seam);
  /* THE JOINT. Left to right: material · white lip · dark crack · page. That two-line edge
     is what a material boundary looks like on macOS, and it is the opposite of what was
     here (a white crack, and a dark line on the trailing lip). */
  box-shadow:
    inset  0 1px 0 var(--rail-lip),         /* top: the lit edge */
    inset  1px 0 0 var(--gl-f3),   /* leading edge: also faces the light */
    inset -1px 0 0 var(--gl-lip-side),   /* the lip immediately inside the seam */
    var(--rail-cast);                       /* and it lands on the content, so it is ABOVE it */
}

/* ── §18.3 · THE GROUP IS WHITESPACE AND A LABEL. NOT A BOX. ──────────────── */
/* ★ THIS IS THE CHANGE THAT DECIDES IT. §16 gave every open section a fill + a 1px border
   + an inset lip + a drop shadow, so five sections stacked into five outlined rounded
   rectangles. And because each fill landed only ~4 units off the rail beneath it, what the
   eye actually received was not the fills — it was the OUTLINES. The rail read as a
   wireframe of nested boxes, which is the most "not glassmorphism" thing on the screen.
   No macOS sidebar has ever put a container around a group: NSTableView source lists
   separate groups with space and a secondaryLabelColor heading, full stop. A box on glass
   is a box, however transparent it is. */
/* ★ THE SECTION IS A CURVED GLASS PANEL — THE OWNER'S CALL, AND IT OVERRULES THE SPEC HERE.
   The design review said to delete the container outright: no macOS sidebar boxes a group, and
   the first version of it genuinely was a wireframe — five 1px outlines whose fills landed only
   ~4 units off the rail beneath them, so the eye received the outlines and nothing else. That
   reasoning was sound about the OLD fill and wrong about what to do next. Deleting the frame
   took the shape away with it, and Vijay's answer was immediate and specific: the sectioned
   menu has a clear curved border and it looks better that way. It is his product; the frame stays.
   ★ WHAT MAKES IT WORK NOW rather than the two times it did not:
     · the fill is a TINT, not more white — .30 white into .14 periwinkle over a .48/.38 rail.
       Stacking white on white is what produced the "plain white" panel he rejected earlier.
     · the corner comes from `var(--radius)`, the theme's own token, so the panel agrees with
       every other surface in the app instead of pinning a literal (base .nav-section says 12px,
       this theme's radius is 16px, and the audit's per-selector-radius rule is about exactly
       this). Its rows stay at `--radius-sm`, so the container is rounder than what it holds.
     · it carries the same joint as the rail: a lip of light on the top edge, a soft cast below.
   ★ THIS OVERRIDES the base `.nav-section` / `.nav-section.is-open` (line ~599, a 4.5% ink wash
   that is invisible on glass) — surface, border, corner and spacing. The collapse/expand
   geometry, the caret and the heading behaviour all stay the base's. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section {
  background: linear-gradient(180deg, var(--gl-f2), rgba(150,172,255,.14));
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  border: 1px solid var(--gl-f4);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 0 var(--gl-lip), var(--el-1);
  margin: 0 var(--space-1) var(--space-1);   /* gap reduced on Vijay's call — tighter stack */
  padding: var(--space-1) var(--space-1) var(--space-2);
}
/* An open section is the same panel, one step denser — it holds more, so it sits slightly
   forward. The base paints `is-open` with a 4.5% ink wash; on glass that reads as nothing. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open {
  background: linear-gradient(180deg, var(--gl-f2), rgba(150,172,255,.17));
  box-shadow: inset 0 1px 0 var(--gl-lip), var(--el-2);
  padding-bottom: var(--space-2);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section:last-child { margin-bottom: var(--space-1); }

/* The heading takes quiet ink, no accent, no shadow. §16 painted it --primary-dark with
   text-shadow 0 1px 0 var(--gl-f5): 4.49:1 on an 11px letterspaced label, under
   the floor, and the white halo makes it perceptually worse. A heading is wayfinding. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-group,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open > .nav-group,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section-title {
  color: var(--rail-ink-dim);       /* 7.20:1 */
  text-shadow: none;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-group:hover { color: var(--text); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-group .nav-caret { color: var(--rail-ico); }

/* ── §18.4 · ITEM INK IS OPAQUE ───────────────────────────────────────────── */
/* ★ The base rail paints .nav-item at color-mix(in srgb, var(--text) 72%, transparent) —
   72% ALPHA — over a translucent surface over a ground that now carries hard-edged discs.
   Its contrast is therefore A FUNCTION OF WHERE THE PANE HAPPENS TO SIT, and the worst
   case (a label over the brightest disc) is unbounded. Alpha ink is fine on paper and is a
   defect on glass. Icons the same: 40% alpha ≈ 2.1:1, and an icon that carries meaning
   needs 3:1. This lands WITH the new discs, which make the ground more variable, not less. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item {
  color: var(--rail-ink);           /* 8.5:1, and it does not move with the ground */
  font-weight: 500;
  border-radius: var(--radius-sm);  /* the THEME's corner token, not the base file's literal 8px */
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item .nav-icon { color: var(--rail-ico); }

/* HOVER = the material densifying under the cursor, which is literally what NSTableView
   does. It must be clearly above the rail's own .48 or it reads as a rendering wobble.
   ★ It also repairs a live failure: `.nav-item:hover { color: var(--primary) }` puts
   #3b82f6 on this rail at 2.54:1. Hover must not be the state that makes a label unreadable. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item:hover:not(.active),
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.nav-parent:hover:not(.expanded) {
  background: var(--rail-hover);
  color: var(--text);
  box-shadow: inset 0 1px 0 var(--gl-lip);   /* its own lip, so hover LIFTS */
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item:hover:not(.active) .nav-icon { color: var(--rail-ink-dim); }

/* ── §18.5 · WHERE YOU ARE IS SOLID ──────────────────────────────────────── */
/* ★ The other half of the complaint. §16 expressed selection as a 20% accent wash + a 3px
   tick + coloured text; over the lilac ground the wash landed ~8 units off the rail, so
   what you saw was the tick and a faint outline — a focused input, at the same visual
   weight as the five section boxes around it. The eye could not find it.
   Apple's source-list selection is a FILLED rounded rect in the accent colour with a white
   label. It is the one opaque object in the rail, and that is exactly why you find it
   instantly: glass everywhere is glass nowhere — a material needs one solid thing to be a
   material AGAINST.
   ★ THE GRADIENT IS BIASED TO --primary-dark ON PURPOSE AND MUST NOT BE LIGHTENED. White on
   --primary alone measures 3.68:1 and FAILS. Capping the light end at 55% primary puts the
   worst pixel at 4.79:1 and the dark end at 6.70:1. */
/* ★ VIJAY CHOSE THE FROSTED ONE. The review argued for a solid accent lozenge — it is what
   Apple's own source list does, and "a material needs one solid thing to be a material
   against" — and he looked at both renders and said not blue. His product, and he is choosing
   the more consistent of the two: the selection now stays INSIDE the material instead of being
   the one opaque object in it.
   ★ THE INK CHANGES WITH IT, AND THAT IS THE PART THAT MATTERS. White on this fill measures
   about 1.4:1 and is unreadable, so the frosted variant CANNOT keep white text — it takes
   --text, which measures ~10:1 on the tinted pane. Weight 600 plus the 3px accent rail carry
   "you are here" instead of the fill doing it alone. Never put white back on this. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.active,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.nav-parent.expanded {
  background-color: transparent;
  background-image: linear-gradient(180deg, rgba(59,130,246,.36), rgba(99,102,241,.26));
  -webkit-backdrop-filter: saturate(1.4) blur(12px);
          backdrop-filter: saturate(1.4) blur(12px);
  color: var(--text);
  font-weight: 600;
  border: 1px solid var(--gl-f4);
  border-radius: var(--radius-sm);
  box-shadow:
    inset 0 1px 0 var(--gl-lip), /* the lip */ inset 3px 0 0 var(--primary), var(--el-1);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.active .nav-icon,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.nav-parent.expanded .nav-icon { color: var(--primary-dark); }
/* was `color:#fff` for the solid lozenge — on the frosted fill that is ~1.4:1 */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.active .nav-caret,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.nav-parent.expanded .nav-caret { color: var(--primary-dark); }

/* THE READ-ONLY EYE. `.nav-ro` is a disc of var(--card) inside a 1.5px ring of var(--card)
   — on this theme two stacked 58% whites compositing to ~82%. It was the brightest, crispest
   object on the left half of the screen, on eight rows, dragging the eye to a QUALIFIER.
   On glass it has to be a hole, not a sticker. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-ro {
  background: var(--gl-f4);
  color: var(--rail-ink-dim);
  box-shadow: 0 0 0 1.5px var(--rail-seam);         /* ringed by the joint, like everything else */
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.active .nav-ro {
  background: var(--gl-f5);
  color: var(--primary-dark);
  box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--primary-dark) 42%, transparent);
}

/* ── §18.6 · THE BRAND BLOCK IS NOT A SECOND SURFACE ─────────────────────── */
/* §15b painted it var(--gl-f1) ON TOP of the rail, so the rail's top came out
   BRIGHTER than its body — which inverts the light. The rail is lit from above; its top is
   already the lit end, and a paler slab there makes the rail read as three stacked panels
   (brand / nav / account) instead of one pane. It takes no fill. What it takes is the SAME
   JOINT as everything else, so the seam where the rail meets the topbar and the seam where
   it meets the page form one continuous L instead of three unrelated lines. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-brand {
  background: none;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border-bottom: 1px solid var(--rail-seam);
  box-shadow: 0 1px 0 var(--gl-f4);        /* the lip below the crack */
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-brand h2 { color: var(--text); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-brand .brand-sub,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-brand .brand-dept { color: var(--rail-ink-dim); }
/* The mark is one of the two saturated objects in the rail. Keep the design system's own
   gradient and give it a lip, so it reads as glass over colour, not a flat tile. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-brand .brand-icon {
  box-shadow:
    inset 0 1px 0 var(--gl-lip),
    0 3px 10px -2px color-mix(in srgb, var(--primary-dark) 55%, transparent);
}

/* ── §18.7 · THE FLOOR ───────────────────────────────────────────────────── */
/* Same joint, mirrored: crack on top, lip under it. The foot DOES take a faint
   densification (+.14 over the rail) — unlike the brand block — because it is the one part
   of the rail that is PINNED while .sidebar-nav scrolls behind it. A fixed shelf over moving
   content is exactly the case frosting exists for, and it has to be a step denser or nav
   rows show through it as they pass. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .side-foot {
  background: var(--gl-f1);
  border-top: 1px solid var(--rail-seam);
  box-shadow: inset 0 1px 0 var(--gl-lip);
}
/* …and .cacct inside it takes NO fill, for the same reason the brand block does not: a box
   inside a box. It appears on hover, like every other row in the rail. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .cacct {
  background: none;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border: none;
  border-radius: var(--radius-sm);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .cacct .cnm { color: var(--text); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .cacct .crl { color: var(--rail-ink-dim); }  /* was --text-muted → 3.82:1, FAILED */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .cacct .cdl { color: var(--rail-ico); }
/* The avatar keeps its solid accent (it ties to .brand-icon, so the rail starts and ends on
   the accent — a decision this file already made). It only gains the lip. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .cacct .cav {
  box-shadow: inset 0 1px 0 var(--gl-lip), var(--el-1);
}

/* ── §18.8 · THE HANDLE STRADDLES THE SEAM, SO IT IS MADE OF THE SEAM ────── */
/* It was var(--card) with a var(--border) edge: the crispest, whitest object on the left
   half of the screen, floating with no relationship to anything around it. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .side-toggle {
  background: var(--gl-f4);
  -webkit-backdrop-filter: saturate(1.6) blur(18px);
          backdrop-filter: saturate(1.6) blur(18px);
  border: 1px solid var(--rail-seam);
  border-radius: var(--radius-sm);                  /* the theme's corner, not a literal 8px */
  color: var(--rail-ink-dim);
  box-shadow: inset 0 1px 0 var(--gl-lip), var(--el-2);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .side-toggle:hover {
  background: var(--primary-dark);                  /* --primary would be 3.68:1 with the white glyph */
  border-color: var(--primary-dark);
  color: #fff;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
}

/* ── §18.9 · THE COLLAPSED RAIL (64px) ───────────────────────────────────── */
/* ★ A 40px BLUR ON A 64px STRIP IS NOT THE SAME MATERIAL. backdrop-filter samples ±radius
   around every pixel, so at 40px each pixel of a 64px rail averages nearly the whole rail
   plus whatever Chrome's edge-extension invents off the left of the viewport. The vibrancy
   flattens to one colour and the rail goes straight back to being paint — the exact failure
   this block exists to fix, reappearing in a state nobody screenshots. Narrower kernel,
   denser tint: less to average, more of its own material. */
@media (min-width: 769px) {
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout.collapsed .sidebar {
    background:
      linear-gradient(90deg,  var(--gl-f2) 0, transparent 26px),
      linear-gradient(180deg, var(--gl-f4), var(--gl-f3));
    -webkit-backdrop-filter: saturate(2) brightness(1.05) blur(26px);
            backdrop-filter: saturate(2) brightness(1.05) blur(26px);
  }
  /* An icon-only selection has to sit INSIDE the rail, not span it. A pill touching both
     edges reads as a stripe across the rail rather than an object on it.
     ★ Inset from the scale, not from a raw px size — the audit's theme-raw-px rule is right
     and I tripped it here first time round: pinning 42×42 in a theme is exactly the Console
     drift it exists to stop, and it would also have fought the base's `padding: 11px 0`
     instead of leaving the row's own geometry alone. Margin insets; it does not resize. */
  /* No panels on a 64px strip: framed boxes per section on a rail that narrow read as a stack
     of tiles, and the base already clears the section surface when collapsed (line ~815). */
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout.collapsed .nav-section,
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout.collapsed .nav-section.is-open {
    background: none; border: none; box-shadow: none; margin: 0; padding: 0;
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout.collapsed .nav-item.active,
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout.collapsed .nav-item.nav-parent.expanded {
    margin-inline: var(--space-2);
  }
  /* The flyout is the ONE piece of the rail where frosting does real work: a pane floating
     over live content. So it takes the strong material, not the rail's. */
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .nav-fly {
    background: var(--glass-strong);
    -webkit-backdrop-filter: var(--glass-blur);
            backdrop-filter: var(--glass-blur);
    border: 1px solid var(--rail-seam);
    box-shadow: var(--shadow-lg), inset 0 1px 0 var(--gl-lip);
  }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .nav-fly .nav-fly-head { color: var(--rail-ink-dim); }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .nav-fly .pit { color: var(--rail-ink); }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .nav-fly .pit:hover { background: var(--gl-f4); color: var(--text); }
}

/* ── §18.10 · DEGRADATION — SIX DOORS, AND THE RAIL GOES THROUGH ALL SIX ─── */
/* It can, because §18.1 made the rail token-driven. Each door restates values instead of
   re-listing twenty selectors. These sit AFTER the rail rules and are (0,2,0) or higher, so
   none can be outranked the way the earlier attempts were. */

/* 1 · Appearance → Glass effects → Off */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] {
  --rail-a:#ffffff; --rail-b:#ffffff; --rail-spec:transparent; --rail-blur:none;
  --rail-seam:#dfe3ee; --rail-lip:transparent;
  --rail-cast:0 0 0 0 transparent;        /* NOT `none` — see the note in §18.1 */
  --rail-hover:#eef1f8;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] .sidebar { box-shadow: none; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] :is(.side-foot, .side-toggle, .nav-ro, .nav-fly,
                                           .nav-item.active) {
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] .side-foot  { background:#ffffff; box-shadow:none; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] .side-toggle{ background:#ffffff; box-shadow:var(--el-2); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-glass="off"] .nav-ro     { background:#ffffff; box-shadow:0 0 0 1.5px #dfe3ee; }

/* 2 · An OS-level request outranks the switch. */
@media (prefers-reduced-transparency: reduce) {
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) {
    --rail-a:#ffffff; --rail-b:#ffffff; --rail-spec:transparent; --rail-blur:none;
    --rail-seam:#dfe3ee; --rail-lip:transparent;
    --rail-cast:0 0 0 0 transparent; --rail-hover:#eef1f8;
  }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar { box-shadow: none; }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.side-foot, .side-toggle, .nav-ro, .nav-fly, .nav-item.active) {
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
}

/* 3 · High contrast. The existing [data-contrast] blocks move TOKENS only, so a rail built
      from literal rgba would have ignored the axis entirely. It reaches the rail now
      because the rail has tokens of its own. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-contrast="high"] {
  --rail-a:var(--gl-f6); --rail-b:var(--gl-f6);
  --rail-spec:transparent; --rail-blur:blur(18px);
  --rail-seam:rgba(80,90,120,.42); --rail-lip:var(--gl-f6);
  --rail-hover:var(--gl-f7);
  --rail-ink:#151a22; --rail-ink-dim:#242a36; --rail-ico:#3d4450;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-contrast="xhigh"] {
  --rail-a:#ffffff; --rail-b:#ffffff; --rail-spec:transparent; --rail-blur:none;
  --rail-seam:#3d4450; --rail-lip:transparent; --rail-cast:0 0 0 0 transparent;
  --rail-hover:#e6ecf7;
  --rail-ink:#000000; --rail-ink-dim:#151a22; --rail-ico:#2b313c;
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-contrast="xhigh"] .sidebar { box-shadow: none; }
/* At xhigh the selection drops the gradient: a flat --primary-dark is 6.70:1 with white,
   and the gradient's light end is only 4.79:1 — acceptable normally, not here. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-contrast="xhigh"] .sidebar .nav-item.active,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])[data-contrast="xhigh"] .sidebar .nav-item.nav-parent.expanded {
  background-image: none;
  box-shadow: none;
}

/* 4 · No backdrop-filter → the tint is noise under text, not a plainer look. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) {
    --rail-a:#ffffff; --rail-b:#ffffff; --rail-spec:transparent; --rail-blur:none;
    --rail-seam:#dfe3ee; --rail-lip:transparent;
    --rail-cast:0 0 0 0 transparent; --rail-hover:#eef1f8;
  }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar { box-shadow: none; }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.side-foot, .side-toggle, .nav-ro, .nav-fly) { background:#ffffff; }
}

/* 5 · ★ AND THE MOBILE DRAWER GUARD MUST BE RESTATED HERE, OR THIS BLOCK BREAKS IT.
      The existing guard is `@media (max-width:768px){ .sidebar, [data-glass] .sidebar }` —
      (0,1,1) and (0,2,0). §18.2 is (0,2,0) and LATER IN THE FILE, so it would win on source
      order and the drawer would slide over live content at 48% opacity with the page legible
      straight through the menu. On a phone the rail is not a column beside the page, it is a
      sheet on top of it, and a sheet is opaque. */
@media (max-width: 768px) {
  /* ★ THE DRAWER IS THE DESKTOP RAIL. NOT A DENSER COUSIN OF IT — THE SAME ONE.
     This guard has now been wrong twice in the same direction. First it made the drawer a flat
     opaque #f4f6fb; then it made it --glass-over (.93), which is glass but WHITE glass, because
     at 93% almost no colour refracts through. Desktop shows violet and magenta moving under the
     rail; mobile showed a white panel with the same furniture on it. Vijay's complaint both
     times was the same one and it was about the LOOK, not the structure.
     ★ THE MISTAKE WAS PUTTING THE DENSITY IN THE WRONG LAYER. "Nothing legible behind the menu"
     is a property of the STACK, not of the drawer: the scrim sits between the page and the
     drawer (z-index 99 vs 100), so blurring the SCRIM destroys legibility once, and then the
     drawer's own blur runs on top of that. Two passes, ~56px of effective radius — no text
     survives that — while the drawer itself stays at the desktop's .48 and keeps the colour.
     So this block no longer touches the surface at all. It inherits §18.2 verbatim, which is
     the only way the two can be guaranteed to match: there is one rail, and the phone gets it. */
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar {
    /* the one thing a drawer needs that a column does not: it is ABOVE the page, so it casts */
    box-shadow:
      inset 0 1px 0 var(--rail-lip), inset 1px 0 0 var(--gl-f3), inset -1px 0 0 var(--gl-lip-side), var(--el-4);
  }
  /* ★ AND THE SCRIM CARRIES THE JOB THE DRAWER WAS DOING. A flat 50% black also FOUGHT the
     drawer — its blur then sampled a grey field and the material went dull, which is the other
     half of why mobile looked nothing like desktop. A light wash plus a wide blur keeps the
     ground's colour alive under the drawer and still leaves nothing readable. */
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-overlay {
    background: rgba(233,237,250,.30);
    -webkit-backdrop-filter: saturate(1.15) blur(22px);
            backdrop-filter: saturate(1.15) blur(22px);
  }
  /* And the menu button is a piece of the same set, not a white tile. */
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .mobile-menu-fab {
    background: var(--glass-over);
    -webkit-backdrop-filter: saturate(1.3) blur(20px); backdrop-filter: saturate(1.3) blur(20px);
    border-color: var(--glass-brd);
    box-shadow: inset 0 1px 0 var(--gl-lip), var(--el-3);
  }
}

/* ── 19 · THE OPAQUE OBJECTS STILL SITTING ON THE GLASS, AND THE TRANSLUCENT ONES WITH NO
       BLUR UNDER THEM. Both lists are measured, not swept by eye — every entry below was
       found by reading computed alpha and backdrop-filter off every element ≥1200px² on
       three pages. "Glassmorphism everywhere" fails on exactly these two kinds of leftover. */

/* ★ --primary-light IS AN OPAQUE HEX, and two shared classes paint with it directly, so every
   list ROW carried an opaque #dbeafe tile: measured 11 chips on Work Permits and 14 on Work
   Orders, all alpha 1, 38×38, backdrop-filter none. The glass block never named either class.
   The value is the one §6 already chose for `.ox-b.info`, so the chips now agree with the badges. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ox-rowic, .ox-ic-plain) {
  background: rgba(59,130,246,.18);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
/* The "How this works" tab is the FIRST control on every module page (the header contract puts
   it first) and it was a fully opaque #dbeafe button — measured rgb(219,234,254) alpha 1. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .khtfw-tab {
  background: rgba(59,130,246,.16);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border-color: var(--glass-brd);
}

/* ★ THE STICKY FORM BAR AND FOOT WERE THE NINTH AND TENTH. `.ds-fp-bar` / `.ds-fp-foot` (§ the
   full-page form panel, above) set `background: var(--card)` with no filter. On Glass and Liquid
   Glass `--card` IS the glass ladder, so measured on /plant/work-orders with the panel open:
   rgba(255,255,255,.33) light and rgba(44,52,72,.27) dark, backdrop-filter none. These are the two
   surfaces in the app that a whole form scrolls underneath by design — two thirds of the fields
   came straight through the strip pinned over them, which is the "printing through" defect.
   Overrides the `background: var(--card)` in the .ds-fp-bar / .ds-fp-foot rules above, on these
   four themes only, with the same treatment the topbar gets — same problem, same answer. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ds-fp-bar, .ds-fp-foot) {
  /* --gl-f6, not --glass-bg: the ladder rung, not the card's. --glass-bg IS --card on these themes
     (and --glass-strong is a `color-mix(--card 82%)`, i.e. THINNER), so both would have added the
     blur and taken opacity away — measured .33 -> .27. f6 is .86/.78/.80/.74 across the four, which
     with the blur leaves nothing legible behind the strip. */
  background: var(--gl-f6);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-color: var(--glass-brd);
}

/* ★ EIGHT SURFACES WERE TRANSLUCENT WITH `backdrop-filter: none`, which is the worst of both:
   unblurred page content crawls under them as you scroll. Two of these were rules I wrote in
   §5/§6 that set a translucent background and forgot the filter — the header comment above one
   of them even names this exact gap. A translucent surface without a filter is not glass. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ox-tabs2, .ox-tabs2 > button.on, .ox-search, .ox-views > button.on,
                         .ui-find, .wo-colhead, .ox-scope, .ui-scale-tr, .side-foot) {
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
/* …and the three of those eight that had no glass rule at all, so they were still reading
   `--bg` (34% white) or `--tint-neutral-bg` as a flat fill. */
/* `.ui-find` was in this list and is NOT here any more: it is a container of cards, so §26
   gives it a boundary and no fill. Overriding it later would have worked and been a worse
   record of the decision. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.wo-colhead, .ox-scope) {
  background: var(--glass-bg); border-color: var(--glass-brd);
}

/* ★ AND THE FABS' backdrop-filter WAS A MEASURED NO-OP — their fills are 100% opaque, so the
   filter changed 0.06% of their pixels (only the anti-aliased corner fringe) while costing two
   full-viewport compositing passes. The rule's own comment said two solid circles were the
   least glassy thing on the page; it set a filter and never touched the background, so they
   stayed two solid circles. Translucent fills, and the filter now has something to show. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) #help-fab  { background: color-mix(in srgb, var(--primary) 66%, transparent); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) #comms-fab { background: color-mix(in srgb, var(--comms-accent) 74%, transparent); }

/* ── 20 · EVERY SCROLLBAR IN THE APP IS ONE THIN BAR — AND IT IS NOT A LIST ANY MORE.
       There were two hand-maintained lists (the sidebar pair at ~2294, and ten popover/list
       selectors at ~2484) and everything NOT on them got the browser's ~15px default: the page
       scrollbar itself, every table, every `overflow-x:auto` wrapper a page adds. A list that
       has to be extended by hand each time somebody makes something scroll is the defect — the
       ten entries are proof it was already being maintained badly.
       ★ `*` is (0,0,0), so BOTH existing lists still win where they apply and neither changes
       behaviour: the sidebar keeps its appear-on-hover thumb, the popovers keep theirs. This
       only reaches what nothing was reaching.
       ★ AND THE FIREFOX SPLIT IS NOT OPTIONAL, it is already written down twice in this file:
       Chrome IGNORES ::-webkit-scrollbar as soon as `scrollbar-width` is also set and falls back
       to its own ~11px "thin" bar, so that property goes ONLY to browsers without the pseudo. */
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-corner { background: transparent; }
/* Visible at rest, unlike the sidebar's. A rail you already know is there can hide its thumb;
   a page or a wide table has to advertise that it scrolls at all, and a 4px invisible thumb on
   a horizontally-scrolling table is a control nobody finds. Thin, not hidden. */
*::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text) 20%, transparent);
  border-radius: var(--radius-sm);
  border: 2px solid transparent;      /* insets the thumb to ~4px of ink inside an 8px gutter */
  background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--text) 38%, transparent);
  background-clip: padding-box;
}
@supports not (selector(::-webkit-scrollbar)) {
  * { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--text) 20%, transparent) transparent; }
}

/* ── 21 · THE SURFACES VIJAY NAMED: FORMS, POPUPS, DROPDOWNS, THE AVATAR MENU AND THE
       LANGUAGE SELECTOR. Every one was a real miss, and checking them turned up a dead
       selector as well.

   ★ `.side-user-menu` IS IN THE OVERLAY LIST AND MATCHES NOTHING. It appears five times in
   this stylesheet and ZERO times in any view; the sibling spelling `.side-usermenu` appears
   seven times here and also never in a view. The menu that actually renders is `.cacct-pop`.
   A selector that matches nothing looks exactly like a rule that works — which is how the
   avatar menu sat unthemed while the list appeared to name it. Named here so the next person
   does not trust that list; the real class is covered below.

   ★ AND `#lang-pop` HAD NO GLASS RULE OF ANY KIND. Nor did `.ds-list`, `.ds-opt`, `.cacct-sec`,
   or `.pit` outside the collapsed-rail media query. `.ds-opt` paints `var(--bg)` — a 34% white
   film with no blur — so an open dropdown showed the page crawling under its own options. */

/* The language selector and the avatar menu FLOAT, so they take the overlay density, not the
   pane density — the same distinction the notification panel needed. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(#lang-pop, .ds-pop, .ds-list) {
  background: var(--glass-over);
  -webkit-backdrop-filter: var(--over-blur); backdrop-filter: var(--over-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--shadow-lg), inset 0 1px 0 var(--gl-lip);
}
/* Rows inside those menus get no surface of their own until hovered — a fill per row inside a
   floating pane is the nested-box mistake the sidebar sections already made once. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.pit, .ds-opt, .cacct-sec) { background: transparent; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.pit:hover, .ds-opt:hover) { background: var(--gl-f3); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.pit.is-on, .ds-opt[aria-selected="true"]) {
  background: var(--nav-active-bg); color: var(--primary-dark);
}

/* FORMS. The field itself was already frosted; what was missed is everything AROUND it — the
   grid, the fieldset, the section, the label cell. A form on glass has to read as one pane
   with fields cut into it, not as a stack of white strips. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.form-grid, .form-grid-2, .form-section, fieldset) {
  background: transparent; border-color: var(--glass-brd);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.form-table td.row-label, .form-label-sm) { background: var(--gl-f1); }

/* ── 22 · AND THE ROOT CAUSE UNDER ALL OF IT: THE THEME FROSTS BY SELECTOR LIST WHILE THE
       TOKENS ARE CONSUMED BY HUNDREDS OF CLASSES IT NEVER NAMES.
   Measured across 43 admin/company pages: 897 elements on 27 pages paint with `var(--bg)` and
   489 on 17 pages paint with `var(--card)` — and the glass pass names about a dozen of them.
   The largest surfaces in the app are in that unnamed set: a 1000x15028 `.sheet`, an 808x7436
   book body, 320 org-chart nodes, 58 store tiles.
   ★ A CHILD OF AN ALREADY-FROSTED PANE DOES NOT NEED ITS OWN BLUR — the blur happened at the
   card, and a film inside it composites correctly. So the ones that matter are the surfaces
   sitting DIRECTLY ON THE GROUND, and they are listed here rather than left to be found one
   complaint at a time. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])
  :is(.sheet, .og-book, .fb-book, .og-toc, .fb-toc, .oc-node, .st-pkg, .sld-scroll, .sld-pal,
      .sld-cfg, .adm-filters, .ps-rule, .ps-obs, .ps-win, .ps-gs, .ta-mod, .st-limit, .oxc,
      .fb-mock, .fb-meta-strip, .acs-fact) {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  border-color: var(--glass-brd);
}

/* ── 23 · THE HEADER WASH, THE INVISIBLE TEXT BARS, AND THE BESPOKE CARDS.
       All three came out of a page-by-page sweep of 107 endpoints; none would have been found
       by reading the stylesheet, and two are legibility bugs rather than looks.

   ★ 23a · `.content-area` PAINTS THE TOP 210px OF EVERY APP-SHELL PAGE, AND GLASS WAS THE ONLY
   OPINIONATED THEME THAT NEVER RESTATED IT. black, soft, console and console-day each rebuild
   it; grep after the glass block returned one comment and no rule. With --primary-light still
   the opaque #dbeafe it was a near-solid pale band sitting exactly where the page header is —
   between the frosted panes and the discs they are supposed to refract. On this theme the
   GROUND IS the wash, so the wash is removed rather than re-tinted: two ambient layers fighting
   in the same 210px is how you get a muddy header. */
/* ★ NO GLASS OVERRIDE ON `.content-area` — ON PURPOSE, AND THIS IS THE SECOND TIME I HAVE HAD
   TO LEARN IT. The base rule (line ~2692) paints the ambient band behind every page hero, and I
   first DELETED it here, then restored it, then bolted a 42% scrim onto it — three edits to
   somebody else's working design, and the hero still did not look right because the real fault
   was never in this rule at all. Vijay's instruction: if glass wants a frame, glass ADDS a
   layer; it does not rewrite what already works.
   So the base rule now runs untouched on this theme, and the thing that was actually drowning
   it — glass's own ground, whose pool alphas I had roughly doubled to give the rail something
   to refract — is turned down where the CONTENT sits and kept strong only in the rail's column.
   The header design is the base's again. See the ground block above for the alphas. */


/* ★ 23b · WHITE TEXT ON A 38%-WHITE BAR. Four standalone pages set `background:var(--sidebar-bg)`
   with `color:#fff` — a pairing that is correct for every dark-sidebar theme and catastrophic
   here, because glass declares --sidebar-bg as var(--gl-f2). Measured invisible: the
   225,000px² `.lv-top` on /platform/wiring/live (title plus 25 company chips), the 86,400px²
   `.top` on /group/help, `.gc-top` on the group dashboard, and `.ds-topbar` — which is the
   class views/partials/page-head.ejs PRESCRIBES for every new standalone page, so it would
   have kept propagating.
   The bar keeps its white ink and is given a surface that can carry it: dark glass. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.lv-top, .gc-top, .ds-topbar, .ghelp .top) {
  background: rgba(23,33,61,.72);
  -webkit-backdrop-filter: saturate(1.3) blur(22px); backdrop-filter: saturate(1.3) blur(22px);
  border-bottom: 1px solid var(--gl-f1);
  box-shadow: inset 0 1px 0 var(--gl-lip);
}

/* ★ 23c · BESPOKE CARD CLASSES READ --card AND ARE NAMED IN NO FROST SELECTOR. The frost lists
   name `.card`; these do not use it. Measured unblurred surface: 2,374,000px² across six
   `.lv-panel`s on one page, 715,067px² of `.g-card` on /group/help, plus `.ds-card` — again the
   prescribed class for new pages. A pane that reads --card gets the theme's colour and none of
   its material, which is the worst of both: translucent, so content shows through, and
   unblurred, so it crawls. */
/* ★ AND I MADE THE SAME MISTAKE HERE TEN MINUTES AGO. `.lv-panel` holds `.lv-stat`, and I gave
   BOTH `--glass-bg` in the same rule — identical density, so every stat disappears into the
   panel containing it. `.lv-panel` is a container: boundary, no fill. Caught by grepping my own
   glass block for filled selectors after the third instance, which is what should have happened
   before writing any of them. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .lv-panel {
  background: transparent;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 1px solid var(--glass-brd); border-radius: var(--radius);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.lv-stat, .g-card, .ds-card, .kpi, .gpg .card, .ghelp .note) {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  border-color: var(--glass-brd);
}

/* ── 24 · THE LOGIN SCREEN IS DARK GLASS. It is the first thing every user sees and it had no
       glass at all — and, uniquely in this app, it sits OUTSIDE the appearance system: no
       theme attribute is ever stamped on it (views/login.ejs:2 and server.js:1405 both emit a
       bare <html lang="en">), and there is no [data-theme] rule for #login-page or .login-card
       anywhere in the file. So it cannot follow anybody's theme, and the only sensible answer
       is to give it one look and make that look good. Dark glass, because the navy ground it
       already has is the right ground for it.

   ★ THIS IS NOT SCOPED TO A THEME ON PURPOSE. Scoping it would do nothing: there is no
   attribute to match. It therefore reaches every screen built on this shell —
   server.js authShell() has NINE call sites: every tenant's own portal login, both
   reset-password steps, tenant set-password, /group/login, /group/set-password, /g/:slug/login,
   and errorPage() at server.js:5618, which is EVERY 404 and 403 page in the product.

   ★ AND .login-card's BACKGROUND IS PINNED, NOT TOKENISED. It read `var(--card)` (line 267).
   With no theme attribute that resolves to opaque white and looks fine, which is why this was
   never noticed — but the moment any theme attribute reaches this markup the card becomes a
   58%-translucent white sheet with no blur over a #1e293b ground, and body text measures
   2.27:1. A screen outside the theme system must not read theme tokens for the one colour it
   cannot survive being wrong. */
#login-page {
  /* hard-stopped discs FIRST — a blur needs edges, and the two repeating grids already on
     ::before are 1px lines at 3% alpha, which is not something a 30px kernel can act on */
  background:
    /* ★ SMALL AND FAINT ON PURPOSE. The first pass used 180-260px discs at .38-.55 and they
       rendered as four flat circles sitting on the ground — the card is a small object on a
       big page, so most of each disc landed in BARE ground where nothing refracts it. A disc
       exists to give the blur an edge to bite on, not to be looked at: clustered near the
       centre, where the card actually is, and quiet enough that the eye reads light rather
       than geometry. */
    radial-gradient(96px  96px at 38% 34%, rgba(99,102,241,.34) 0 46%, transparent 47%),
    radial-gradient(78px  78px at 62% 40%, rgba(14,165,233,.30) 0 46%, transparent 47%),
    radial-gradient(104px 104px at 58% 68%, rgba(219,39,119,.26) 0 46%, transparent 47%),
    radial-gradient(72px  72px at 41% 66%, rgba(20,184,166,.24) 0 46%, transparent 47%),
    radial-gradient(1100px 800px at 20% 10%, rgba(79,70,229,.55), transparent 100%),
    radial-gradient(900px 760px at 88% 20%, rgba(14,165,233,.42), transparent 100%),
    radial-gradient(900px 800px at 70% 95%, rgba(219,39,119,.34), transparent 100%),
    linear-gradient(135deg, #0b1020 0%, #131a33 50%, #1b1533 100%);
}
.login-card {
  /* pinned: see above. A dark pane, a light hairline, a lip on the top edge, and a cast that
     says it floats above the ground rather than being printed on it. */
  background: rgba(20,26,46,.62);
  -webkit-backdrop-filter: saturate(1.4) blur(30px); backdrop-filter: saturate(1.4) blur(30px);
  border: 1px solid var(--gl-f1);
  box-shadow: var(--el-4), inset 0 1px 0 var(--gl-lip);
}
/* The ink has to invert with the surface, and these were all reading light-theme tokens. */
.login-card, .login-card .form-label-sm, .login-brand h1 { color: #e9ecf6; }
.login-brand p, .login-card .muted, .login-card small { color: #a9b1c6; }
.login-card :is(input, select, .form-control) {
  /* ★ LITERALS, NOT --gl-f1 — THIS PAGE IS OUTSIDE THE THEME SYSTEM. Every declaration of --gl-f1
     lives inside a [data-theme=…] block, and the sign-in shell carries no theme attribute at all,
     so both of these were invalid at computed-value time: the fields rendered with no fill and no
     border, a shapeless gap on the first screen of the product. A page that is not themed cannot
     borrow a themed token — it has to state its own value. */
  background: rgba(255,255,255,.07);
  border: 1px solid rgba(255,255,255,.14);
  color: #e9ecf6;
}
.login-card :is(input, select, .form-control)::placeholder { color: #8f97ac; }
.login-card :is(input, select, .form-control):focus {
  background: rgba(255,255,255,.10);
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(59,130,246,.35);
}
.login-card .demo-creds {
  background: var(--gl-f1);
  border: 1px solid var(--gl-f1);
  color: #bcc3d6;
}
.login-card .demo-creds :is(b, strong) { color: #e9ecf6; }
/* ★ THE INK WAS INVERTED FOR CLASSES THIS PAGE DOES NOT USE. The rule above names
   `.form-label-sm`, and this markup labels its fields with a plain <label> and its rows with
   `.cred-label` — so neither was ever reached, both kept `--text`, and with no theme attribute
   on this page that is #292929: measured 1.19:1 against the .62 dark card. Sixty elements on
   this page sat below 3:1, which is every label and every credential on the sign-in screen.
   Invert what the markup actually contains, not what it was assumed to contain. */
.login-card :is(label, .cred-label) { color: #cdd4e6; }
/* ★ the legal pages from the sign-in (16-10, core/legal.js) - the card's quiet ink, its links a step brighter */
.login-legal { display: block; text-align: center; margin: 14px 0 0; font-size: .74rem; line-height: 1.6; }
.login-card .login-legal a { color: #cdd4e6; text-decoration: none; }
.login-card .login-legal a:hover { text-decoration: underline; }
/* ★ AND A PILL IS A SURFACE, NOT ONLY AN INK. `.cred-value` takes `background: var(--border)`
   from the base design system — a LIGHT chip — and the glass pass set its text to #e9ecf6 and
   left the chip alone: near-white on near-white, 1.05:1, which is why every credential on the
   quick-login list rendered as a blank white lozenge. Whenever ink is inverted, the thing
   behind the ink has to be inverted with it or the pair stops agreeing. */
.login-card .demo-creds .cred-value {
  background: rgba(255,255,255,.11);
  border: 1px solid rgba(255,255,255,.15);
  color: #e9ecf6;
}
.login-card .demo-creds .cred-value:hover { background: rgba(255,255,255,.22); }
/* The mark keeps the design system's gradient and gains the lip, so it reads as glass over
   colour rather than a flat tile — the same treatment the sidebar brand mark gets. */
.login-brand .logo-icon {
  box-shadow: inset 0 1px 0 var(--gl-lip), 0 8px 24px -6px rgba(29,78,216,.65);
}
@media print {
  #login-page { background:#fff; }
  .login-card { background:#fff; color:#111827; box-shadow:none; border:1px solid #cbd5e1;
                -webkit-backdrop-filter:none; backdrop-filter:none; }
}

/* ── 25 · THE PAGE FRAME IS A BOARD WITH EDGES, AND IT STARTS AT THE HERO.
       ★ THE FIRST VERSION WAS A FULL-BLEED INVISIBLE SCRIM and that is not a frame — Vijay
       asked for a board that begins at the front of the page hero, which means something with
       a visible edge, a corner and a shadow, not a sheet of tint stretched over the viewport.

   ★ AND THE BOARD IS `.content-area` ITSELF, WHICH IS WHY THIS FINALLY DOES NOT DISTURB THE
   HERO. That element already carries the hero band as its own `background-image` (line ~2692,
   210px tall, top-aligned). So giving it a surface, an edge and a corner turns it into the
   board WITH the hero as the board's own top — the hero is not moved, not restyled, not
   re-declared, and nothing here touches `background-image`. Everything added below is a
   property the base rule never sets: background-color (which paints UNDER the hero image, so
   the band still reads on top of it), the filter, the border, the radius, the shadow, the
   inset. That is the whole difference between adding a layer and rewriting a design, and it
   took me four attempts to get to it.

   ★ THE TINT STAYS AT .16. A card is 55% white; a denser board leaves it nothing to refract
   and the theme stops looking like glass — measured, std(R) inside a card fell from 8.5 to
   about 1 when this was .52. The blur is what stops a bare disc reading as a flat circle, not
   the tint. Density was the wrong dial. */
/* THE CANVAS. Not a board — no edge, no corner, no inset. Its only job is to keep a bare disc
   from reading as a flat circle on the page, and the BLUR does that (it destroys the disc's
   edge) while the .16 tint barely touches the colour a card needs to refract. At .52 this
   killed the effect outright: std(R) inside a card fell from 8.5 to about 1. */
/* ★ NO CANVAS LAYER AT ALL — AND THE BLUR WAS THE REASON THE CARDS LOOKED FLAT.
   This rule existed to stop a bare disc reading as a flat circle, and blurring the canvas does
   achieve that — but it blurs the ground BEFORE ANY CARD SEES IT, so every card's own
   backdrop-filter then had a uniform field to work on and returned it unchanged. That is the
   identity-operation trap this theme has now hit FOUR times: a smooth gradient (the original
   ground), an occluded ground (the opaque .app-layout), a clipped channel (saturate), and now a
   pre-blurred backdrop. Same lesson every time — a blur needs structure BEHIND it, and putting
   a blur in front of the structure destroys it for everything downstream.
   So the canvas is gone. The discs stay sharp, the cards refract them, and the cards are what
   the reader is looking at anyway — they cover most of the column. */

/* ── 25 · THE BOARD STARTS AT THE BOTTOM OF THE HERO.
       ★ FOUR WRONG ANSWERS PRECEDED THIS ONE, and every one of them disturbed the hero: I
       deleted the hero wash, restored it, bolted a scrim onto it, then made `.content-area`
       itself the board — which framed the hero INSIDE the board, when what was asked for was a
       board BELOW it. The hero is a full-bleed band across the top of the page; the board is
       the page's working surface underneath it. Two different things.

   ★ IT NEEDS A WRAPPER, AND THERE WAS NONE TO REUSE. Measured: on Operations the hero has
   three siblings after it (`.op-bar`, `#op-skel`, `#op-body`); on Work Orders the hero is
   INSIDE `#wo-root`. So no existing element contains "everything after the hero" on even two
   pages, and CSS cannot draw one box around an arbitrary run of siblings. `wrapBoard()` in
   views/layouts/main.ejs inserts it — once, idempotently, and again after ui.pageEnter for the
   pages that build their content later.
   Safe to insert: nothing in the app depends on being a direct child of `.content-area` except
   two `[data-theme="warm"] .content-area > h1/h2` rules, and the only h1 in the region is
   `.ox-title`, which lives inside the hero and therefore stays outside the wrapper. */
/* ★ A FRAME IS AN EDGE, NOT A FILL. The first version gave the board `--glass-bg` — a filled
   .55 pane — and then pushed the cards inside it to `--glass-strong` at .72 to compensate. A
   17% difference is nothing: the cards stopped reading as objects and the page became one flat
   sheet. Vijay's words: "you are giving full page background to every section, all my cards
   visibility has gone." Exactly right, and it is the same mistake as the sidebar sections in a
   new place — I keep reaching for a FILL when what is wanted is a BOUNDARY.
   So the board has no surface at all. It draws its edge, its corner and its shadow, and the
   cards inside keep the full contrast of `--glass-bg` against the canvas behind them. Nothing
   competes: ground, canvas, frame outline, cards. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .pg-board {
  background: transparent;
  border: 1px solid var(--glass-brd);
  border-radius: var(--radius);
  /* a hairline of light just inside the edge, and a soft cast so the frame has depth — but no
     inset fill, so it never sits between the canvas and a card */
  box-shadow: inset 0 1px 0 var(--gl-lip), var(--el-3);
  padding: var(--space-4);
  margin-top: var(--space-3);
}
/* On a phone the board's side edges eat 32px of a 390px screen for no gain. */
@media (max-width: 768px) {
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .pg-board {
    padding: var(--space-2); margin-inline: calc(-1 * var(--space-2));
    border-left: none; border-right: none; border-radius: 0;
  }
}


/* ★ A MENU NESTED INSIDE A backdrop-filtered ANCESTOR CANNOT HIDE THAT ANCESTOR'S OWN CONTENT.
   `.cacct-pop` and `.nav-fly` live inside `.sidebar`, and `.sidebar` has a backdrop-filter,
   which makes it a BACKDROP ROOT: a filter on a descendant samples what is behind the ROOT — the
   page behind the rail — never the nav rows painted inside it. So the popup blurred the wrong
   layer, and the rail's own labels stayed legible straight through a surface computing at 93%
   white. Measured and then proved by forcing the fill opaque, which stopped it dead.
   ★ PLACED LAST ON PURPOSE. FOUR earlier rules in this block also paint these two menus
   (the overlay list, the shared pane recipe, the floating-surface list, and a rule pinning
   .cacct-pop directly), all at (0,2,0) or (0,3,0). At equal specificity the last one wins, so
   this sits at the end of the theme rather than being duplicated into four places — and the
   first attempt at this fix was written mid-block and lost to the rule 200 lines below it,
   which is why the measurement still read 0.93 after the "fix".
   These two therefore do not get translucency at all. They keep the glass EDGE — the hairline,
   the lip, the shadow — because that is what makes them belong to the theme; what they cannot
   have is a see-through fill, and a menu covering a menu should not have one anyway. */

/* ── 15 · PRINT. The print block forces white surfaces but never reset --border, so
       every rule on the page printed as WHITE ON WHITE — invisible tables. */
@media print {
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) { --border:#cbd5e1; --border-strong:#94a3b8; --card:#fff;
                         --glass-bg:#fff; --glass-strong:#fff; --glass-brd:#cbd5e1; --bg:#fff; }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) body, :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout { background:#fff; }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) body::before { display:none; }
  /* ★ AND THE RAIL. §18 built it from its own tokens, so paper has to answer them too or the
     sidebar prints as a tinted translucent block over nothing. Paper has no backdrop to filter. */
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) { --rail-a:#fff; --rail-b:#fff; --rail-spec:transparent; --rail-blur:none;
                         --rail-seam:#cbd5e1; --rail-lip:transparent;
                         --rail-cast:0 0 0 0 transparent; --rail-hover:#f1f5f9;
                         --rail-ink:#111827; --rail-ink-dim:#374151; --rail-ico:#4b5563; }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar { box-shadow:none; }
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.side-foot, .sidebar-brand, .nav-ro, .side-toggle, .nav-fly) {
    background:#fff; box-shadow:none;
    -webkit-backdrop-filter:none; backdrop-filter:none; }
}

/* ============================================================================
   DARK GLASS — the same theme on a dark ground. It is a TOKEN SWAP, not a copy.
   Every rule above matches `:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"])`, so the
   material recipe, the rail, the joints, the sections, the overlays, the scrollbars and all
   six degradation doors are shared. Only the values below differ. That is deliberate: a
   second 400-line copy would agree with this one today and drift by the third edit.

   ★ WHY THE LADDER STAYS WHITE HERE. A film of WHITE at low alpha over a dark ground IS dark
   glass — .11 white over #0d1226 is a dark translucent pane, and the same token at .28 is the
   lip of light on its top edge. That is the one property that makes the ladder work across
   both grounds: on light it reads as density, on dark it reads as light, and a lip must stay
   light in both. The ONE exception is the overlay step — see --glass-over below.
   ============================================================================ */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) {
  /* ★ DARK GLASS IS A DARK FILL, NOT A THIN WHITE ONE. This is the correction the reference
     theme made obvious. I had the ladder stay white here and argued that white at .11 over black
     IS dark glass — it is not, it is a WASH, and it is why Vijay kept saying everything looked
     white however far I dropped the ground. A pane on a dark ground is a pool of darker,
     slightly-tinted glass you look INTO; the light lives only on its edges.
     The steps now run from a barely-there film to an overlay you cannot read through, in the
     same seven positions, so every one of the ~116 sites that reads them keeps working. */
  /* ★ THIN ENOUGH TO SEE THROUGH. The first dark ladder ran .30-.92 and Vijay's verdict was that
     it looked like a general dark theme with no glass in it — correct: at .55 a card already
     hides most of what is behind it, and at .82 (the dense-content step) it hides all of it.
     The published dark recipes sit at .60-.65 for a CARD and that is the ceiling, not the
     middle. The lower steps come down hard so a pane is a pane and not a panel; only the
     overlay steps stay dense, because those exist to hide the page. */
  /* ★ A CARD IS LIGHTER THAN ITS GROUND, NOT DARKER. The fills were rgba(10,11,18,…) — darker
     than the #0b0b11 field behind them — so every card read as a hole punched in the page and
     took its colour from whatever the ground was doing. Look at any dark UI that works: the
     panel is a step UP from the background. These are now a neutral grey-blue slightly above the
     field, so a card lifts, stays neutral instead of going violet, and still transmits the
     ground at these opacities. The overlay steps stay dark and dense — those hide the page. */
  --gl-f1:rgba(38,41,56,.20); --gl-f2:rgba(38,41,56,.32);
  --gl-f3:rgba(34,37,52,.48); --gl-f4:rgba(32,35,50,.58);
  --gl-f5:rgba(28,31,44,.68); --gl-f6:rgba(24,26,38,.78);
  --gl-f7:rgba(16,17,26,.92);
  /* the edges stay light, and the top edge is brighter than the sides — that directional lip is
     what makes a dark pane read as glass rather than as a flat dark rectangle */
  --gl-lip:rgba(255,255,255,.20);
  --gl-lip-side:rgba(255,255,255,.09);

  --primary:#60a5fa; --primary-dark:#3b82f6;
  /* ★ THE INFO TINT HAS TO BE DARK, NOT A PALE BLUE WASH. `--primary-light` is the fill behind
     every id-scoped `.td-src`, `.ox-b.info` and `.ui-pill` that paints its ink with
     --primary-dark, and those selectors are (1,1,0) inside a page's own <style> — no theme rule
     can reach them, so the only lever is what sits BEHIND the ink. A light wash on a dark ground
     put that pairing at 3.58:1; a dark wash puts the same ink well clear. */
  --primary-light:rgba(29,78,216,.30);
  /* Ink. Not #fff — pure white on a dark translucent pane haloes and reads as glare. */
  --text:#e9ecf6; --text-secondary:#bcc3d6; --text-muted:#8f97ac;

  /* ★ AND THE OVERLAY STEP MUST INVERT. Everything else in the ladder is a film of light; an
     overlay is the one surface whose whole job is that you CANNOT read the page through it,
     and on a dark ground a light film cannot do that job — .42 white over dark is mid-grey and
     the rows behind it stay legible, which is the exact "merging the views" fault this theme
     was just repaired for on the light side. So the overlay is a DARK dense film here. */
  --glass-over:rgba(16,20,34,.94);
  /* the two rail menus cannot be translucent on either ground — see --menu-bg in the light
     block for why (a backdrop-filtered ancestor is a backdrop root) */
  --menu-bg:#141827;

  /* ★ 1 · BORDERS MUST STAY LIGHT. THE LADDER INVERSION TOOK THEM WITH IT.
     `--border` and `--border-strong` read ladder steps, so when the fill ladder went from white
     films to DARK fills, every edge in the app became a dark line on a dark ground. Measured
     consequence, and it is the worst number in the sweep: the Operations sparkline's TARGET RULE
     is stroked `var(--border-strong)` (public/js/core/ui.js:1702) and came out at 1.24:1 —
     rgb(34,37,56) on rgb(17,16,33). That chart exists to show a reading against what it should
     be, and the "should be" line was invisible. Same root cause as the lip: fill and edge are
     different families and only the FILL inverts on a dark ground. */
  /* --border-strong is not only an edge: public/js/core/ui.js:1702 strokes the sparkline's
     TARGET RULE with it, and a chart's reference line is meaningful non-text content at a 3:1
     floor. .24 measured ~2.8:1 on this ground, so it goes to .32 — which also gives table and
     card edges a visible hairline instead of a suggestion. */
  --border:rgba(255,255,255,.13); --border-strong:rgba(255,255,255,.32);

  /* ★ 2 · THE THREE `-dark` SEMANTIC INKS, WHICH `:root` OWNS AND THIS THEME NEVER ANSWERED.
     app.css:102-104 declare --success-dark #059669, --warning-dark #b45309, --danger-dark
     #dc2626 — light-theme inks. Measured leaks: 114 instances on /plant/tech alone (.td-ed,
     .td-watch, .td-res, .td-unset, .td-refed), 59 more on .td-src.derived, plus .pf-flag and
     .pf-flag.sev on /plant/performance, at 2.58-3.86.
     ★ AND THIS IS WHY THE PER-SELECTOR REDIRECTS IN §28e DID NOT REACH THEM. Those families are
     declared in a page's own <style> scoped by an id — `#pf-body .pf-flag`, `#td-body .td-ed`,
     `#wo-root .wo-colhead` — which is (1,1,0) and beats a theme rule at (0,3,0). Every family
     still leaking is id-scoped; every one §28e fixed was class-only. Answering the TOKENS
     reaches all of them at once, which no amount of selector-chasing can. */
  --success-dark:#5ee0ab; --warning-dark:#ffc766; --danger-dark:#ff8f8f;

  /* ★ 3 · A 62%-ALPHA GREY HINT. `--text-hint` is color-mix(--text-muted 62%, transparent)
     (app.css:2387) and had no dark answer, so every column header and row-meta line in the app
     scraped 2.90-3.21 — "REF / WORK ORDER / PRIORITY / STATUS" and every row's department line,
     which the sweep calls the dimmest real content in the app. Opaque, because alpha ink over a
     translucent pane over a variable ground has no stable ratio at all. */
  --text-hint:#9aa2b8;

  /* ★ 4 · THE ACCENT INK. On a dark ground `--primary-dark` is the WRONG DIRECTION: this theme
     set --primary #60a5fa and --primary-dark #3b82f6, i.e. "dark" really is darker, so every
     place that uses it as INK on a dark tint failed — .ox-ic 2.76, .ui-find-n 3.46, .ui-pill
     3.58, .td-src.template 3.58, .cav 3.68, 13 routes. A separate token is the only honest fix:
     the gradient still needs a genuinely dark end, and ink still needs to be light. */
  --accent-ink:#a8c8ff;

  /* ★ 5 · the two tint washes left at their light-theme alpha, so info and hot status pills were
     visibly fainter than the other three (the sweep flagged the inconsistency, not a ratio). */
  --tint-info-bg:rgba(96,165,250,.24); --tint-hot-bg:rgba(249,115,22,.28);

  /* ★ 6 · the theme picker promised a LIGHT theme for the dark one you are standing on: all 14
     swatch tiles computed the same light gradient, because --swatch-bg is declared in the SHARED
     block and only --swatch-card was given a dark answer. Half a token family, again. */
  --swatch-bg:linear-gradient(135deg,#141a2e 0%,#111827 45%,#1d1630 100%);
  /* ★ THE TINT FOREGROUNDS — THE THIRD TIME THIS FAMILY HAS CAUGHT ME OUT. The `-bg` half was
     missed, then the four `-light` tokens, and now the `-fg` half: neither glass theme declared
     a single `--tint-*-fg`, so every one fell through to :root's DARK inks — #475569 slate,
     #065f46 green, #92400e amber. Measured on the department workspace under glass-dark:
     74 elements rendering rgb(71,85,105) at opacity .65-.7 on a dark pane, i.e. invisible, plus
     dark-green figures on the Asset Register tile. That is the whole "some fonts not visible"
     screenshot in one line of missing tokens.
     The lesson, written down: when a theme redeclares part of a token FAMILY it has silently
     opted the rest of that family out of theming. Check the whole family, every time. */
  --tint-neutral-fg:#c3c9da; --tint-info-fg:#a8c8ff;
  --tint-ok-fg:#5ee0ab;      --tint-warn-fg:#ffc766;
  /* #ff8f8f measured 4.34:1 as the "Critical" label on its own tint (rgb(114,50,43)) — under. */
  --tint-danger-fg:#ffa0a0;  --tint-hot-fg:#ffab72;
  --over-bg:var(--glass-over); --over-blur:saturate(1.25) blur(26px);

  /* The rail. The lip stays light; the crack goes darker, because on a dark ground a seam
     between two dark surfaces is a shadow, not a highlight — the inverse of the light theme. */
  --rail-seam:rgba(0,0,0,.52);
  --rail-cast:10px 0 34px -14px rgba(0,0,0,.62);
  --rail-ink:#e2e6f2; --rail-ink-dim:#a9b1c6; --rail-ico:#8b93a8;
  --sidebar-text:#c2c9db; --sidebar-active:#f2f5ff;
  --nav-active-bg:rgba(96,165,250,.20);

  /* Status tints carry more alpha on dark: a .17 wash that reads clearly on a white pane
     disappears against #0d1226, and a status colour that cannot be seen is not a status. */
  --tint-warn-bg:rgba(245,158,11,.26); --tint-danger-bg:rgba(239,68,68,.26);
  --tint-ok-bg:rgba(16,185,129,.24);

  --toast-bg:rgba(16,20,34,.92); --toast-fg:#e9ecf6;
  color-scheme: dark;   /* so native controls, the caret and the UA scrollbar follow */
  /* ★ AND THE FILTER CAN BE MUCH STRONGER ON DARK. The light theme had to be held at
     contrast(1.07) saturate(1.1) because saturate CLIPPED the blue channel to 255 over a pale
     ground — that was measured and it is a light-ground problem only. Here the channels sit far
     from the ceiling, so the reference theme's blur(32px) saturate(180%) is safe, and the extra
     saturation is what stops a dark frosted pane looking like dirty grey. */
  --glass-blur: saturate(1.7) blur(30px);
  --rail-blur: saturate(1.8) blur(32px);
  --over-blur: saturate(1.6) blur(28px);
  /* ★ THE SCALE, RESTATED FOR A DARK GROUND — the whole reason elevation is tokens. A slate cast
     at 5-12% alpha is invisible on near-black; a dark room needs a deeper shadow to read as
     depth. Same four meanings, different numbers. */
  --el-1: 0 1px 2px rgba(0,0,0,.34), 0 1px 3px rgba(0,0,0,.26);
  --el-2: 0 2px 6px rgba(0,0,0,.38), 0 6px 14px rgba(0,0,0,.30);
  --el-3: 0 6px 14px rgba(0,0,0,.42), 0 16px 34px rgba(0,0,0,.36);
  --el-4: 0 12px 28px rgba(0,0,0,.50), 0 40px 80px rgba(0,0,0,.55);
}

/* THE GROUND. Same construction as the light one — hard-stopped discs FIRST so the blur has
   edges to eat, then soft pools, then the base wash — with the alphas raised, because a
   coloured pool that reads on white vanishes on near-black. */
/* ★ TRUE BLACK — Vijay's call, shown both ways side by side. On #000 the panes separate harder
   (the card ladder starts at rgb(38,41,56), so every step now reads as a lift rather than a
   tint) and the amber accent carries further. The colour pools come down about a third to suit
   it: the same alpha over black reads much stronger than it does over #0b0b11. */
html:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) { background: #000000; }
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) body { background: transparent; }
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .app-layout { background: transparent; }
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) body::before {
  /* ★ NO MORE BUBBLES. The hard-stopped discs were there for one technical reason — a blur needs
     an EDGE to act on, and a smooth gradient blurred is the same gradient — but they read as
     four coloured circles parked on the page. Vijay called them bad and the contrast sweep said
     the same thing independently: "artefacts rather than light being refracted."
     ★ THE REQUIREMENT IS AN EDGE, NOT A CIRCLE. Hard-stopped DIAGONAL BANDS satisfy the same
     maths — a step change the kernel can spread — and read as raked light across a surface,
     which is what glass actually does. Two band sets at different angles and periods so the
     pattern never repeats visibly, plus a fine grid for high-frequency detail the blur turns
     into sheen, and the soft pools underneath for colour. */
  background:
    /* raked light — hard stops, so the blur has real edges to eat */
    repeating-linear-gradient(115deg,
      rgba(150,160,255,.05) 0 2px, transparent 2px 190px),
    repeating-linear-gradient(64deg,
      rgba(120,190,255,.04) 0 2px, transparent 2px 260px),
    /* a fine grid: below the eye's threshold at rest, but it gives the kernel something to work
       with everywhere rather than only where a disc happened to sit */
    repeating-linear-gradient(0deg,
      rgba(255,255,255,.014) 0 1px, transparent 1px 84px),
    repeating-linear-gradient(90deg,
      rgba(255,255,255,.014) 0 1px, transparent 1px 84px),
    /* the colour, soft and low — depth, not decoration.
       ★ THE BOTTOM TWO POOLS ARE WARM, AND THAT IS THE WHOLE POINT OF THEM. This ground was
       translated from the light one by keeping the four positions and dropping the alphas — but
       the light ground's bottom pair is PINK and TEAL against an indigo/sky top, and both of
       those came across as more indigo and violet. All four pools ended up in one hue family,
       so the ground's colour travelled 28 units left-to-right and only 7.5 top-to-bottom
       (measured on the bare ground with every pane hidden). The page stacks its panes in a
       COLUMN, so each one spanned a field that never changed down its height, and a blur of an
       unchanging field returns that field — the panes read as flat dark rectangles and the
       whole theme read as "a dark theme" rather than dark glass. Vijay said exactly that, and
       the same conclusion came back independently from the reference build.
       A pane only tells you there is something behind it by being a different colour at one
       edge than the other. So the vertical hue split the light ground already has is restored
       here: cool at the top, warm at the bottom. Alphas stay low — this is depth, not a sunset. */
    radial-gradient(1200px 900px at 22%  4%, rgba(79,70,229,.22), transparent 100%),
    radial-gradient(1000px 820px at 84% 14%, rgba(59,130,246,.16), transparent 100%),
    radial-gradient(1000px 900px at 66% 90%, rgba(217,119,6,.17), transparent 100%),
    radial-gradient( 950px 800px at 12% 82%, rgba(20,184,166,.15), transparent 100%),
    /* the rail's column keeps a touch more, so the rail still has something to refract */
    radial-gradient(560px 900px at -4% 30%, rgba(99,102,241,.30), transparent 100%),
    /* and the base carries the same cool-to-warm walk, so the split survives where the pools
       have faded out — it was #000000 → #04040a → #06050c, three shades of one black */
    linear-gradient(160deg,#000208 0%,#04040a 55%,#0a0705 100%);
}
/* The mobile scrim inverts with the ground: a light wash over a dark page would flash. */
@media (max-width: 768px) {
  :is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .sidebar-overlay { background: rgba(6,9,18,.46); }
}
/* Print is paper in both themes — the shared print block resets the tokens, but the ground
   and the ink are declared here, so they have to be answered here too. */
@media print {
  :is([data-theme="glass-dark"],[data-theme="liquid-dark"]) { --text:#111827; --text-secondary:#374151; --text-muted:#4b5563;
                              --gl-f1:#fff; --gl-f2:#fff; --gl-f3:#fff; --gl-f4:#fff;
                              --gl-f5:#fff; --gl-f6:#fff; --gl-f7:#fff;
                              --glass-over:#fff; --rail-seam:#cbd5e1;
                              --rail-ink:#111827; --rail-ink-dim:#374151; --rail-ico:#4b5563; }
  :is([data-theme="glass-dark"],[data-theme="liquid-dark"]) body { background:#fff; }
  :is([data-theme="glass-dark"],[data-theme="liquid-dark"]) body::before { display:none; }
}

/* ★ THE NEUTRAL CONTROLS NEEDED THEIR OWN ANSWER ON DARK. The shared rules build a chip, a
   button and a field out of the film ladder, which on dark is white at .05–.21 — correct for a
   PANE, and far too faint for something you are meant to press. Measured against the render:
   "How this works" and "Refresh" read as ghosts. They take a denser film and light ink; the
   accent states are untouched because those already carry their own colour. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) :is(.ox-chip, .btn, .btn-secondary, .btn-sm, .ghost-btn):not(.btn-primary):not(.ox-hero):not(.on):not(.active):not(.is-on):not(.sel):not([class*="-on"]) {
  background: rgba(255,255,255,.10);
  border-color: rgba(255,255,255,.18);
  color: #e2e6f2;
}
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) :is(.ox-chip, .btn, .btn-secondary, .btn-sm, .ghost-btn):not(.btn-primary):not(.ox-hero):hover {
  background: rgba(255,255,255,.17);
  color: #f4f6fc;
}
/* Fields are inputs, not panes: a person has to see where to type. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) :is(.form-control, .ox-search input, .search-input, select, textarea, .ds-sel) {
  background: rgba(255,255,255,.09);
  border-color: rgba(255,255,255,.20);
  color: #e9ecf6;
}
/* The tagline and every muted label sit on a dark pane, so the light theme's muted grey is
   under the floor — these are the tokens the shared rules read. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) :is(.ox-sub, .khtfw-eyebrow) { color: #b3bbd0; }
/* Icons carry meaning and were rendering as empty outlines. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) :is(.nav-icon, .ox-chip svg, .btn svg) { color: currentColor; opacity: .92; }

/* The read-only marker is a white disc on the light theme; on dark that inverts into the
   brightest object in the rail, and it appears on eight rows. On a dark pane it has to be a
   HOLE — darker than the rail, ringed by the same crack every other edge uses. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .sidebar .nav-ro {
  background: rgba(0,0,0,.32);
  color: #a9b1c6;
  box-shadow: 0 0 0 1.5px rgba(255,255,255,.14);
}
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .sidebar .nav-item.active .nav-ro {
  background: rgba(0,0,0,.42);
  color: #dbe3f7;
  box-shadow: 0 0 0 1.5px rgba(96,165,250,.45);
}

/* ── 27 · A MODAL IS AN OVERLAY, NOT A PANE — ON BOTH GROUNDS.
   `.apx-modal`, `.modal-dialog` and `.khtfw-modal` were in the shared pane recipe, so they took
   `--glass-bg`: on light that is 46% white over live page content (the "merging the views"
   fault, in the one place where it matters most), and on dark it is 11% WHITE — measured, the
   Appearance dialog rendered as a pale sheet with light ink on it, over a light scrim.
   Density is a function of what is behind a surface, and behind a modal is the whole page. */
/* ★ THE RIGHT-HAND DRAWER (ui.drawer, `.ox-drawer`) IS THE SAME KIND OF SURFACE. Unnamed here it read `--card` -
   58% white with no blur on glass - so the page behind showed through crisp and Receive Goods and Start Enquiry
   could not be read (measured 01-10). */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.apx-modal, .modal-dialog, .khtfw-modal, .ox-drawer) {
  background: var(--glass-over);
  -webkit-backdrop-filter: var(--over-blur); backdrop-filter: var(--over-blur);
  border: 1px solid var(--glass-brd);
}
/* …and the scrim behind them follows the ground. A light scrim over a dark page flashes and
   lifts the whole document toward grey, which is what made the dialog unreadable. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) :is(.apx-overlay, .khtfw-overlay, .modal, .ovl, .ox-drawer-ov) {
  background: rgba(6,9,18,.58);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
/* The segmented control had no surface of its own, so its unselected options were light ink on
   the dialog with nothing to separate them. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .apx-seg { background: rgba(0,0,0,.28); }
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .apx-seg > button { color: #b3bbd0; }
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .apx-seg > button.on { background: var(--primary-dark); color: #fff; }

/* ★ OPACITY IS NOT A DIMMER ON A DARK GROUND. `.md-cube.zero .n` is opacity .65 and `.l` is .85,
   which on the light theme reads as "this figure is nil" and on dark pushes a light ink back
   under the contrast floor — measured rgb(195,201,218) at .65 on a dark pane. A zero is still
   information: somebody has to be able to read that there are none. The dimming stays, as a
   smaller step, and the ink carries it instead of the alpha. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .md-cube.zero .n { opacity: .82; }
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .md-cube .l { opacity: .95; }
/* The info cube reads --primary directly rather than --tint-info-fg, so it stayed the light
   theme's blue on a dark pane. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) .md-cube.info { color: var(--tint-info-fg); }

/* ── 28 · VISIBILITY REPAIRS FROM THE 27-ROUTE CONTRAST SWEEP. Every number below was measured
       on a real page against a 45x29 map of the ground's actual luminance, not against the flat
       base colour — measuring against the base overstates every ratio on this theme. */

/* ★ 28a · 32 VIEWS HAND-ROLL A DIALOG AS `.ovl` + AN INNER `.card`, so §27's modal rule never
   reaches them and the dialog falls to the PANE recipe: .46 white floating over a
   rgba(15,23,42,.5) scrim. Measured on /plant/assets, the whole dialog composites to mid-grey
   #a1a6b4, its seven field labels land at 2.34-2.46:1 and an amber note at 1.76:1. The class is
   the tell — `.modal-dialog` was themed and `.ovl > .card` was not, so whether a dialog is
   readable depended on which of two conventions its author happened to use. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ovl > .card,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ovl > div > .card {
  background: var(--glass-over);
  -webkit-backdrop-filter: var(--over-blur); backdrop-filter: var(--over-blur);
  border: 1px solid var(--glass-brd);
}

/* ★ 28b · THE FIELD BORDER WAS LIGHTER THAN THE FIELD FILL, so `edgeOut` measured 1.00:1 on 20
   fields across 8 routes — the border was invisible and the input was identified by its fill
   alone. `background: var(--gl-f5)` with `border-color: var(--gl-f6)` is backwards: an edge has
   to be darker than what it encloses, or it is not an edge. */
/* ★ AND IT NEVER APPLIED TO .form-control. The frosted-field rule above (`:is(.search-input, .ox-search input,
   .form-control, …)`) takes the specificity of its MOST specific argument — `.ox-search input` is (0,1,1) — so it
   out-ranked this (0,1,0) fix and every text field kept the white --glass-brd edge: measured 28-09 on the Fuel form,
   border rgba(255,255,255,.58) on a rgba(255,255,255,.46) field inside a card — the Litres, Reading and PIN boxes
   were invisible. `.ox-search input` is named here too so this rule carries the same weight and, coming later, wins. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.form-control, .ox-search, .ox-search input, .ds-sel, select, textarea) {
  border-color: var(--border-strong);
}
/* …and a bare <input> in a form table had `border: transparent; background: transparent` —
   measured 1.00:1, i.e. 37 editable number fields rendering identically to static text on
   /admin/targets. This one fails on the `white` theme too; it is not glass's fault, but it is
   the worst single number in the sweep so it is answered here for both grounds. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .form-table td input {
  background: var(--gl-f4);
  border: 1px solid var(--border-strong);
}

/* ★ 28c · THE TOPBAR ICON BUTTONS. Full screen, QR and Language are icon-ONLY controls, so they
   answer to WCAG 1.4.11 at 3:1 and measured 2.87-2.95 on all 27 routes — --primary as an icon
   ink on a pale pane. The bell and every rail icon already clear it. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .topbar-icon-btn { color: var(--primary-dark); }

/* ★ 28d · WHITE ON --primary IS 3.67:1 AND FAILS, EVERYWHERE IT IS USED AS A FILL. The account
   avatar (27 routes), the selected calendar day, the Appearance size segment. --primary-dark is
   6.70:1 with white. I flagged this on the avatar hours ago and left it as pre-existing; it is
   in the sweep on all 27 routes, so it is not staying. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.cacct .cav, .cal-num.on, .apx-seg > button.on) {
  /* --accent-solid, for the reason written against the same three selectors in §26:
     --primary-dark inverts on a dark theme and took the avatar to 3.68:1 on Glass Dark. */
  background: var(--accent-solid);
}

/* ★ 28e · A SEMANTIC COLOUR USED AS INK ON ITS OWN TINT. `--warning` on `--tint-warn-bg`
   measured 1.49-1.73 — the worst text in the app: the Operations status pill, its delta figure,
   and the Work Orders count pill. The tone must come from the *-fg token, which is chosen to be
   readable; the raw --warning/--danger are FILL colours. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.op-mh .st, .op-lead .d, .wo-qn,
      .tl-state.ref, .pf-flag, .ui-find-sev.warn) { color: var(--tint-warn-fg); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.ui-find-sev.act, .pf-flag.sev) { color: var(--tint-danger-fg); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.td-src.derived, .rec-score) { color: var(--tint-ok-fg); }

/* ★ 28f · INLINE LINKS PAINTED `color:var(--primary)` IN THE VIEWS — 1.80:1, and visually
   confirmed barely readable ("AOP workspace", "16 findings elsewhere"). They are inline styles,
   so no stylesheet rule can reach them; the token is redirected for this theme instead. */
/* `--accent-ink` falls back to --primary-dark, which is correct on the light ground; the dark
   block overrides it with a LIGHT blue, because on a dark tint "primary-dark" is the wrong end. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.st-eyebrow, .ui-find-n, .td-caret, .st-n,
      .cacct-sec, .ox-ic, .ui-pill, .sh-face, .oc-tag, .td-src.template, .td-src-cell.own,
      .panel-head span, .khtfw-tab, .ox-b.info, .pit.is-on > span) {
  color: var(--accent-ink, var(--primary-dark));
}
/* ★ "HOW THIS WORKS" IS THE FIRST CONTROL ON EVERY MODULE PAGE (the header contract puts it
   first), and it is the one chip in that group whose fill is only rgba(59,130,246,.16) — so what
   sits behind the text is mostly the THEME'S GROUND, and the ground is three blurred orbs that
   move across the page. Measured on /plant + /equipment in Liquid light: the composite came out
   rgb(148,188,202) and --accent-ink (#0b5a6e) read 3.81:1 on it. A darker teal takes it to 5.80:1
   with headroom for wherever the orb happens to fall. Only the light half needs it: on the dark
   pair --accent-ink is already a light cyan on a dark ground.

   ★ THE CLASS IS REPEATED ON PURPOSE. The rule this answers is
   `:is([data-theme=…]) :is(… .td-src.template, .khtfw-tab …)`, and `:is()` takes the specificity of
   its MOST SPECIFIC argument — and that list holds `.pit.is-on > span`, which is (0,2,1), so the
   whole selector is (0,3,1), not the (0,3,0) I read off `.td-src.template`. Two attempts lost
   before I stopped counting by eye and asked Chrome (CSS.getMatchedStylesForNode) which rule was
   actually winning. Doubled class plus the element gives (0,3,1) — a tie, and this is later. */
[data-theme="liquid"] { --accent-ink-deep: #083d4a; }
[data-theme="liquid-dark"] { --accent-ink-deep: var(--accent-ink); }   /* already light on a dark ground */
[data-theme="liquid"] button.khtfw-tab.khtfw-tab { color: var(--accent-ink-deep); }
/* `.ox-scope` is the same case and the same arithmetic: the header contract makes it a FACT — which
   unit you are looking at — on a translucent chip, and --primary (#0e7490) measured 3.21:1 against
   the composite on /plant/tech. Its own theme rule is (0,2,0), so the doubled class clears it. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-scope.ox-scope { color: var(--accent-ink-deep); }

/* The brand subtitle sits on the rail at 4.20 — just under. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-brand .brand-sub { color: var(--rail-ink); }

/* ── 29 · DENSE CONTENT GETS A DENSER SURFACE. From the published guidance on where
       glassmorphism actually fails, and it names this app's main screen type outright:
       "tables, code and long-form reading on translucent panels produce blur halos", and
       "an uncontrolled backdrop can swing from 8:1 contrast to under 2:1 as content scrolls
       behind it." That second sentence is the 2.21:1 the sweep measured on 345 elements — my
       ground's luminance runs 0.17 to 0.63, so ink on it swings exactly as described.
       Darkening the ink (§28) fixes the ratio. This fixes the CAUSE for the worst case: a pane
       whose job is to carry a table stops being a thin film and becomes a proper surface, so
       the text on it is isolated from whatever the ground is doing underneath.
       ★ Deliberately NOT applied to every pane — a page of dense tables would then be one flat
       slab and the theme would be pointless. Only the container that actually holds the table. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .card:has(table),
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .card:has(.data-table),
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .card:has(.form-table),
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .pg-board:has(> .card > table) {
  background: var(--gl-f6);
}
/* and the sticky header of a long table has scrolling rows passing under it, which is the one
   place in a table that genuinely needs its own frosting */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.data-table thead th, .form-table thead th) {
  background: var(--gl-f7);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}

/* ★ AND THE EDGE IS AN ACCESSIBILITY FEATURE, NOT A DECORATION — it is what survives
   forced-colors mode, which is why the published recipes all put a 1px light border on the
   glass rather than relying on the fill alone. Dark was at .13; the references sit at .12-.25
   with a brighter top. Raised, with the top lip kept clearly above the sides. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) { --glass-brd:rgba(255,255,255,.17); --gl-lip:rgba(255,255,255,.24); }
@media (forced-colors: active) {
  :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.card, .ox-stat, .pg-board, .sidebar,
       .topbar, .modal-dialog, .apx-modal, .ovl > .card) { border: 1px solid CanvasText; }
}

/* ============================================================================
   §30 · LIQUID GLASS — a SECOND glass theme, not a change to the first one.

   ★ THIS BLOCK IS GENERATED FROM ui-lab/glass-agent/glass.css, NOT TRANSCRIBED FROM IT.
   The first attempt read that file's numbers and re-typed them rule by rule, and Vijay's verdict
   was the correct one: "sonnet already wrote the code — why are you writing it again". Every
   re-typing is a chance to drop a declaration, and several were dropped. What follows is its own
   97 rules, run through a selector map (its `.glass` is this product's `.card`, its `.nav-group`
   is `.nav-section`, its `.theme-switch` is `.lg-ground`, and so on) with each rule scoped to the
   theme, and its token names resolved onto the tokens this app's ~219 shared glass rules already
   read. Declarations that describe ITS page structure — position, grid, width, animation — are
   dropped: what is being ported is the material, not the layout, because this app has 140 real
   screens and it has four mockups.

   ★ GLASS IS UNTOUCHED. Vijay asked for that twice. Everything here is additive.
   ★ ONE ENTRY IN THE PICKER. `liquid-dark` is a resolved state, not a choice — see
   views/partials/theme-resolve.ejs, which swaps the attribute from the OS setting or from the
   control in the top bar, before the first paint.
   ============================================================================ */

/* ============================================================================
   §32 · THE SKY — REFRESHED TO THE APPROVED PROTOTYPE (scratchpad build/liquid-reference.html
   .sky): a base two-stop gradient plus four large, softly-falling "lights", in place of the three
   hard-tuned blobs this block used to carry (teal/orange/indigo, fixed, no setting). The three-
   literal-colour version above is GONE — every colour here is a token (--lg-sky-1/-2, --lg-a1..a4,
   declared on [data-theme="liquid"/"liquid-dark"] above, §32b overrides them per [data-sky=…]), so
   "give in settings" the look rather than a colour well (Vijay).

   ★ EACH LIGHT IS ITS OWN background-image LAYER, SIZED AND POSITIONED, NOT "at X Y" INSIDE ITS OWN
   GRADIENT. A generated image with an internal "at" position is still the full painting area in
   size, so background-position could not SLIDE it — only an explicit background-size per layer
   turns each light into a tile smaller than the viewport that background-position can move, which
   is what the drift (§32a) animates.
   ★ NO filter:blur() ON A FULL-VIEWPORT ELEMENT. §27's own note two sections up explains why: a
   position:fixed, inset:0 pseudo-element blurred every frame is the expensive way to do this. Each
   light's falloff is the same multi-stop-gradient trick already used here, generalised with
   color-mix() so ONE formula works for every preset's own base alpha. */
html:is([data-theme="liquid"],[data-theme="liquid-dark"]) { background: var(--lg-sky-2); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) body::before {
  /* ★ THE LIGHTS TURNED DOWN (14-10, Vijay: "reduce the background colors in liquid glass skins"). Every preset's
     four lights are drawn through --lg-sky-light here, once, rather than re-tuning twenty alpha values across
     ten preset blocks - so a preset keeps its own colours, only quieter, and one number turns them all up or down. */
  --lg-sky-light: 10%;   /* 55% first, then 25% ("reduce more man.."), then "it 10%" (14-10) */
  --lg-a1s: color-mix(in srgb, var(--lg-a1) var(--lg-sky-light), transparent);
  --lg-a2s: color-mix(in srgb, var(--lg-a2) var(--lg-sky-light), transparent);
  --lg-a3s: color-mix(in srgb, var(--lg-a3) var(--lg-sky-light), transparent);
  --lg-a4s: color-mix(in srgb, var(--lg-a4) var(--lg-sky-light), transparent);
  /* ★ `circle closest-side`, NOT A BARE `circle` — a bare circle's 100% stop defaults to
     farthest-corner, so along the MIDDLE of each box edge (background-size, no-repeat) the
     gradient had only reached its 70%-ish stop — still carrying real alpha — when the box simply
     STOPPED. Outside a no-repeat layer is nothing, so that residual colour cut straight to
     transparent at the box boundary: a dead-straight edge in what is supposed to be a sky with
     none. Measured as a hard vertical band at the fourth light's own left/right edges. closest-
     side makes the ending shape exactly the box's own half-width, so "transparent 100%" lands
     exactly ON the boundary that no-repeat already draws — the fade finishes precisely where the
     box ends, instead of being cut off mid-fade. */
  background-image:
    radial-gradient(circle closest-side at 50% 50%,
      var(--lg-a1s) 0%, color-mix(in srgb, var(--lg-a1s) 70%, transparent) 42%,
      color-mix(in srgb, var(--lg-a1s) 32%, transparent) 70%, transparent 100%),
    radial-gradient(circle closest-side at 50% 50%,
      var(--lg-a2s) 0%, color-mix(in srgb, var(--lg-a2s) 70%, transparent) 42%,
      color-mix(in srgb, var(--lg-a2s) 32%, transparent) 70%, transparent 100%),
    radial-gradient(circle closest-side at 50% 50%,
      var(--lg-a3s) 0%, color-mix(in srgb, var(--lg-a3s) 70%, transparent) 42%,
      color-mix(in srgb, var(--lg-a3s) 32%, transparent) 70%, transparent 100%),
    radial-gradient(circle closest-side at 50% 50%,
      var(--lg-a4s) 0%, color-mix(in srgb, var(--lg-a4s) 70%, transparent) 42%,
      color-mix(in srgb, var(--lg-a4s) 32%, transparent) 70%, transparent 100%),
    linear-gradient(160deg, var(--lg-sky-1), var(--lg-sky-2));
  background-repeat: no-repeat;
  background-size: 84vw 84vw, 68vw 68vw, 80vw 80vw, 44vw 44vw, 100% 100%;
  background-position: -14vw -24vw, 84vw -8vw, 26vw 68vw, 6vw 96vw, 0 0;
  /* ★ "38-60s… infinite alternate" — ONE shared 52s timeline, each layer drifting its own amount
     and direction within it, rather than four independent durations: a single animated property
     can only run on one clock, and the reference's 38/44/52/60s read as "slow and uneven", which
     this reproduces close enough that nobody would tell the two apart watching it. */
  /* ★ IN 200 STEPS, NOT 60 FRAMES A SECOND (13-10). background-position is not a compositor property: every
     frame REPAINTS this full-screen layer, and every pane over it re-runs its backdrop blur (and, with Glass Edge
     Bending, its displacement map). At 60 fps that kept a headless Chrome's renderer so busy it stopped answering
     navigation - and a plant PC with no graphics card is the same machine. The widest light moves ~11vw in 52 s,
     so 200 steps is under a pixel a step at 1440 px, about four repaints a second: the drift reads the same, at
     roughly a fifteenth of the work. */
  animation: lg-sky-drift 52s steps(200) infinite alternate;
}
@keyframes lg-sky-drift {
  from { background-position: -14vw -24vw, 84vw -8vw, 26vw 68vw,  6vw 96vw, 0 0; }
  to   { background-position:  -4vw -14vw, 75vw  3vw, 15vw 61vw,  9vw 89vw, 0 0; }
}
/* Moving Sky → off, or the system asks for reduced motion either way (CLAUDE.md's own rule for
   the drift, and the one the reference's own @media query states outright). */
:is([data-theme="liquid"],[data-theme="liquid-dark"])[data-sky-move="off"] body::before { animation: none; }
@media (prefers-reduced-motion: reduce) {
  :is([data-theme="liquid"],[data-theme="liquid-dark"]) body::before { animation: none; }
}

/* ★ THE GRAIN — an SVG fractal-noise overlay at the reference's own 7% opacity, soft-light blended
   so it never LOOKS like a texture, only stops the sky reading as a perfectly flat gradient (the
   reference's own note: "real glass never shows a perfectly flat colour"). body::after is free for
   this in every theme (grep confirms nothing else in this file uses it), so it needs no new element. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) body::after {
  content: ''; position: fixed; inset: -50%; width: 200%; height: 200%; z-index: -1; pointer-events: none;
  opacity: .07; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}
:is([data-theme="liquid"],[data-theme="liquid-dark"])[data-grain="off"] body::after { display: none; }

/* ============================================================================
   §32b · SKY PRESETS — [data-sky=…] overrides only the colour tokens §32 reads; the size,
   position and drift stay the one formula above. 'sky' (the default) needs no rule: it is what
   [data-theme="liquid"/"liquid-dark"] already declare.
   ============================================================================ */
[data-theme="liquid"][data-sky="brand"] {
  --lg-sky-1: #f6e9ec; --lg-sky-2: #f8f1e2;
  --lg-a1: rgba(225,29,72,.55); --lg-a2: rgba(212,160,23,.45);
  --lg-a3: rgba(225,29,72,.28); --lg-a4: rgba(212,160,23,.22);
}
[data-theme="liquid-dark"][data-sky="brand"] {
  --lg-sky-1: #170c10; --lg-sky-2: #171209;
  --lg-a1: rgba(225,29,72,.42); --lg-a2: rgba(212,160,23,.38);
  --lg-a3: rgba(225,29,72,.22); --lg-a4: rgba(212,160,23,.16);
}
[data-theme="liquid"][data-sky="steel"] {
  --lg-sky-1: #e7eaee; --lg-sky-2: #eef1f4;
  --lg-a1: rgba(96,140,200,.45); --lg-a2: rgba(150,160,175,.22);
  --lg-a3: rgba(120,130,145,.16); --lg-a4: rgba(170,178,190,.14);
}
[data-theme="liquid-dark"][data-sky="steel"] {
  --lg-sky-1: #0c0e12; --lg-sky-2: #11141a;
  --lg-a1: rgba(70,120,190,.40); --lg-a2: rgba(90,98,112,.22);
  --lg-a3: rgba(70,78,92,.18); --lg-a4: rgba(110,118,130,.12);
}
[data-theme="liquid"][data-sky="aurora"] {
  --lg-sky-1: #dff1ec; --lg-sky-2: #e8f4ee;
  --lg-a1: rgba(20,184,166,.55); --lg-a2: rgba(34,197,94,.42);
  --lg-a3: rgba(124,58,237,.42); --lg-a4: rgba(45,212,191,.22);
}
[data-theme="liquid-dark"][data-sky="aurora"] {
  --lg-sky-1: #061512; --lg-sky-2: #081b16;
  --lg-a1: rgba(20,184,166,.45); --lg-a2: rgba(34,197,94,.34);
  --lg-a3: rgba(124,58,237,.38); --lg-a4: rgba(45,212,191,.18);
}
/* 'calm' — "a single soft light, no colour drama": a2-a4 go to transparent, which removes those
   three layers from the render (color-mix with a transparent base stays transparent throughout the
   falloff) rather than giving calm three more rules to carry. */
[data-theme="liquid"][data-sky="calm"] {
  --lg-sky-1: #eef0f3; --lg-sky-2: #f4f5f7;
  --lg-a1: rgba(120,140,170,.26); --lg-a2: transparent; --lg-a3: transparent; --lg-a4: transparent;
}
[data-theme="liquid-dark"][data-sky="calm"] {
  --lg-sky-1: #0b0d10; --lg-sky-2: #0e1013;
  --lg-a1: rgba(120,150,190,.22); --lg-a2: transparent; --lg-a3: transparent; --lg-a4: transparent;
}

/* --- the tokens, lifted from its :root and its dark block --- */
[data-theme="liquid"] {
  --gl-f1: rgba(255,255,255,.11);  --gl-f2: rgba(255,255,255,.19);
  /* ★ f3 (card/modal/stat fill) AND f5 (topbar/sidebar fill) RAISED TOWARD THE REFERENCE'S OWN
     38% white (--glass in liquid-reference.html) — raising a WHITE fill's opacity on a light
     ground only ever LIGHTENS the composite dark text sits on, so every contrast measurement
     recorded against the old .27/.38 (§29, §31) still clears 4.5:1 with more room than before,
     never less. */
  --gl-f3: rgba(255,255,255,.38);  --gl-f4: rgba(255,255,255,.33);
  --gl-f5: rgba(255,255,255,.42);  --gl-f6: rgba(255,255,255,.80);
  --gl-f7: rgba(255,255,255,.93);
  --gl-lip: rgba(255,255,255,.9);  --gl-lip-side: rgba(255,255,255,.35);
  --glass-brd: rgba(15,23,42,.09);
  /* ★ THE SKY — the reference's own base gradient and its four drifting lights (body::before,
     §32 below). These ARE the 'sky' preset's colours; [data-sky="…"] overrides them (§32b). */
  --lg-sky-1: #d7e4fb; --lg-sky-2: #e9eefb;
  --lg-a1: rgba(80,140,255,.75); --lg-a2: rgba(60,210,200,.55);
  --lg-a3: rgba(150,110,255,.50); --lg-a4: rgba(255,190,120,.30);
  /* ★ THE SHEEN — plain alpha-composited white, NOT the reference's mix-blend-mode:soft-light.
     That blend needs its own pseudo-element (the reference's `.gl::after`) with the pane's real
     content lifted to a higher z-index above it; every pane here already carries real children in
     normal flow, and `mix-blend-mode` on the PANE ITSELF would blend its own text against whatever
     sits behind the pane, not just its own background layers. §33 below layers these as a second
     `background-image` instead (a safe, independent longhand next to the pane's own fill colour) —
     a glossy highlight rather than true soft-light, close enough that nobody would tell behind a
     cursor that is always moving. */
  --lg-sheen: rgba(255,255,255,.45); --lg-sheen-top: rgba(255,255,255,.20);
  /* ★ REFRESHED TO THE APPROVED PROTOTYPE (scratchpad build/liquid-reference.html .gl):
     blur(16px) saturate(185%) brightness(1.04) — ONE recipe for both grounds, exactly as the
     reference uses it. Raising saturate/brightness only affects what shows THROUGH the pane
     (the backdrop), never the pane's own fill opacity, so the contrast measurements recorded
     throughout this theme (§28-29) still hold. */
  --glass-blur: saturate(1.85) blur(16px) brightness(1.04);
  --glass-shadow: 0 24px 60px -20px rgba(15,23,42,.32), 0 2px 8px rgba(15,23,42,.08),
                  inset 0 1px 0 rgba(255,255,255,.95);
  --el-3: 0 24px 60px -20px rgba(15,23,42,.32), 0 2px 8px rgba(15,23,42,.08);
  --el-4: 0 32px 80px -24px rgba(15,23,42,.38), 0 3px 10px rgba(15,23,42,.10);
  --primary: #0e7490;  --primary-dark: #3730a3;  --primary-light: rgba(14,116,144,.12);
  /* ★ --text-muted IS #3d4a5c, NOT #475569, AND THE REASON IS THIS THEME'S GROUND. #475569 clears
     the 4.5 text floor easily on a white card (7.58:1) — but almost nothing on this theme IS a
     white card. A card is 33% white over three blurred orbs, so the muted caption inside it sits
     on a composite that moves: measured rgb(169,197,229) on /plant/gate's empty-state hint (4.26),
     rgb(174,205,207) on its tagline (4.49), rgb(210,194,192) on /plant/vendors' "(3 of 3)" (4.41)
     and rgb(167,205,225) on the calendar's "Overdue" (4.50). Five separate failures, one token, all
     of them 1-5% under — chasing them per element would have been five patches for one cause.
     #3d4a5c reads 5.06-5.34 on those same composites and 9.00 on a white card. */
  --text: #1e293b;  --text-muted: #3d4a5c;
  /* Declared because it was NOT: --text-secondary fell back to :root's #404040, a neutral grey
     dropped into a blue-grey palette. #334155 is the step between --text and --text-muted here. */
  --text-secondary: #334155;
  --sidebar-bg: rgba(238,242,248,.62);  --sidebar-hover: rgba(255,255,255,.52);
  --rail-ink: #111827;  --rail-ink-dim: #47536a;  --rail-ico: #55627a;
  --rail-active: rgba(14,116,144,.16);  --rail-hover: rgba(15,23,42,.05);
  --rail-group-bg: rgba(255,255,255,.40);  --rail-group-brd: rgba(15,23,42,.09);
  /* ★ 26px — the reference's own `.gl` corner. A pane that overrides it locally (a chip, a tab, an
     icon button) keeps its own smaller radius; this is only the one every unnamed pane falls back
     to (CLAUDE.md's "a surface takes the theme's corner"). */
  --radius-sm: 9px; --radius: 26px; --radius-lg: 20px; --radius-pill: 999px;
  --radius-chip: 11px; --rail-group-radius: 12px;
  --fs-root: 16px;   /* its base is 16 where this product's html is 15 */
  /* ★ FOUR TOKENS THE PORT REFERENCED AND NOTHING DECLARED. An undefined var() makes its
     declaration invalid at computed-value time — and in the ported `.sidebar` rule the invalid one
     was `box-shadow`, after which Chrome kept NOTHING from the rest of the rule: `display:flex`,
     `flex-direction:column` and `align-self` never reached the CSSOM. The rail stopped being a
     flex column, its footer stopped being pinned, and the whole thing grew to 2881px against the
     viewport's 913 — which is why Vijay could not find the account menu at the bottom left: it was
     rendering nearly two screens below the fold. A missing token is not a missing colour; it can
     silently delete the rest of the rule it sits in. */
  --mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
  --text-strong: #0b1220;
  --rail-shadow: 14px 0 40px -20px rgba(15,23,42,.28), 4px 0 16px -10px rgba(15,23,42,.16);
  --rail-inset: inset -1px 0 0 rgba(255,255,255,.55);
  /* ★ THE REFERENCE'S OWN TYPE SCALE, DECLARED AS TOKENS RATHER THAN INLINED. Its sizes are half-
     pixel values off this product's ladder — 10.5 / 11.5 / 12.5 / 13.5 — and rounding them to the
     nearest rung put the type visibly back where it started. A theme is allowed its own scale (the
     Console theme has one); what it is not allowed is raw px in a rule, which is what --fs-* is
     for. So the ladder is re-pointed here and every ported rule keeps reading tokens. */
  --fs-label: 10.5px; --fs-meta: 11.5px; --fs-aux: 12.5px; --fs-body: 13.5px;
  --fs-ui: 14px;      --fs-head: 16px;   --fs-lede: 17px;  --fs-title: 20px;
  --fs-value: 26px;
  --fs-hero: 23px;   /* its page title: smaller and heavier than the product's 31/600 */

  /* ★ FOUR VALUES THE REFERENCE SETS THAT THIS PRODUCT CANNOT KEEP, each with the measurement.
     It is a mockup carrying a dozen strings; this app puts real sentences in these colours on 140
     screens, and the sweep over 17 routes in both grounds is what found them.
       --text-faint #64748b  measured 4.0:1 on a .27 pane — under AA for the small type that
                             actually uses it ("Showing 13 of 343 Work Orders", a field hint).
       accent as INK         #0e7490 on a link measured 3.2:1, while the same colour under white
                             ink on a button is 5.5:1. A surface colour and an ink colour are not
                             the same job.
       the "-dark" inks      .pf-ok and .td-src.derived paint --success-dark on --success-light,
                             and at this pane density that pair measured 2.9:1 and 3.0:1. */
  --text-hint: #4b5563;
  --accent-ink: #0b5a6e;
  --success-dark: #065f46; --warning-dark: #92400e; --danger-dark: #991b1b;
}
[data-theme="liquid-dark"] {
  --gl-f1: rgba(56,64,86,.14);  --gl-f2: rgba(56,64,86,.19);
  /* ★ THE REFERENCE'S OWN DARK FILL, EXACTLY: rgba(22,30,52,.40) (its --glass under
     prefers-color-scheme:dark / data-theme="dark"). f5 raised the same proportion as the light
     ground's. A darker, MORE opaque navy fill only ever raises contrast for the light text this
     ground carries — the same favourable direction as the light ground's f3/f5, never the other
     way. */
  --gl-f3: rgba(22,30,52,.40);  --gl-f4: rgba(44,52,72,.27);
  --gl-f5: rgba(16,23,38,.46);  --gl-f6: rgba(24,26,38,.74);
  --gl-f7: rgba(16,17,26,.90);
  --gl-lip: rgba(255,255,255,.24);  --gl-lip-side: rgba(255,255,255,.08);
  --glass-brd: rgba(255,255,255,.07);
  /* ★ THE SKY, DARK GROUND — the reference's own dark 'sky' preset. */
  --lg-sky-1: #070b17; --lg-sky-2: #0b1022;
  --lg-a1: rgba(60,110,255,.55); --lg-a2: rgba(20,180,170,.38);
  --lg-a3: rgba(130,80,255,.45); --lg-a4: rgba(255,120,90,.18);
  --lg-sheen: rgba(255,255,255,.14); --lg-sheen-top: rgba(255,255,255,.07);
  /* ★ REFRESHED TO THE APPROVED PROTOTYPE (scratchpad build/liquid-reference.html .gl):
     blur(16px) saturate(185%) brightness(1.04) — ONE recipe for both grounds, exactly as the
     reference uses it. Raising saturate/brightness only affects what shows THROUGH the pane
     (the backdrop), never the pane's own fill opacity, so the contrast measurements recorded
     throughout this theme (§28-29) still hold. */
  --glass-blur: saturate(1.85) blur(16px) brightness(1.04);
  --glass-shadow: 0 24px 60px -18px rgba(0,0,0,.65), 0 2px 10px rgba(0,0,0,.45),
                  inset 0 1px 0 rgba(255,255,255,.24);
  --el-3: 0 24px 60px -18px rgba(0,0,0,.65), 0 2px 10px rgba(0,0,0,.45);
  --el-4: 0 32px 80px -24px rgba(0,0,0,.74), 0 3px 10px rgba(0,0,0,.40);
  --text: #dfe7f0;
  --sidebar-bg: rgba(9,14,24,.64);  --sidebar-hover: rgba(56,64,86,.42);
  --rail-ink: #eef2f8;  --rail-ink-dim: #d1d8e3;  --rail-ico: #8f97ac;
  --rail-active: rgba(8,145,178,.30);  --rail-hover: rgba(255,255,255,.07);
  --rail-group-bg: rgba(255,255,255,.035);  --rail-group-brd: rgba(255,255,255,.08);
  /* ★ 26px — the reference's own `.gl` corner. A pane that overrides it locally (a chip, a tab, an
     icon button) keeps its own smaller radius; this is only the one every unnamed pane falls back
     to (CLAUDE.md's "a surface takes the theme's corner"). */
  --radius-sm: 9px; --radius: 26px; --radius-lg: 20px; --radius-pill: 999px;
  --radius-chip: 11px; --rail-group-radius: 12px;
  --fs-root: 16px; --fs-hero: 23px;
  --mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
  --text-strong: #f7fafc;
  --rail-shadow: 14px 0 44px -20px rgba(0,0,0,.55), 4px 0 18px -10px rgba(0,0,0,.35);
  --rail-inset: inset -1px 0 0 rgba(255,255,255,.08);
  /* ★ THE REFERENCE'S OWN TYPE SCALE, DECLARED AS TOKENS RATHER THAN INLINED. Its sizes are half-
     pixel values off this product's ladder — 10.5 / 11.5 / 12.5 / 13.5 — and rounding them to the
     nearest rung put the type visibly back where it started. A theme is allowed its own scale (the
     Console theme has one); what it is not allowed is raw px in a rule, which is what --fs-* is
     for. So the ladder is re-pointed here and every ported rule keeps reading tokens. */
  --fs-label: 10.5px; --fs-meta: 11.5px; --fs-aux: 12.5px; --fs-body: 13.5px;
  --fs-ui: 14px;      --fs-head: 16px;   --fs-lede: 17px;  --fs-title: 20px;
  --fs-value: 26px;

  /* ★ AND ON THIS GROUND, --primary-b CANNOT BE THE INK. The reference sets #818cf8 and uses it
     only as the far end of a button gradient. Here the same token is ALSO the ink that
     `.md-cube.plain`, `.ui-pill` and `.td-src` paint over their own tint — all id-scoped at
     (1,1,0) inside each page's <style>, so no theme rule can reach them and the token is the only
     lever. Measured before this split: 1.06:1, 1.09:1, 1.18:1 — invisible. So the ink is bright
     and the button states its own dark gradient below. */
  --primary: #22d3ee;  --primary-dark: #67e8f9;  --primary-light: rgba(8,145,178,.32);
  --accent-ink: #67e8f9;
  --text-muted: #b3c1d2; --text-hint: #a3b0c4;   /* its #9fb0c3/#7d8ea3 measured 3.5-4.4:1 here */
  --success-dark: #5ee0ab; --warning-dark: #ffc766; --danger-dark: #ffa0a0;   /* #ff8f8f read 4.25:1 on the teal tint and 4.34:1 on the red one — both under 4.5 */
}
html[data-theme="liquid"], html[data-theme="liquid-dark"] { font-size: var(--fs-root); }

/* ============================================================================
   The reference's own component rules, mapped and scoped. Generated — do not hand-edit; change
   ui-lab/glass-agent/glass.css and re-run the port, or add a correction BELOW this block so it
   is obvious which declarations are its and which are this product's.
   ============================================================================ */
/* from glass.css  a */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) a:not(.btn):not(.ox-chip):not(.ox-hero):not(.nav-item) {
  color:inherit;
}
/* from glass.css  h1,h2,h3 */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) h1, :is([data-theme="liquid"],[data-theme="liquid-dark"]) h2, :is([data-theme="liquid"],[data-theme="liquid-dark"]) h3 {
  font-family:inherit;
}
/* from glass.css  .glass */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .card, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-cardpop, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .wl-fig, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .mini, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .modal-dialog, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .apx-modal {
  background:var(--gl-f3);
  -webkit-backdrop-filter:var(--glass-blur);
  backdrop-filter:var(--glass-blur);
  border:1px solid var(--glass-brd);
  border-top-color:var(--gl-lip);
  box-shadow:var(--glass-shadow), inset 0 1px 0 var(--gl-lip);
}
/* from glass.css  .glass-strong */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .topbar, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .ds-pop, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .ui-expmenu {
  background:var(--gl-f5);
  -webkit-backdrop-filter:saturate(1.85) blur(19px);
  backdrop-filter:saturate(1.85) blur(19px);
  border:1px solid var(--glass-brd);
  border-top-color:var(--gl-lip);
  box-shadow:var(--glass-shadow), inset 0 1px 0 var(--gl-lip);
}
/* from glass.css  .sidebar */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar {
  background:var(--sidebar-bg);
  -webkit-backdrop-filter:saturate(1.85) blur(19px);
  backdrop-filter:saturate(1.85) blur(19px);
  border:1px solid var(--rail-group-brd);
  border-right-color:var(--gl-lip);
  border-left:none;
  border-top:none;
  border-bottom:none;
  box-shadow:var(--rail-shadow), var(--rail-inset);
  color:var(--rail-ink);
  display:flex;
  flex-direction:column;
  align-self:flex-start;
}
/* from glass.css  .nav-group */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section {
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-1) var(--space-2);
  border-radius:12px;
  background:var(--rail-group-bg);
  border:1px solid var(--rail-group-brd);
  border-top-color:var(--gl-lip);
}
/* from glass.css  .nav-group:first-child */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section:first-child {
  margin-top: var(--space-05);
}
/* from glass.css  .nav-group-label */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .nav-group-label {
  font-size: var(--fs-label);
  font-weight:700;
  text-transform:uppercase;
  color:var(--rail-ink-dim);
  padding: var(--space-1) var(--space-2) var(--space-1);
}
/* from glass.css  .nav-item */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item {
  display:flex;
  align-items:center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2);
  border-radius:9px;
  margin: var(--space-05) 0;
  font-size: var(--fs-body);
  font-weight:500;
  color:var(--rail-ink-dim);
  text-decoration:none;
  cursor:pointer;
  transition:background .15s ease, color .15s ease;
}
/* from glass.css  .nav-item:hover */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item:hover {
  background:var(--rail-hover);
  color:var(--rail-ink);
}
/* from glass.css  .nav-item.active */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.active {
  background:var(--rail-active);
  color:var(--rail-ink);
  font-weight:600;
  box-shadow:inset 2px 0 0 0 var(--primary);
}
/* from glass.css  .nav-item:focus-visible */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item:focus-visible {
  outline:2px solid var(--primary);
  outline-offset:-2px;
}
/* from glass.css  .topbar */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .topbar {
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-6);
  margin: var(--space-3) var(--space-5) 0;
  border-radius:16px;
}
/* from glass.css  .theme-switch */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .lg-ground {
  display:flex;
  align-items:center;
  gap: var(--space-1);
  border-radius:999px;
  padding: var(--space-1);
  border:1px solid var(--glass-brd);
  background:rgba(127,127,127,.08);
}
/* from glass.css  .theme-switch button */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .lg-ground button {
  border:none;
  background:transparent;
  cursor:pointer;
  border-radius:999px;
  display:flex;
  align-items:center;
  justify-content:center;
  color:var(--text-muted);
}
/* from glass.css  .theme-switch button.on */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .lg-ground button.on {
  background:linear-gradient(135deg, var(--primary), var(--primary-dark));
  color:#fff;
  box-shadow:0 3px 10px -2px color-mix(in srgb, var(--primary) 35%, transparent);
}
/* from glass.css  .icon-btn */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .tb-ico, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .topbar-actions > button {
  border-radius:10px;
  display:flex;
  align-items:center;
  justify-content:center;
  border:1px solid var(--glass-brd);
  background:rgba(127,127,127,.08);
  color:var(--text);
  cursor:pointer;
  transition:background .12s ease;
}
/* from glass.css  .icon-btn:hover */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .tb-ico:hover, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .topbar-actions > button:hover {
  background:rgba(127,127,127,.18);
}
/* from glass.css  .page-head */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-head {
  border-radius:20px;
  padding: var(--space-5) var(--space-6);
  margin-bottom: var(--space-4);
  display:flex;
  align-items:flex-start;
  justify-content:space-between;
  gap: var(--space-5);
  flex-wrap:wrap;
}
/* from glass.css  .page-head h1 */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-title {
  margin: 0 0 var(--space-1);
  font-size: var(--fs-hero);
  font-weight:700;
  color:var(--text-strong);
  letter-spacing:-.2px;
}
/* from glass.css  .page-head .tagline */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-sub {
  margin: 0 0 var(--space-2);
  font-size: var(--fs-body);
  color:var(--text-muted);
}
/* from glass.css  .scope-chip */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-scope {
  display:inline-flex;
  align-items:center;
  gap: var(--space-1);
  font-size: var(--fs-meta);
  font-weight:600;
  color:var(--primary);
  background:rgba(14,116,144,.12);
  border:1px solid rgba(14,116,144,.28);
  padding: var(--space-1) var(--space-2);
  border-radius:999px;
  margin-right: var(--space-1);
}
/* from glass.css  .page-head-actions */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-acts {
  display:flex;
  align-items:center;
  gap: var(--space-2);
  flex-wrap:wrap;
}
/* from glass.css  .btn */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-chip, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .btn {
  display:inline-flex;
  align-items:center;
  gap: var(--space-2);
  font-size: var(--fs-body);
  font-weight:600;
  padding: var(--space-2) var(--space-4);
  border-radius:11px;
  cursor:pointer;
  border:1px solid var(--glass-brd);
  background:rgba(127,127,127,.08);
  color:var(--text);
  transition:transform .12s ease, box-shadow .12s ease, background .12s ease;
  text-decoration:none;
}
/* from glass.css  .btn:hover */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-chip:hover, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .btn:hover {
  background:rgba(127,127,127,.15);
}
/* from glass.css  .btn:active */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-chip:active, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .btn:active {
  transform:translateY(1px);
}
/* from glass.css  .btn:focus-visible */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-chip:focus-visible, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .btn:focus-visible {
  outline:2px solid var(--primary);
  outline-offset:2px;
}
/* from glass.css  .btn-primary */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-hero, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .btn-primary {
  color:#fff;
  border:none;
  background:linear-gradient(135deg, var(--primary), var(--primary-dark));
  box-shadow:0 10px 24px -8px color-mix(in srgb, var(--primary) 35%, transparent);
}
/* from glass.css  .btn-primary:hover */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-hero:hover, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .btn-primary:hover {
  box-shadow:0 14px 30px -8px color-mix(in srgb, var(--primary) 35%, transparent);
  transform:translateY(-1px);
}
/* from glass.css  .btn-sm */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .btn-sm {
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-aux);
  border-radius:9px;
}
/* from glass.css  .btn[disabled] */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-chip[disabled], :is([data-theme="liquid"],[data-theme="liquid-dark"]) .btn[disabled] {
  opacity:.45;
  cursor:not-allowed;
}
/* from glass.css  .stats-row */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stats {
  display:grid;
  /* five across on a desk; auto-fit with a floor on anything narrower — a fixed five at 412px put
     the last two tiles off the right edge (measured: the strip ran to 564px) */
  grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
@media (min-width: 1100px) { :is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stats { grid-template-columns:repeat(5, 1fr); } }
/* every sideways-wide list scrolls in its own box on a phone; the page never does */
@media (max-width: 600px) {
  .ox-stats .ox-stat { min-width: 0; }
  .card > table, .card-body > table { display:block; overflow-x:auto; }
  .ox-views { max-width: 100%; overflow-x: auto; flex-wrap: nowrap; }   /* the five view buttons scroll in their own strip */
}
/* from glass.css  .stat-tile */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat {
  border-radius:var(--radius);
  padding: var(--space-4) var(--space-4) var(--space-3);
  text-align:left;
  cursor:pointer;
  transition:transform .12s ease, box-shadow .12s ease;
}
/* from glass.css  .stat-tile:hover */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat:hover {
  transform:translateY(-2px);
}
/* from glass.css  .stat-tile .num */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat b, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat .n {
  font-family:var(--mono);
  font-size: var(--fs-value);
  font-weight:600;
  color:var(--text-strong);
  line-height:1;
}
/* from glass.css  .stat-tile .lbl */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat small, :is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat .l {
  margin-top: var(--space-2);
  font-size: var(--fs-aux);
  font-weight:500;
  color:var(--text-muted);
}
/* ★ OVERRIDES THE `color` IN THE GENERATED RULE DIRECTLY ABOVE, on the light half only. A stat
   card's tint is rgba(...,.22) over the theme's ground, so what the caption actually sits on is a
   composite that moves with the orbs: measured rgb(174,176,207) on /plant/shift-log ("Carried in",
   3.58:1) and rgb(132,203,203) on /plant/assets ("Isolation Points", 4.11:1) — both under 4.5 with
   --text-muted. --text-secondary (#334155, newly declared for this theme) reads 4.89 and 5.61 and
   is still a step quieter than the figure above it. The dark half is untouched: there --text-muted
   is a light grey on a dark pane — EXCEPT on the saturated tints: measured 4.05:1 for
   "Isolation Points" on /plant/assets, where the tile's teal is rgb(18,96,85). One step lighter
   (#cfd9e5) reads 5.20 there and stays quieter than the figure. */
[data-theme="liquid"] .ox-stat small, [data-theme="liquid"] .ox-stat .l { color: var(--text-secondary); }
[data-theme="liquid-dark"] { --text-secondary: #cfd9e5; }
[data-theme="liquid-dark"] .ox-stat small, [data-theme="liquid-dark"] .ox-stat .l { color: var(--text-secondary); }

/* ============================================================================
   Corrections this product needs on top, each one measured. Everything above is the reference's.
   ============================================================================ */

/* ★ THE RAIL HAS TO BE DRESSED, NOT JUST TOKENISED — and this is why the side menu has never
   looked like glass on ANY theme. `.sidebar` is set to `background:transparent;
   backdrop-filter:none` by a bare (0,1,0) rule in the Claude.ai-sidebar block, whose own comment
   says themes that "dress their own sidebar on purpose (black, flat)" carry higher specificity and
   are unaffected. Glass never dressed its own — it only declares --sidebar-bg, and nothing reads
   that token for the rail. Measured: the reference's rail is rgba(238,242,248,.62); ours computed
   rgba(0,0,0,0). Two attribute selectors is (0,2,0) and beats the bare rule. Glass still carries
   the dead token, deliberately — Vijay asked for that theme not to be touched. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar {
  /* restated after the ported rule, because that rule carries the reference's own `position:sticky
     / align-self:flex-start` layout for a page that scrolls as one column. This app's rail is
     fixed and full-height with only its nav scrolling, and the brand and the account block are
     pinned to the two ends. */
  position: fixed; top: 0; bottom: 0; align-self: auto;
  display: flex; flex-direction: column;
  background: var(--sidebar-bg);
  -webkit-backdrop-filter: saturate(1.85) blur(19px); backdrop-filter: saturate(1.85) blur(19px);
  border-right: 1px solid var(--glass-brd);
  color: var(--rail-ink);
}
/* the shared glass recipe paints .nav-section with a gradient and .nav-section.is-open at (0,3,0)
   with a denser one; the reference uses one flat fill for both, so this has to match (0,3,0) and
   `background` (not background-color) is what clears the inherited image */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open {
  background: var(--rail-group-bg);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  box-shadow: none;
}
/* ★ A SURFACE AND AN INK CANNOT BE THE SAME TOKEN WHEN ONE OF THEM INVERTS WITH THE GROUND. The
   shared rule fills these three from --primary-dark and puts light ink on them, which is right
   while that token is a mid blue and wrong here, where it is bright so ink over a dark tint stays
   readable. Measured: the account avatar 1.45:1, today's date on the calendar 1.81:1. */
:is([data-theme="liquid-dark"]) :is(.cacct .cav, .cal-num.on, .apx-seg > button.on,
                                    .cal-cell.today .cal-num) { background: #0e7490; color: #fff; }
/* and the primary button keeps a dark gradient under white ink for the same reason */
:is([data-theme="liquid-dark"]) :is(.btn-primary, .ox-hero, .ds-btn, .lg-ground button.on) {
  background: linear-gradient(135deg, #0e7490, #3730a3); color: #fff;
}
/* `a { color:#3b82f6 }` at the top of this file is the product default and not a token, so a theme
   that changes its accent must answer it or every link stays the old blue */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) a:not(.btn):not(.ox-chip):not(.ox-hero):not(.nav-item) {
  color: var(--accent-ink);
}
/* ★ THE BLANKET RULE ABOVE ERASES A DELIBERATELY INVERTED LABEL. Every `:not()` contributes a
   class, so `a:not(.btn):not(.ox-chip):not(.ox-hero):not(.nav-item)` is (0,4,1) and the whole
   selector (0,5,1) — it outranks `.md-prem-pick a.on { color:#fff }` at (0,2,1) and repaints the
   SELECTED premises pill in the accent it is already filled with. Measured as ceo / vp / finance /
   flow.buyer on /home: 1.45:1 light, 1.25:1 dark — the label of the premises you are looking at
   was invisible. Restated at the same (0,5,1), later in the file.
   ★ AND THE UNIT PILL BESIDE IT (15-10, Vijay: "unit selector not visible... same problem in liquid glass and frosted
   skins"): `.md-unit-pick a.on` is the same white-on-fill pill one row down - All Units · Combined, or the unit picked -
   and was left out, so its label was painted the blue it is filled with. The note here said the premises pill was the
   only white ink on an <a>; measured, there were three: this one, and the group console help's `.ghelp a.cta` (same
   blanket, its own view). Every <a> that carries white on a fill is named here. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .md-prem-pick a.on:not(.btn):not(.ox-chip) { color:#fff; }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .md-unit-pick a.on:not(.btn):not(.ox-chip) { color:#fff; }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ghelp a.cta:not(.btn):not(.ox-chip) { color:#fff; }
/* A blanket `a` override was tried here and REVERTED: at (0,5,1) it landed after the pill rule
   above and repainted that white label dark again (measured 2.11:1), while the case it was added
   for — "Open all" on /home — kept reading --primary, so its colour comes from somewhere else
   entirely. Ink-on-translucent-chip is a structural property of this theme's ground, not something
   a blanket link rule fixes; see the note at the end of this section. */
/* every other theme leaves the ground control out of the flow entirely */
.lg-ground { display: none; }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .lg-ground { display: flex; }

/* ★ THE CURRENT-PAGE CHIP CANNOT READ --primary-dark HERE. The glass block redirects .ox-page's
   fill to --primary-dark to fix a 3.68:1 failure on light glass — correct there, wrong on this
   ground, where that token is deliberately a BRIGHT cyan so ink over a dark tint stays legible.
   White on it measured unreadable. Same split, same answer, as the avatar and today's date. */
:is([data-theme="liquid-dark"]) .ox-page { background: #0e7490; color: #fff; }

/* ★ THE COLLAPSED RAIL MUST KEEP THIS THEME'S MATERIAL. `.app-layout.collapsed .sidebar` in the
   glass block is (0,3,0) and the liquid rail rule is (0,2,0), so clicking the « handle swapped the
   rail's fill and blur to Glass's — the same rail changing material on a click. */
@media (min-width: 769px) {
  :is([data-theme="liquid"],[data-theme="liquid-dark"]) .app-layout.collapsed .sidebar {
    background: var(--sidebar-bg);
    -webkit-backdrop-filter: saturate(1.85) blur(19px); backdrop-filter: saturate(1.85) blur(19px);
    border-right: 1px solid var(--glass-brd);
  }
}

/* ★ AN ID-SCOPED PAGE RULE AT (1,1,0) BEATS EVERY THEME RULE, AND THREE OF THIS THEME'S RULES WERE
   DEAD BECAUSE OF IT. `#wo-root` re-states the row tint, the selected tab and the column-title
   strip inside views/plant/work-orders.ejs's own <style>, so Work Orders rendered those three
   controls differently from every other module on the same theme. Matching the id is the only way
   a stylesheet can answer a page's own <style>; the alternative is editing 140 views. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) #wo-root .ox-row:hover,
:is([data-theme="liquid"],[data-theme="liquid-dark"]) #wo-root .ox-row[data-prio]:hover {
  background: var(--gl-f2);
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) #wo-root .ox-tabs2 button.on {
  background: var(--gl-f4);
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) #wo-root .wo-colhead {
  background: var(--gl-f2); border-color: var(--glass-brd);
}

/* ★ THE CHART TRACK WAS A FILM OF THE GROUND'S OWN COLOUR. --tint-neutral-bg resolves to a
   translucent white/near-black on this theme, so the unfilled part of every scale bar vanished and
   the bar read as floating with no scale behind it. A track is a surface; it needs a step. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ui-scale-tr {
  background: var(--gl-f4); border: 1px solid var(--glass-brd);
}

/* ============================================================================
   §30c · LIQUID GLASS — --primary-dark IS NOT A SURFACE ON THIS THEME.

   ★ THE PRODUCT'S ACCENT IS ONE HUE IN TWO STEPS; THIS THEME'S IS TWO DIFFERENT HUES.
   Everywhere else `--primary` and `--primary-dark` are light and dark blue, so a rule can fill a
   chip with the dark one and put white on it and get a deeper version of the same colour. The
   reference build's accent is `--primary-a #0e7490` TEAL into `--primary-b #3730a3` INDIGO, used
   only ever as the two ends of a 135deg gradient on a primary button — never as a flat fill. Read
   as "the dark step", the indigo turns every one of those chips into a heavy violet slab sitting
   beside teal, which is what Vijay photographed on the Tech Directory: an open section header
   rendered as a solid #3730a3 bar with its source badge invisible on top of it.

   These are the surfaces that fill from that token. Each one gets the TEAL, which is this theme's
   accent as a surface, and keeps its white ink — #0e7490 under white measures 5.5:1.
   ============================================================================ */
/* ★ THE SELECTOR HAS TO MATCH THE SHAPE OF THE ONE IT ANSWERS, NOT JUST NAME THE ELEMENT.
   The rule that fills these from --primary-dark groups them in a single `:is()` that contains
   `.apx-seg > button.on`, and `:is()` takes the specificity of its MOST SPECIFIC argument — so
   that rule is (0,3,1) while `:is([data-theme=…]) .brand-icon` is only (0,2,0). Naming the element
   directly lost, three times, and the first two attempts at this fix changed nothing on screen
   while reading as though they should have. Same grouping, same specificity, later in the file. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.skip-link, .brand-icon, .ox-page,
    .apx-seg > button.on, .rec-stage.joined, .cal-num.on, .cacct .cav,
    .cal-cell.today .cal-num, #td-body .td-sec[open] > .td-sum) {
  background-color: var(--accent-solid);   /* the same #0e7490, from the one place that declares it */
  background-image: none;
  color: #fff;
}
/* the hover of an open section was --primary, i.e. the same teal it now already is — so hovering
   an open header did nothing. One step darker, so the control still answers the pointer. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) #td-body .td-sec[open] > .td-sum:hover {
  background-color: #0b5a6e;
}
/* ★ AND THE BADGE ON THAT HEADER HAS TO INVERT WITH IT. `.td-src` paints --primary-dark on
   --primary-light — correct on a pale card, invisible once the header behind it is a solid teal
   bar, which is the blank pill in the screenshot. On the open header it becomes a light-on-dark
   chip; everywhere else it keeps the page's own treatment. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) #td-body .td-sec[open] > .td-sum .td-src {
  background: rgba(255,255,255,.16);
  border-color: rgba(255,255,255,.34);
  color: #fff;
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   §31 · LIQUID GLASS — PORT CORRECTIONS, AND VIJAY'S NOTES ON TOP OF THEM
   ───────────────────────────────────────────────────────────────────────────────────────────────
   §30/§30a/§30b are GENERATED from ui-lab/glass-agent/glass.css and must not be hand-edited, so
   every correction lives here instead, at the same specificity and later in the file. Each was
   found by rendering Sonnet's lab build and the shipped app at 1440x1100 and diffing 21 computed
   properties per component — not by reading the CSS.

   TWO KINDS OF CHANGE, LABELLED:
     [PORT]  the generator lost or crossed a value Sonnet authored — restoring his intent.
     [NOTE]  Vijay's own direction, which deliberately departs from the lab build.

   ★ The rail and switch measurements below are TOKENS, not raw px in the rules. Sonnet's rail is
   drawn on 7/6/9/10 and 30/17/6, which is off this product's 4-8-12-16 grid — a theme is allowed
   its own scale, but a rule is never allowed a raw px (that is the Console drift the audit exists
   to catch), so the scale is declared once here and the rules read it.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) {
  /* the missing half of Sonnet's edge pair — --rail-group-brd (the dark hairline) IS declared in
     §30, its bright counterpart never was, which is how the two came to be crossed */
  --rail-group-lit: rgba(255,255,255,.85);
  --rail-pane-gut: 10px;                      /* the clear gutter each side of a nav panel */
  --rail-pane-pad: 7px 6px 9px;               /* his panel's own padding */
  --rail-row-pad: 9px 10px;  --rail-row-gap: 10px;  --rail-row-mar: 1px;
  --rail-row-lh: 1.2;        /* NOT the body's 1.5 — see the row-height note below */
  --rail-tab-py: 7px;        /* the ONE vertical padding a section tab gets */
  --rail-tab-gap: 6px;       /* between collapsed tabs */
  /* ★ --rail-ico-size, NOT --rail-ico (14-10): --rail-ico is the rail ICON'S COLOUR (§29), and this box size used the same
     name - so on Liquid the icon's colour was "17px" (invalid, it inherited the row's ink), and under Contrast High or Extra
     High, which sets --rail-ico to a colour, the icon's WIDTH became a colour and every side-menu icon vanished. */
  --rail-ico-size: 17px;     /* his icon box; the app's hydrator was giving 19-in-20 */
  --rail-head-pad: 5px 8px 6px;
  --rail-brand-pad: 22px 20px 16px;
  --sw-btn: 30px;  --sw-ico: 17px;  --sw-gap: 6px;  --sw-pad: 4px;
  --tb-pad: 12px 28px;  --tb-mt: 14px;
  --track-ui: -0.16px;                        /* he tracks the whole interface tighter */
}
:is([data-theme="liquid-dark"]) { --rail-group-lit: rgba(255,255,255,.24); }

/* [PORT] THE TWO RAIL EDGE TOKENS WERE SWAPPED. Sonnet puts the bright lip (.85) on the rail's
   right edge and a DARK hairline (rgba(15,23,42,.09)) around each nav panel. The app shipped the
   exact inverse — dark on the rail, white .27 on the panels. A white border on a 40%-white panel
   sitting on a 62%-white rail is not an edge at all, which is why the rail reads as one blank
   column instead of a stack of panes. This is the "too white". */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar { border-right-color: var(--rail-group-lit); }

/* [PORT] The panel takes the dark hairline with the bright lip on its TOP edge only — light
   falling on the top of a pane. [PORT] And it gets its gutter back: his panel is 243px inside a
   264px rail (10px clear each side); the app's was 259px inside 260px, i.e. flush, so nothing
   could read as a separate card. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section,
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open {
  border-color: var(--rail-group-brd);
  border-top-color: var(--rail-group-lit);
  margin: var(--space-05) var(--rail-pane-gut) var(--rail-pane-gut);
  padding: var(--rail-pane-pad);
}

/* [PORT] Rows shipped at 8px padding / 8px gap against his 9px 10px / 10px — every label sat
   tighter to its icon and its neighbour than he drew it. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item {
  padding: var(--rail-row-pad);
  gap: var(--rail-row-gap);
  margin: var(--rail-row-mar) 0;
  /* ★ AND THE ROW'S OWN LEADING, WHICH IS WHERE "TOO MUCH GAP" ACTUALLY CAME FROM. Padding, gap
     and margin already matched the lab to the pixel; the rows were still 38.3px against his 35,
     and the pitch 39 against 36. A row's height is max(icon, line-height) + padding, and BOTH
     terms were bigger: `body { line-height: 1.5 }` (§1) inherits into the row, so at 13.5px the
     text box is 20.25px where his `normal` is ~16.3px; and the icon hydrator emits a 19px svg in
     a 20px span where his raw svg is 17px. 4px of leading plus a 3px icon box = 3px on every row,
     ~24px down a panel and ~90px down the rail. 1.2 gives 16.2px, which is his `normal`. */
  line-height: var(--rail-row-lh);
}
/* The icon slot: size the svg AND stop its wrapper adding a line-box of its own — the hydrator
   replaces <i class="ti ti-x"> with <span><svg>, so CSS written against .ti never lands (see the
   icon gotcha in CLAUDE.md) and the span is what has to be measured. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item .nav-icon,
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item > span:first-child {
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 0; flex: none;
  width: var(--rail-ico-size); height: var(--rail-ico-size);
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item .nav-icon svg,
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item > span:first-child > svg {
  width: var(--rail-ico-size); height: var(--rail-ico-size);
}
/* The collapsible section header is a row too, and inherits the same 1.5. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-group { line-height: var(--rail-row-lh); }
/* [PORT] The active row in the lab has NO border and NO backdrop-filter — a solid fill that reads
   as the one lit row. The app added a 1px white border and a blur, so on a white panel it became
   another faint outline competing with the panel's own edge. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-item.active {
  border: 0;                       /* width, not just colour: 1px transparent still takes space */
  -webkit-backdrop-filter: none; backdrop-filter: none;
}

/* [PORT] The brand block: 22/20/16 with a 12px gap in the lab, 16/20/12 and no gap here — 9px
   shorter. The app also adds an inset white lip he does not draw, a third white line in a column
   that already has too many. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar-brand {
  padding: var(--rail-brand-pad);
  gap: var(--space-3);
  box-shadow: none;
}

/* [PORT] THE DARK/LIGHT TOGGLE WAS BUILT AT 40% OF ITS SIZE. Lab: 76x40 overall, two 30x30 round
   buttons, 6px gap. Shipped: 46x26 with 16x16 buttons and a 4px gap — and 16px is well under any
   touch target. This is the control Vijay named first. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .lg-ground {
  gap: var(--sw-gap);
  padding: var(--sw-pad);
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .lg-ground button {
  width: var(--sw-btn); height: var(--sw-btn);
  padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .lg-ground button svg {
  width: var(--sw-ico); height: var(--sw-ico);
}

/* [PORT] The topbar is 66px tall in the lab with 28px side padding and a 14px top margin; the app
   shipped 60 / 24 / 12. The round chrome buttons take a 12px corner, not 10px. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .topbar {
  padding: var(--tb-pad);
  margin: var(--tb-mt) var(--space-5) 0;
  /* ★ AND NO BORDER — THIS IS THE "OLD NAV BAR FRAME BEHIND THE NEW ONE". The base .topbar (§3) is
     a full-width sticky bar and carries `border-bottom: 1px solid var(--border)`; §26 then adds a
     four-side glass edge. This theme makes the topbar a FLOATING 16px-radius pill, and a
     bottom-only hairline on a rounded pill draws a straight line across its base, past both
     bottom corners — which reads exactly as the leftover edge of the bar it used to be. Measured
     present in all six glass x background combinations. Sonnet's lab topbar has NO border on any
     side: it is held by the shadow and the inset lip, which are both already here. */
  border: 0;
}
/* …and in PLAIN-background mode the same line comes back from a different rule: §11's
   `[data-bg="plain"][data-glass] .topbar` strips the glass and gives the bar a bottom rule, which
   is right for a full-width bar and wrong for this theme's floating pill — a pill with no shadow
   and one straight line under it. Plain mode keeps the float and takes a whole edge instead.
   Matched at (0,3,0), the same as the rule it answers, and later. */
:is([data-theme="liquid"],[data-theme="liquid-dark"])[data-bg="plain"] .topbar {
  border: 1px solid var(--border);
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .topbar-icon-btn { border-radius: var(--rail-group-radius); }

/* [PORT] He tracks the whole interface at -0.16px; the app shipped `normal`, so every label, row
   and heading is slightly looser than drawn — invisible on one word, cumulative down a rail. The
   uppercase section heads opt OUT below: negative tracking on uppercase micro-type is the one
   place it hurts rather than helps. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) body { letter-spacing: var(--track-ui); }

/* ── VIJAY'S NOTES ─────────────────────────────────────────────────────────────────────────── */

/* [NOTE] "the sidemenu is too white". The edge and gutter corrections above do most of the work,
   but the rail is still a 62% wash of #eef2f8 over a pastel ground, which suppresses the orbs
   behind it almost entirely. At 44% the ground reads through and the rail still holds as a pane —
   the 19px blur is what makes it a pane, not the opacity. Departs from the lab on purpose: his
   value is .62. */
[data-theme="liquid"] { --sidebar-bg: rgba(238,242,248,.44); }
[data-theme="liquid-dark"] { --sidebar-bg: rgba(9,14,24,.46); }

/* [NOTE] "menu section heads are very small". They are 10.5px/700/uppercase in the lab too, so
   this is a disagreement with the lab, not a port error. 10.5px is exactly this app's stated
   minimum, and uppercase at the minimum with NEGATIVE tracking is the hardest combination there
   is. --fs-meta (11.5px) with positive tracking keeps the small-caps character and stops it being
   a squint. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .nav-group-label,
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .nav-section-title {
  font-size: var(--fs-meta);
  letter-spacing: .055em;
  /* ★ NO VERTICAL PADDING ON THE LABEL. It sits inside .nav-group, which is the button and already
     pads the hit area — so its own 5/6 was a second padding inside the first. Horizontal only. */
  padding: 0 var(--space-2);
  line-height: var(--rail-row-lh);
}

/* ★ A SECTION TAB WAS 67px OF PITCH AROUND 14px OF TEXT. Measured on a collapsed rail: the label
   padded itself 11px, inside a .nav-group padding 14px, inside a .nav-section padding 16px, inside
   a 12px margin — FOUR nested vertical paddings, 53px of them, for one 11.5px word. Sonnet never
   hits this because his lab rail has no collapsed state: every panel is open, so the panel's own
   padding is doing a real job. Here the tab is the whole panel, and it only needs one padding.
   Collapsed: the panel contributes none and the .nav-group gives the single 7px. ~28px tab. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section > .nav-group {
  padding-top: var(--rail-tab-py);
  padding-bottom: var(--rail-tab-py);
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section:not(.is-open) {
  padding-top: 0;
  padding-bottom: 0;
  margin-bottom: var(--rail-tab-gap);
}
/* Open, the panel keeps his 7/6/9 so the items stay inset — but the head no longer double-pads. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar .nav-section.is-open > .nav-group {
  padding-bottom: var(--space-1);
}

/* ── §31b · A PORTALLED POPOVER CAN BE GLASS ─────────────────────────────────────────────────
   `.is-portaled` is set by UIPop (views/layouts/main.ejs) when it moves a menu onto <body>. That
   is the whole condition: inside `.sidebar` or `.topbar` these menus sit within a backdrop-filtered
   ancestor, so their own filter samples what is behind that ancestor rather than what they cover —
   which is why --menu-bg had to be opaque (#fbfcff / #141827) and why --glass-over sat at .93/.94.
   Proven, not assumed: forcing the account menu translucent IN PLACE made the rail's own rows
   ("Work Permits", "Parts & Inventory", "Vendors") legible through its own items.
   On <body> that constraint is gone, so the fill drops to the f6 rung and the theme's blur does
   the hiding: .80 light / .74 dark with an 15-19px blur leaves nothing readable behind, while the
   ground's colour comes through. --gl-f6, not --glass-over, for exactly the reason in §31's note
   about the rail — the dense rung exists for surfaces that CANNOT blur. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) :is(.cacct-pop, .ds-pop, .ui-expmenu).is-portaled,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) #lang-pop.is-portaled,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) #notif-panel.is-portaled {
  /* ★ #notif-panel carries its surface in an INLINE style (`background:var(--over-bg,var(--card))`
     in main.ejs), and an inline declaration outranks any stylesheet rule — so setting `background`
     here did nothing, measured. What a stylesheet CAN still reach is the TOKEN the inline style
     reads, so the two --over-* channels are redirected instead of the property. */
  --over-bg: var(--gl-f6);
  --over-blur: var(--glass-blur);
  background: var(--gl-f6);
  /* The theme's own blur. A 30px kernel was tried here to cut the variance of whatever a tall menu
     hangs over — the language list is 548px and crosses cards, tables and the ground's orbs — and
     it is NOT kept: the ten dark-mode failures that motivated it were a bug in my measuring
     script, which read `color(srgb .875 .906 .941 / .82)` as 0-255 and so scored pale grey text as
     near-black. With the reading corrected, every one of those rows passes at the theme's 15px. */
  -webkit-backdrop-filter: var(--glass-blur);
          backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-brd);
  box-shadow: var(--shadow-lg), inset 0 1px 0 var(--gl-lip);
}

/* ── §31c · LIQUID GLASS GETS ITS OWN SWATCH, NOT GLASS'S ────────────────────────────────────
   --swatch-bg is declared once in the SHARED glass/liquid block, so both tiles in Appearance drew
   the same blue-pink gradient and the two themes were indistinguishable in the picker — you could
   not tell which one you were choosing. These are this theme's ACTUAL grounds, copied from its own
   body::before above, so the tile promises what the page delivers: cool blue into warm sand on the
   light half, and deep teal into ember on the dark one. */
[data-theme="liquid"] { --swatch-bg: linear-gradient(135deg, #d9e4f2 0%, #eef1f6 52%, #f5eae0 100%); }
[data-theme="liquid-dark"] { --swatch-bg: linear-gradient(135deg, #0a1e30 0%, #0a0e17 52%, #291c10 100%); }

/* ── §31d · THE FOCUS RING GOES ON THE FIELD, NOT ON THE BARE INPUT ──────────────────────────
   §14 above is right that a 15%-blue ring on a translucent field is not a focus indicator, and it
   is an accessibility requirement rather than a look. What it got wrong is WHICH element it rings.
   `.ox-search` is the field: 44px tall, 16px radius, its own border. The `<input>` inside it is
   stripped bare — `border:none; background:transparent` — and is only 21px tall with radius 0. So
   a 3px ring plus a 2px outline on the INPUT drew two hard-cornered rectangles inside a rounded
   pill, hugging the text rather than the control. Measured on /plant/parts-inventory focused:
   outline 2px solid rgb(55,48,163) and box-shadow rgba(59,130,246,.45) 0 0 0 3px, both on a
   776x21 radius-0 box inside an 836x44 radius-16 one. That is the square Vijay photographed.

   The ring moves to the wrapper via :focus-within, the input contributes nothing, and the colour
   follows the theme instead of the literal default blue — on Liquid the accent is teal #0e7490,
   so rgba(59,130,246,…) was off-palette as well as the wrong shape. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-search:focus-within {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 55%, transparent);
}
/* …and the input keeps none of its own. Both of these have to out-rank §14's
   `:is([data-theme=…]) .ox-search input:focus` (0,3,1) and its blanket
   `:is([data-theme=…]) :focus-visible` (0,2,0) — hence the element plus the two classes. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-search input:focus,
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ox-search input:focus-visible {
  outline: none;
  box-shadow: none;
  background: transparent;
  border-color: transparent;
}

/* ── §31e · A CONTAINER OF ROWS IS NOT A SECOND PANE ─────────────────────────────────────────
   §26's own rule — a container of cards gets a boundary and no fill; the cards are the objects —
   was never applied to `#notif-list`, which §13's floating-surface list lumps in with the PANELS
   it lives inside. Measured with the bell open: `#notif-panel` painting 0.80 (light) / 0.74 (dark)
   and `#notif-list` painting another 0.93 / 0.94 across 358x380 of the panel's 360x423. Those
   composite to ~0.99 — an opaque white slab with a glass frame around it, which is why the panel
   read as white however translucent its own surface was. The nested backdrop-filter was doing
   nothing either: inside the panel's filter it samples what is behind the PANEL, not behind the
   list, and costs a compositing layer to do it.
   Matched at (1,1,0), the same as the rule it answers (whose :is() holds #notif-panel), and later. */
/* The meta line inside a floating panel needs a step more ink than a card's does: the panel is
   0.80 over WHATEVER is behind it, and on a mid-tone page that composites to rgb(156,179,191),
   where --text-muted (#3d4a5c) measured 4.12:1 at 10.6px.
   ★ IT IS THE TOKEN THAT IS REDIRECTED, NOT THE `color`. Those rows are built in JS with
   `style="color:var(--text-muted)"` on each line (main.ejs), and an inline declaration outranks
   every stylesheet rule — a `span { color: … }` here changed nothing, measured. What a stylesheet
   CAN still reach is the custom property the inline style resolves, because custom properties
   inherit: redefining --text-muted on the panel re-points every inline var() inside it at once —
   the body line, the timestamp and the icon. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) #notif-panel {
  --text-muted: var(--text-secondary);
}
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) #notif-list {
  background: transparent;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 0;
  box-shadow: none;
}

/* ── §31f · THE RAIL DOES NOT ANIMATE ITS OWN STARTING STATE ─────────────────────────────────
   `nav-boot` is on #sidebar-nav-container for the first two frames only (set and cleared in
   views/layouts/main.ejs). The rail is rendered with every group OPEN and the remembered ones are
   closed by script; if anything has already forced a layout by then, that close becomes a
   TRANSITION rather than an initial style, and the whole menu animates shut on every click —
   measured at a 1218px swing over 290ms. Suppressing the transition for the boot frames makes the
   restored state arrive silently however late it lands. (0,2,0) beats the base rule's (0,1,0). */
.nav-boot .nav-group-items { transition: none; }

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   PLANTX — the lime-on-graphite skin (12-10; lime since 13-10), built from the approved prototype
   (scratchpad build/skin-reference.html). Two MORE choices in the picker, 'plantx' and
   'plantx-dark' — independent swatches, like 'console'/'console-day', not an OS-resolved pair like
   glass/liquid. Nobody's default look moves: every token below is new to these two theme keys only.

   ★ GLASS IS OFF HERE ON PURPOSE. Every surface is solid — no frosted card, no blurred sidebar —
   the one exception being the top bar, which keeps a LIGHT translucency with a blur under it
   (the reference's own `.pulse`), because that is this skin's signature, not the app's Liquid
   Glass material. Overriding `.card/.panel/.sidebar/...` at `[data-theme="plantx"] X` ties on
   specificity with every `[data-glass="…"] X` rule above (one attribute + one class each), and a
   tie is won by SOURCE ORDER — these sit at the end of the file, so they win regardless of what
   the viewer's own Glass Effects setting is. That is deliberate: a solid-surface skin must stay
   solid whoever's "full glass" preference is also turned on.
   ════════════════════════════════════════════════════════════════════════════════════════════════ */
[data-theme="plantx"] {
  /* ★ THE MESSENGER BUTTON'S LIME IS THE PRIMARY (Vijay 13-10: "not gold, use that colour of messenger icon as
     primary color for plantx") - #e1ff00 with #c8e400 as its deeper step and #12160a as its ink, the exact
     three values views/partials/comms-fab.ejs paints the Messenger button with. It FILLS: the primary button,
     the active rail item, the Messenger button itself. But lime measures 1.13:1 against a pure white page, so
     on THIS ground it cannot be a line, an icon or a word.
     ★ AND THOSE ARE DRAWN IN THE MESSENGER'S OWN INK, NOT A DARKER GREEN (14-10, Vijay: "PlantX skin is in green?").
     The first cut took the lime down to olive (#7a9200 / #5c6e00 / #4a5800) for every line, icon and word, and a
     white page full of olive icons, olive links and an olive logo read as a GREEN skin - nothing like the lime and
     charcoal of the button he named. So, as that button does: LIME FILLS, CHARCOAL DRAWS. --primary (icons,
     borders, bars, the tab line) and every ink are the button's #12160a (18:1); a --primary fill (an avatar, the
     logo, a pager's page) takes white on it; the soft tints stay lime. */
  --primary: #12160a; --primary-dark: #12160a; --primary-light: #f4ffc2; --nav-active-bg: #e1ff00;
  --primary-ink: #12160a; --accent-solid: #12160a;
  /* the accent's own four (the Colour switcher swaps them, [data-accent] below): the FILL and its deeper step (the
     primary button, the pressed switch, the Messenger button), the ink ON that fill, the ink on a --primary fill
     (an avatar, a pager's current page), and the button's glow */
  --px-fill: #e1ff00; --px-fill-2: #c8e400; --px-on: #12160a; --px-on-fill: #ffffff;
  --px-glow: rgba(150,180,0,.30); --px-glow-2: rgba(150,180,0,.42);
  /* ★ PURE WHITE (Vijay 13-10: "you said it will be pure white") - the page itself is white; cards are told apart by
     their line and one soft shadow, never by a grey ground */
  --bg: #ffffff; --card: #ffffff;
  --text: #0f141b; --text-muted: #6b7685; --text-secondary: #3a4350;
  --border: #e3e6eb; --border-strong: #cdd3dc;
  --radius: 16px; --radius-sm: 12px;
  --shadow: 0 1px 2px rgba(15,20,27,.06), 0 8px 24px rgba(15,20,27,.06);
  --shadow-md: 0 2px 6px rgba(15,20,27,.08), 0 10px 28px rgba(15,20,27,.08);
  --shadow-lg: 0 4px 10px rgba(15,20,27,.10), 0 20px 48px rgba(15,20,27,.12);
  --sidebar-bg: #ffffff; --sidebar-hover: #f6f7f9;
  --sidebar-text: #3a4350; --sidebar-active: #0f141b;   /* a light rail needs dark rail ink */
  --success: #0f8a5f; --success-light: #dcf3ea; --success-dark: #047857;
  --warning: #d9620b; --warning-light: #fdebd9; --warning-dark: #b45309;
  --danger: #be123c; --danger-light: #fde4ea; --danger-dark: #9f1239;
  --tint-warn-bg: #fdebd9;    --tint-warn-fg: #b45309;
  --tint-danger-bg: #fde4ea;  --tint-danger-fg: #9f1239;
  --tint-ok-bg: #dcf3ea;      --tint-ok-fg: #047857;
  --tint-info-bg: #f4ffc2;    --tint-info-fg: #12160a;   /* this theme has no blue, so "info" reads lime */
  --tint-neutral-bg: #f6f7f9; --tint-neutral-fg: #3a4350;
  --th-bg: #f6f7f9;
}
[data-theme="plantx-dark"] {
  /* the Messenger button's lime as it is - #e1ff00 on the near-black card reads 15.7:1, so here it is its OWN
     ink, line and fill at once. --accent-solid is a deep olive (white on lime is 1.13:1; on #4d5900 it is
     7.65:1), so a brand-icon gradient or a filled chip can still carry white. */
  --primary: #e1ff00; --primary-dark: #c8e400; --primary-light: rgba(225,255,0,.12);
  --nav-active-bg: rgba(225,255,0,.12);
  --primary-ink: #e1ff00; --accent-solid: #4d5900;
  --px-fill: #e1ff00; --px-fill-2: #c8e400; --px-on: #12160a; --px-on-fill: #12160a;
  --px-glow: rgba(150,180,0,.30); --px-glow-2: rgba(150,180,0,.42);
  --bg: #0a0d12; --card: #121821;
  --text: #eef2f7; --text-muted: #8b97a8; --text-secondary: #c3ccd8;
  --border: #212b38; --border-strong: #2e3a49;
  --radius: 16px; --radius-sm: 12px;
  --shadow: 0 1px 0 rgba(255,255,255,.03) inset, 0 10px 30px rgba(0,0,0,.35);
  --shadow-md: 0 1px 0 rgba(255,255,255,.04) inset, 0 14px 36px rgba(0,0,0,.42);
  --shadow-lg: 0 1px 0 rgba(255,255,255,.05) inset, 0 24px 56px rgba(0,0,0,.5);
  --sidebar-bg: #0d1117; --sidebar-hover: #161e29;
  --sidebar-text: #8b97a8; --sidebar-active: #eef2f7;
  --success: #34d399; --success-light: rgba(52,211,153,.12); --success-dark: #34d399;
  --warning: #ff9a3d; --warning-light: rgba(255,154,61,.13); --warning-dark: #ff9a3d;
  --danger: #fb5373;  --danger-light: rgba(251,83,115,.13);  --danger-dark: #fb5373;
  --tint-warn-bg: rgba(255,154,61,.13);  --tint-warn-fg: #ff9a3d;
  --tint-danger-bg: rgba(251,83,115,.13); --tint-danger-fg: #fb5373;
  --tint-ok-bg: rgba(52,211,153,.12);     --tint-ok-fg: #34d399;
  --tint-info-bg: rgba(225,255,0,.12);    --tint-info-fg: #e1ff00;
  --tint-neutral-bg: rgba(255,255,255,.05); --tint-neutral-fg: #8b97a8;
  --th-bg: #161e29;
  color-scheme: dark;
}

/* the lime's ink — the Messenger button's own #12160a (16.2:1 on #e1ff00) — a token so .btn-primary/.ox-hero/
   .ds-btn can read it; every OTHER theme keeps the :root default of #fff (declared near the top of this file) */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) { --on-primary: var(--px-on); }

/* ---------- surfaces: solid, no blur (the one exception is the top bar, below) ---------- */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .app-layout { background: var(--bg); }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .card,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .panel,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .stat,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .kpi-card,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ds-card,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ovl > .card,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .modal .modal-content,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .apx-modal,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) #preview-body {
  background: var(--card);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
/* .ox-stat keeps its own meaning colour (an "Overdue" tile stays tinted) rather than being flattened
   to --card like its neighbours above — same carve-out [data-glass="off"] makes. */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ox-stat {
  background: var(--stat-tint, var(--card));
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .card:hover,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .panel:hover,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .stat:hover,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .kpi-card:hover {
  box-shadow: var(--shadow-md);
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .sidebar {
  background: var(--sidebar-bg);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border-right: 1px solid var(--border);
}
/* the ONE glass effect this skin keeps — a lightly translucent, blurred top bar (the reference's
   `.pulse`) — everything else on the page is solid. */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .topbar {
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
  box-shadow: none;
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ds-pop,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .nav-fly,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ui-expmenu,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .cacct-pop,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ox-cardpop {
  background: var(--card);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  border-color: var(--border);
}

/* ---------- the rail: the lime on the active item - a soft lime fill and a 3px lime bar in the dark, the full
   Messenger lime with its dark ink on the white rail (the light override follows this rule) ----------
   Scoped past the generic (0,2,0) "vibrant accent glass pill" rule earlier in this file, which
   would otherwise put a full border + glow on every active row and read --primary-dark as its
   text colour — wrong here, since neither --primary nor --primary-dark is a safe ink (see above). */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .nav-item.active,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .nav-item.nav-parent.expanded {
  background: var(--nav-active-bg);
  color: var(--primary-ink);
  font-weight: 600;
  border: none;
  box-shadow: inset 3px 0 0 var(--primary);
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .nav-item.active .nav-icon,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .nav-item.nav-parent.expanded .nav-icon {
  color: var(--primary);
}

/* ---------- the primary button: the Messenger button's lime, both grounds ----------
   The accent's own fill (--px-fill → --px-fill-2: the lime #e1ff00 → #c8e400 by default, the Messenger button's own
   colour and hover) with its own ink (--px-on) in light and dark mode, while on the light ground --primary/--primary-dark are the deeper
   greens a line or a word needs (see the token block). The 1px inner line gives the lime an edge on the white page,
   where it measures only 1.13:1. */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .btn-primary,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ox-hero,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ds-btn,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ui-page-actions .btn-primary {
  background-color: var(--px-fill);
  background-image: linear-gradient(135deg, var(--px-fill), var(--px-fill-2));
  color: var(--on-primary);
  box-shadow: inset 0 0 0 1px rgba(18,22,10,.10), 0 4px 14px var(--px-glow);
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .btn-primary:hover,
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ox-hero:hover {
  box-shadow: inset 0 0 0 1px rgba(18,22,10,.10), 0 6px 18px var(--px-glow-2);
  transform: translateY(-1px);
}

/* ---------- plantx-dark only: native controls and the form fields the base rule hardcodes white ----------
   Mirrors the black theme's own (non-glass) form-control rules: `.form-control` and every bare
   input/select/textarea are recessed — a shade darker than the card they sit on — and the modal
   dialog needs nothing extra, since `.modal-dialog { background: var(--card) }` already follows
   the token (the "black theme" modal rule is a no-op here for exactly that reason). */
[data-theme="plantx-dark"] input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=file]):not([type=color]),
[data-theme="plantx-dark"] select,
[data-theme="plantx-dark"] textarea,
[data-theme="plantx-dark"] .form-control {
  background-color: var(--bg);
  color: var(--text);
  border-color: var(--border);
}
[data-theme="plantx-dark"] input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=file]):not([type=color]):focus,
[data-theme="plantx-dark"] select:focus,
[data-theme="plantx-dark"] textarea:focus,
[data-theme="plantx-dark"] .form-control:focus {
  background-color: var(--card);
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-light);
  outline: none;
}
[data-theme="plantx-dark"] input::placeholder,
[data-theme="plantx-dark"] textarea::placeholder,
[data-theme="plantx-dark"] .form-control::placeholder { color: var(--text-muted); }
[data-theme="plantx-dark"] select option { background: var(--card); color: var(--text); }
/* ★ The PlantX skin's own ground and floating buttons (13-10, Vijay's review of the first render):
   OVERRIDES the generic `.content-area` ambient wash above (the primary/warning gradient read as a cream-peach
   slab across the top - the approved reference is a flat page) and OVERRIDES main.ejs's #help-fab fill
   (--accent-solid is a deep olive here) with a quiet surface button. The Messenger button keeps its own lime -
   since 13-10 it IS this skin's primary, so the gold override that used to sit here is gone. */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .content-area { background-image: none; }
/* OVERRIDES the open-section tint (`.nav-section.is-open`, 4.5% ink) on the light PlantX rail: pure white means the
   rail is white too - an open section is shown by its chevron and its gold active item, as [data-bg="plain"] already does */
[data-theme="plantx"] .nav-section.is-open { background: none; }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) #help-fab { background: var(--card); color: var(--text); border: 1px solid var(--border); box-shadow: var(--shadow-md); }
/* OVERRIDES the rail rule above (`.nav-item.active` ink --primary-ink, icon --primary) on the LIGHT PlantX rail only:
   there the active item is the full Messenger lime (--nav-active-bg), so it takes the Messenger button's own dark ink
   for its word and its icon (16.2:1) - an olive word on neon reads muddy. */
[data-theme="plantx"] .nav-item.active,
[data-theme="plantx"] .nav-item.nav-parent.expanded,
[data-theme="plantx"] .nav-item.active .nav-icon,
[data-theme="plantx"] .nav-item.nav-parent.expanded .nav-icon { color: var(--on-primary); }
/* OVERRIDES the white ink of the shared components that sit on a --primary fill (their own rules earlier in this
   file): on the dark ground that fill is the lime itself, where white measures 1.13:1, and on the light ground
   #7a9200, where the dark ink (5.2:1) beats white (3.54:1). So in both PlantX themes they take --on-primary. */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) :is(.skip-link, .sidebar-user .user-avatar, .side-toggle:hover,
  .topbar-user .avatar-sm, .ds-add-plus, .ox-page, .ds-fn-item.is-here .ds-fn-mark, .ch-callbtn:hover,
  .ui-chain.is-trail .ui-chain-step.now .ui-chain-node, .ui-chain-step.here .ui-chain-dot, .wv-btn.on, .gp-callbtn:hover,
  .wb-pick.sel .wb-pick-tick, .ox-trail-av, .pr-chip-on) { color: var(--px-on-fill); }
/* the three gradients under white ink (.login-brand .logo-icon, .form-summary-card, a section header row on hover)
   run --primary → --primary-dark: lime → #c8e400 in the dark, so they take the dark ink there too; on the light
   ground they run #7a9200 → #5c6e00, where white holds (3.5-5.7:1) and stays. */
[data-theme="plantx-dark"] :is(.login-brand .logo-icon, .form-summary-card, .form-table tr.section-header:hover td) { color: var(--px-on-fill); }
/* ---------- the light/dark switch in the top bar, as Liquid Glass has it (Vijay 13-10) ----------
   OVERRIDES `.lg-ground { display: none }` above for these two themes. Liquid's switch picks a ground for the screen;
   this one flips PlantX <-> PlantX Dark and saves it to the account (partials/theme-resolve.ejs pxGround), since the
   two are two choices in the picker. Same shape as Liquid's (two 30px round buttons, 17px icons), on this skin's solid
   surface, the pressed one in the Messenger lime with its dark ink. */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .lg-ground {
  display: flex; align-items: center; gap: var(--space-1); padding: var(--space-1);
  border-radius: 999px; border: 1px solid var(--border); background: var(--card);
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .lg-ground button {
  width: 30px; height: 30px; padding: 0; border: none; border-radius: 999px; background: transparent;
  display: inline-flex; align-items: center; justify-content: center; color: var(--text-muted); cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .lg-ground button svg { width: 17px; height: 17px; }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .lg-ground button:hover { color: var(--text); background: var(--sidebar-hover); }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .lg-ground button.on {
  background: linear-gradient(135deg, var(--px-fill), var(--px-fill-2)); color: var(--px-on);
  box-shadow: inset 0 0 0 1px rgba(18,22,10,.10), 0 2px 8px var(--px-glow);
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .lg-ground button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ---------- THE LIGHT LIME: B's chips, D's side menu (14-10) ----------
   Vijay, after "PlantX skin is in green?" and "now in light theme it is showing black?", chose from four previews
   (scratchpad uiaudit/plantx-light.html): "B, but the side menu design is D". On the white page with the lime colour:
   - THE SIDE MENU (D): a white rail since 14-10 (it was faintly lime with a lime edge), its icons a deeper chartreuse that still reads as lime
     (#a6bf00 - never the olive that made it green), the selected item the full lime with the Messenger's dark ink and
     a chartreuse bar, the logo a lime square with a dark letter.
   - THE PAGE (B): every icon sits on a lime CHIP with a dark glyph - a register row's (.ox-rowic), a module card's
     (.md-ic), a plain stat tile's (.ox-ic-plain) - and the selected tab is filled (19-10; a thick lime bar before).
   Words and lines stay the dark ink (the block above), so everything still reads. Only the light ground with the
   lime colour; PlantX Dark and the other colours are untouched. Each rule below OVERRIDES the skin's own rule for the
   same thing earlier in this section (the rail, the active item, the logo, the chips, the tab). */
/* ★ THE SIDE MENU IS WHITE (14-10, Vijay: "why side menu bg changing? in plantx skin?" then "yes... apply" to option 1): D's
   faint lime ground (#fbffe9, a lime edge #e8f1b8) is gone - it made the menu change colour whenever the colour changed,
   since every other colour's menu is white. D's lime icons, its full-lime selected item and the lime logo square stay. */
:root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) { --sidebar-bg: #ffffff; --sidebar-hover: #f7fbe6; }
:root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) .sidebar { border-right-color: var(--border); }
:root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) .sidebar .nav-item .nav-icon { color: #a6bf00; }
:root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) .sidebar .nav-item.active, :root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) .sidebar .nav-item.nav-parent.expanded { box-shadow: inset 3px 0 0 #a6bf00; }
:root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) .sidebar .nav-item.active .nav-icon, :root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) .sidebar .nav-item.nav-parent.expanded .nav-icon { color: #12160a; }
:root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) .sidebar-brand .brand-icon { background: linear-gradient(135deg, #e1ff00, #b8d200); color: #12160a; }
:root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) :is(.md-ic, .ox-ic-plain) { background: #e1ff00; color: #12160a; }
:root[data-theme="plantx"]:is([data-accent="lime"], :not([data-accent])) .ox-rowic { background: #efff9e; color: #12160a; }
/* ★ THE SELECTED TAB IS FILLED (19-10, Vijay: "selected menu show full highlighted with background color.. not just a bottom bar") -
   on PlantX and PlantX Dark, in every colour: the colour's own fill with its own ink, the way the selected side-menu item and the
   primary button are drawn. It replaces the thick lime bar of 14-10. */
:root:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ox-tabs2 button.on { background: linear-gradient(135deg, var(--px-fill), var(--px-fill-2)); color: var(--px-on); border-bottom-color: transparent; box-shadow: none; }

/* ---------- HEADINGS AND FIGURES AT THE PREVIEW'S WEIGHT (14-10) ----------
   Vijay: "H1 & H2, analytics box numbers needs extra bold..., that is what you showed in plantx skin preview isnt it?" -
   it is: the approved preview (scratchpad build/skin-reference.html) set the titles and every big figure heavy (its
   .big and .v strong at 800) and the skin had kept the app's 600. Every page title and section heading, and every
   number in an analytics box - the module and register tiles (.md-cube), the stat tiles (.ox-stat), the waiting pills
   (.b2w), the portfolio's figures and health ring, the stores tiles, the KPI values - takes 800 on both grounds. A
   typeface that stops at 700 (IBM Plex, Barlow) draws its own boldest. */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) :is(h1, h2, .ox-title, .page-title) { font-weight: 800; }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) :is(.md-cube .n, .ox-stat b, .b2w b, .pf-fig b, .pf-ring b,
  .st-tile .st-n, .kpi-value, .summary-value, .metric-value) { font-weight: 800; }

/* ---------- the COLOUR switcher (Vijay 13-10: "i asked you color switcher for plantx") ----------
   PlantX's own preset colours, as Sky is Liquid Glass's: Appearance -> Colour, shown only on PlantX / PlantX Dark,
   saved per account (`user_prefs.accent`, <html data-accent>). LIME, the Messenger button's own colour, is the token
   blocks above and the default; each block below swaps only tokens, for each ground, every value measured: a line or
   icon >= 3:1 and a word >= 4.5:1 on its ground (--primary / --primary-dark / --primary-ink), the fill's own ink
   (--px-on) and the ink on a --primary fill (--px-on-fill). On the white page the selected rail item is the fill
   itself (--nav-active-bg), taken a step deeper where the fill would not carry its ink (blue, violet).
   ★ THE MESSENGER BUTTON NEVER TAKES THE CHOSEN COLOUR (Vijay 13-10: "messenger color doesnt change with color
   switch"). It keeps its own fixed lime on every colour, as on every theme (views/partials/comms-fab.ejs) - so
   nothing here touches #comms-fab-wrap. */
[data-theme="plantx"][data-accent="gold"] {
  --primary: #a87c06; --primary-dark: #8a6508; --primary-light: #f6ecd2; --nav-active-bg: #f2c75c;
  --primary-ink: #7a5a05; --accent-solid: #7a5a05; --tint-info-bg: #f6ecd2; --tint-info-fg: #7a5a05;
  --px-fill: #f2c75c; --px-fill-2: #d4a017; --px-on: #2b1d02; --px-on-fill: #1a1102;
  --px-glow: rgba(212,160,23,.30); --px-glow-2: rgba(212,160,23,.40);
}
[data-theme="plantx-dark"][data-accent="gold"] {
  --primary: #f2c75c; --primary-dark: #e3b341; --primary-light: rgba(227,179,65,.13); --nav-active-bg: rgba(227,179,65,.13);
  --primary-ink: #f2c75c; --accent-solid: #7a5a12; --tint-info-bg: rgba(227,179,65,.13); --tint-info-fg: #f2c75c;
  --px-fill: #f2c75c; --px-fill-2: #d4a017; --px-on: #2b1d02; --px-on-fill: #2b1d02;
  --px-glow: rgba(212,160,23,.30); --px-glow-2: rgba(212,160,23,.40);
}
[data-theme="plantx"][data-accent="blue"] {
  --primary: #3b82f6; --primary-dark: #1d4ed8; --primary-light: #dbeafe; --nav-active-bg: #2a6de6;
  --primary-ink: #1d4ed8; --accent-solid: #1d4ed8; --tint-info-bg: #dbeafe; --tint-info-fg: #1d4ed8;
  --px-fill: #3b82f6; --px-fill-2: #1d4ed8; --px-on: #ffffff; --px-on-fill: #ffffff;
  --px-glow: rgba(59,130,246,.30); --px-glow-2: rgba(59,130,246,.40);
}
[data-theme="plantx-dark"][data-accent="blue"] {
  --primary: #60a5fa; --primary-dark: #60a5fa; --primary-light: rgba(96,165,250,.14); --nav-active-bg: rgba(96,165,250,.14);
  --primary-ink: #93c5fd; --accent-solid: #1d4ed8; --tint-info-bg: rgba(96,165,250,.14); --tint-info-fg: #93c5fd;
  --px-fill: #3b82f6; --px-fill-2: #1d4ed8; --px-on: #ffffff; --px-on-fill: #0b1220;
  --px-glow: rgba(59,130,246,.35); --px-glow-2: rgba(59,130,246,.45);
}
[data-theme="plantx"][data-accent="teal"] {
  --primary: #0d9488; --primary-dark: #0f766e; --primary-light: #ccfbf1; --nav-active-bg: #2dd4bf;
  --primary-ink: #115e59; --accent-solid: #115e59; --tint-info-bg: #ccfbf1; --tint-info-fg: #115e59;
  --px-fill: #2dd4bf; --px-fill-2: #14b8a6; --px-on: #042f2a; --px-on-fill: #021f1c;
  --px-glow: rgba(20,184,166,.30); --px-glow-2: rgba(20,184,166,.40);
}
[data-theme="plantx-dark"][data-accent="teal"] {
  --primary: #2dd4bf; --primary-dark: #14b8a6; --primary-light: rgba(45,212,191,.13); --nav-active-bg: rgba(45,212,191,.13);
  --primary-ink: #5eead4; --accent-solid: #0f766e; --tint-info-bg: rgba(45,212,191,.13); --tint-info-fg: #5eead4;
  --px-fill: #2dd4bf; --px-fill-2: #14b8a6; --px-on: #042f2a; --px-on-fill: #042f2a;
  --px-glow: rgba(20,184,166,.30); --px-glow-2: rgba(20,184,166,.40);
}
[data-theme="plantx"][data-accent="violet"] {
  --primary: #7c3aed; --primary-dark: #6d28d9; --primary-light: #ede9fe; --nav-active-bg: #7c3aed;
  --primary-ink: #5b21b6; --accent-solid: #5b21b6; --tint-info-bg: #ede9fe; --tint-info-fg: #5b21b6;
  --px-fill: #8b5cf6; --px-fill-2: #6d28d9; --px-on: #ffffff; --px-on-fill: #ffffff;
  --px-glow: rgba(124,58,237,.30); --px-glow-2: rgba(124,58,237,.40);
}
[data-theme="plantx-dark"][data-accent="violet"] {
  --primary: #a78bfa; --primary-dark: #a78bfa; --primary-light: rgba(167,139,250,.14); --nav-active-bg: rgba(167,139,250,.14);
  --primary-ink: #c4b5fd; --accent-solid: #6d28d9; --tint-info-bg: rgba(167,139,250,.14); --tint-info-fg: #c4b5fd;
  --px-fill: #8b5cf6; --px-fill-2: #6d28d9; --px-on: #ffffff; --px-on-fill: #1e1033;
  --px-glow: rgba(124,58,237,.35); --px-glow-2: rgba(124,58,237,.45);
}
/* ★ SIX MORE (14-10, Vijay: "intoduce mote colors in color picker for plantX") - the same tokens, every pair measured
   the same way (_e2e_skin_plantx.js walks each colour on both grounds). */
[data-theme="plantx"][data-accent="orange"] {
  --primary: #ea580c; --primary-dark: #c2410c; --primary-light: #ffedd5; --nav-active-bg: #fb923c;
  --primary-ink: #9a3412; --accent-solid: #9a3412; --tint-info-bg: #ffedd5; --tint-info-fg: #9a3412;
  --px-fill: #fb923c; --px-fill-2: #f97316; --px-on: #2a1205; --px-on-fill: #ffffff;
  --px-glow: rgba(249,115,22,.30); --px-glow-2: rgba(249,115,22,.40);
}
[data-theme="plantx-dark"][data-accent="orange"] {
  --primary: #fb923c; --primary-dark: #fdba74; --primary-light: rgba(251,146,60,.14); --nav-active-bg: rgba(251,146,60,.14);
  --primary-ink: #fdba74; --accent-solid: #c2410c; --tint-info-bg: rgba(251,146,60,.14); --tint-info-fg: #fdba74;
  --px-fill: #fb923c; --px-fill-2: #f97316; --px-on: #2a1205; --px-on-fill: #2a1205;
  --px-glow: rgba(249,115,22,.35); --px-glow-2: rgba(249,115,22,.45);
}
[data-theme="plantx"][data-accent="pink"] {
  --primary: #db2777; --primary-dark: #be185d; --primary-light: #fce7f3; --nav-active-bg: #db2777;
  --primary-ink: #9d174d; --accent-solid: #9d174d; --tint-info-bg: #fce7f3; --tint-info-fg: #9d174d;
  --px-fill: #ec4899; --px-fill-2: #db2777; --px-on: #ffffff; --px-on-fill: #ffffff;
  --px-glow: rgba(219,39,119,.30); --px-glow-2: rgba(219,39,119,.40);
}
[data-theme="plantx-dark"][data-accent="pink"] {
  --primary: #f472b6; --primary-dark: #f9a8d4; --primary-light: rgba(244,114,182,.14); --nav-active-bg: rgba(244,114,182,.14);
  --primary-ink: #f9a8d4; --accent-solid: #be185d; --tint-info-bg: rgba(244,114,182,.14); --tint-info-fg: #f9a8d4;
  --px-fill: #ec4899; --px-fill-2: #db2777; --px-on: #ffffff; --px-on-fill: #3b0a22;
  --px-glow: rgba(219,39,119,.35); --px-glow-2: rgba(219,39,119,.45);
}
[data-theme="plantx"][data-accent="indigo"] {
  --primary: #6366f1; --primary-dark: #4338ca; --primary-light: #e0e7ff; --nav-active-bg: #4f46e5;
  --primary-ink: #3730a3; --accent-solid: #3730a3; --tint-info-bg: #e0e7ff; --tint-info-fg: #3730a3;
  --px-fill: #6366f1; --px-fill-2: #4338ca; --px-on: #ffffff; --px-on-fill: #ffffff;
  --px-glow: rgba(99,102,241,.30); --px-glow-2: rgba(99,102,241,.40);
}
[data-theme="plantx-dark"][data-accent="indigo"] {
  --primary: #818cf8; --primary-dark: #a5b4fc; --primary-light: rgba(129,140,248,.14); --nav-active-bg: rgba(129,140,248,.14);
  --primary-ink: #a5b4fc; --accent-solid: #4338ca; --tint-info-bg: rgba(129,140,248,.14); --tint-info-fg: #a5b4fc;
  --px-fill: #6366f1; --px-fill-2: #4338ca; --px-on: #ffffff; --px-on-fill: #0f1033;
  --px-glow: rgba(99,102,241,.35); --px-glow-2: rgba(99,102,241,.45);
}
/* ★ NAVY (14-10, Vijay: "navy blue color missing in new color picker in plantx") */
[data-theme="plantx"][data-accent="navy"] {
  --primary: #1e3a8a; --primary-dark: #172554; --primary-light: #dbe4ff; --nav-active-bg: #1e3a8a;
  --primary-ink: #1e3a8a; --accent-solid: #1e3a8a; --tint-info-bg: #dbe4ff; --tint-info-fg: #1e3a8a;
  --px-fill: #1e3a8a; --px-fill-2: #172554; --px-on: #ffffff; --px-on-fill: #ffffff;
  --px-glow: rgba(30,58,138,.30); --px-glow-2: rgba(30,58,138,.40);
}
/* navy on the dark ground: the button stays navy with white ink; a line, an icon or a word takes a lighter periwinkle of
   it (a navy line on graphite would not be seen) */
[data-theme="plantx-dark"][data-accent="navy"] {
  --primary: #93a8f4; --primary-dark: #a5b8fb; --primary-light: rgba(147,168,244,.14); --nav-active-bg: rgba(147,168,244,.14);
  --primary-ink: #b4c4fa; --accent-solid: #1e3a8a; --tint-info-bg: rgba(147,168,244,.14); --tint-info-fg: #b4c4fa;
  --px-fill: #1e3a8a; --px-fill-2: #172554; --px-on: #ffffff; --px-on-fill: #0b1533;
  --px-glow: rgba(30,58,138,.45); --px-glow-2: rgba(30,58,138,.55);
}
[data-theme="plantx"][data-accent="sky"] {
  --primary: #0284c7; --primary-dark: #0369a1; --primary-light: #e0f2fe; --nav-active-bg: #38bdf8;
  --primary-ink: #075985; --accent-solid: #075985; --tint-info-bg: #e0f2fe; --tint-info-fg: #075985;
  --px-fill: #38bdf8; --px-fill-2: #0ea5e9; --px-on: #04212f; --px-on-fill: #ffffff;
  --px-glow: rgba(14,165,233,.30); --px-glow-2: rgba(14,165,233,.40);
}
[data-theme="plantx-dark"][data-accent="sky"] {
  --primary: #38bdf8; --primary-dark: #7dd3fc; --primary-light: rgba(56,189,248,.14); --nav-active-bg: rgba(56,189,248,.14);
  --primary-ink: #7dd3fc; --accent-solid: #0369a1; --tint-info-bg: rgba(56,189,248,.14); --tint-info-fg: #7dd3fc;
  --px-fill: #38bdf8; --px-fill-2: #0ea5e9; --px-on: #04212f; --px-on-fill: #04212f;
  --px-glow: rgba(14,165,233,.35); --px-glow-2: rgba(14,165,233,.45);
}
[data-theme="plantx"][data-accent="emerald"] {
  --primary: #059669; --primary-dark: #047857; --primary-light: #d1fae5; --nav-active-bg: #34d399;
  --primary-ink: #065f46; --accent-solid: #065f46; --tint-info-bg: #d1fae5; --tint-info-fg: #065f46;
  --px-fill: #34d399; --px-fill-2: #10b981; --px-on: #022c22; --px-on-fill: #ffffff;
  --px-glow: rgba(16,185,129,.30); --px-glow-2: rgba(16,185,129,.40);
}
[data-theme="plantx-dark"][data-accent="emerald"] {
  --primary: #34d399; --primary-dark: #6ee7b7; --primary-light: rgba(52,211,153,.14); --nav-active-bg: rgba(52,211,153,.14);
  --primary-ink: #6ee7b7; --accent-solid: #047857; --tint-info-bg: rgba(52,211,153,.14); --tint-info-fg: #6ee7b7;
  --px-fill: #34d399; --px-fill-2: #10b981; --px-on: #022c22; --px-on-fill: #022c22;
  --px-glow: rgba(16,185,129,.35); --px-glow-2: rgba(16,185,129,.45);
}
[data-theme="plantx"][data-accent="slate"] {
  --primary: #64748b; --primary-dark: #334155; --primary-light: #f1f5f9; --nav-active-bg: #475569;
  --primary-ink: #1e293b; --accent-solid: #1e293b; --tint-info-bg: #f1f5f9; --tint-info-fg: #1e293b;
  --px-fill: #64748b; --px-fill-2: #334155; --px-on: #ffffff; --px-on-fill: #ffffff;
  --px-glow: rgba(71,85,105,.30); --px-glow-2: rgba(71,85,105,.40);
}
[data-theme="plantx-dark"][data-accent="slate"] {
  --primary: #94a3b8; --primary-dark: #cbd5e1; --primary-light: rgba(148,163,184,.14); --nav-active-bg: rgba(148,163,184,.14);
  --primary-ink: #cbd5e1; --accent-solid: #334155; --tint-info-bg: rgba(148,163,184,.14); --tint-info-fg: #cbd5e1;
  --px-fill: #64748b; --px-fill-2: #334155; --px-on: #ffffff; --px-on-fill: #0f172a;
  --px-glow: rgba(71,85,105,.35); --px-glow-2: rgba(71,85,105,.45);
}


/* ═══ THE PRODUCTION BOARD (public/js/core/prod-board.js) — target vs actual as a smooth annotated line, its
   figures, where the hours went, what it consumed, plan to date. Scoped to `.prod-board`, tokens only. ═══ */
/* THE PRODUCTION BOARD — scoped to this page so no shared component is redefined. Every colour is a
   token; the six themes re-dress it. Rows are declared (lead · pair · full), never a spanning grid that
   reflows two-and-one when the window narrows. */
.prod-board .pb-board{display:flex;flex-direction:column;gap:14px;margin:0 0 16px}
.prod-board .pb-row{display:grid;gap:14px;min-width:0}
.prod-board .pb-row.lead{grid-template-columns:minmax(0,1fr) 250px}
.prod-board .pb-row.pair{grid-template-columns:repeat(2,minmax(0,1fr))}
.prod-board .pb-row.full{grid-template-columns:minmax(0,1fr)}
.prod-board .pb-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);padding:16px 18px 12px;min-width:0}
.prod-board .pb-h{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:6px;flex-wrap:wrap}
.prod-board .pb-h h3{margin:0;font-size:15.5px;font-weight:600;letter-spacing:-.02em}
.prod-board .pb-h p{margin:2px 0 0;font-size:12.5px;color:var(--text-muted)}
.prod-board .pb-kpi{text-align:right;white-space:nowrap}
.prod-board .pb-kpi b{display:block;font-size:22px;font-weight:700;letter-spacing:-.03em;line-height:1;font-variant-numeric:tabular-nums}
.prod-board .pb-kpi b.bad{color:var(--danger-dark)}
.prod-board .pb-kpi b.ok{color:var(--success-dark)}
.prod-board .pb-kpi span{font-size:11px;color:var(--text-hint)}
.prod-board .pb-seg{display:inline-flex;background:var(--bg);border-radius:9px;padding:2px;gap:2px}
.prod-board .pb-seg button{border:0;background:none;border-radius:7px;padding:4px 10px;font-size:11.5px;font-weight:500;color:var(--text-muted);cursor:pointer}
.prod-board .pb-seg button.on{background:var(--card);color:var(--text);font-weight:600;box-shadow:var(--shadow)}
.prod-board .pb-side{display:flex;flex-direction:column;gap:14px;min-width:0}
.prod-board .pb-m{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);padding:14px 16px;flex:1;display:flex;flex-direction:column;justify-content:center}
.prod-board .pb-m .ml{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--text-hint)}
.prod-board .pb-m .mv{font-size:32px;font-weight:700;letter-spacing:-.04em;line-height:1.05;margin:5px 0 2px;font-variant-numeric:tabular-nums}
.prod-board .pb-m .mv i{font-style:normal;font-size:15px;font-weight:600;margin-left:3px;opacity:.6}
.prod-board .pb-m .mv.bad{color:var(--danger-dark)}
.prod-board .pb-m .mv.warn{color:var(--warning-dark)}
.prod-board .pb-m .mv.ok{color:var(--success-dark)}
.prod-board .pb-m .ms{font-size:11.5px;color:var(--text-muted);margin-top:5px}
.prod-board .pb-bl{width:100%;height:30px;margin-top:6px;display:block}
.prod-board svg.pb-ch{width:100%;height:auto;display:block;margin-top:6px;overflow:visible}
.prod-board .pb-chwrap{position:relative}
.prod-board .pb-tip{position:absolute;top:6px;transform:translateX(-50%);z-index:5;min-width:170px;background:var(--sidebar-bg);color:var(--sidebar-text);border-radius:11px;padding:10px 12px;box-shadow:var(--shadow-lg);font-size:12px;pointer-events:none}
.prod-board .pb-tip.flip{transform:translateX(-100%) translateX(-14px)}
.prod-board .pb-tip b{display:block;font-size:12.5px;font-weight:600;margin-bottom:6px;padding-bottom:5px;border-bottom:1px solid rgba(255,255,255,.12)}
.prod-board .pb-tip .r{display:flex;justify-content:space-between;gap:14px;padding:2px 0}
.prod-board .pb-tip .r span{opacity:.72}
.prod-board .pb-tip .r em{font-style:normal;font-weight:600;font-variant-numeric:tabular-nums}
.prod-board .pb-tip .r.bad em{color:var(--danger)}
.prod-board .pb-tip .r.ok em{color:var(--success)}
.prod-board .pb-tip .r.warn em{color:var(--warning)}
.prod-board .pb-lg{display:flex;gap:16px;flex-wrap:wrap;margin-top:10px;padding-top:10px;border-top:1px solid var(--border);font-size:11.5px;color:var(--text-muted)}
.prod-board .pb-lg i{display:inline-block;width:22px;height:0;border-top:2.5px solid var(--primary);vertical-align:3px;margin-right:7px;border-radius:2px}
.prod-board .pb-lg i.d{border-top-style:dashed;border-top-color:var(--text);opacity:.6}
.prod-board .pb-lg i.area{height:9px;border:0;background:var(--tint-danger-bg);width:16px;vertical-align:-1px}
.prod-board .pb-cb{display:flex;flex-direction:column;gap:8px}
.prod-board .pb-cbr{display:flex;align-items:center;gap:12px}
.prod-board .pb-cbr .nm{flex:0 0 180px;font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.prod-board .pb-cbr .tr{flex:1;min-width:0;height:9px;border-radius:5px;background:var(--bg);overflow:hidden}
.prod-board .pb-cbr .tr i{display:block;height:100%;border-radius:5px;background:var(--primary)}
.prod-board .pb-cbr .vv{flex:0 0 56px;text-align:right;font-size:12.5px;font-weight:700;font-variant-numeric:tabular-nums}
.prod-board .pb-crow{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:6px}
.prod-board .pb-ci{padding:12px 14px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm)}
.prod-board .pb-ci b{display:block;font-size:20px;font-weight:700;letter-spacing:-.03em;font-variant-numeric:tabular-nums;line-height:1}
.prod-board .pb-ci b i{font-style:normal;font-size:11px;font-weight:600;color:var(--text-hint);margin-left:4px}
.prod-board .pb-ci span{display:block;font-size:12.5px;font-weight:500;margin-top:4px}
.prod-board .pb-ci em{display:block;font-style:normal;font-size:10.5px;color:var(--text-hint);margin-top:2px}
.prod-board .pb-cue{display:flex;align-items:flex-start;gap:10px;margin:0 0 14px;padding:13px 15px;background:var(--card);border:1px dashed var(--border);border-radius:var(--radius);font-size:13px;color:var(--text-muted)}
.prod-board .pb-cue.warn{background:var(--warning-light);border-color:var(--warning);color:var(--warning-dark)}
.prod-board .pb-cue b{font-weight:600}
@media (max-width:1020px){.prod-board .pb-row.lead{grid-template-columns:minmax(0,1fr)}.prod-board .pb-side{flex-direction:row}.prod-board .pb-side .pb-m{flex:1 1 0}}
@media (max-width:700px){.prod-board .pb-row.pair{grid-template-columns:minmax(0,1fr)}.prod-board .pb-side{flex-direction:column}.prod-board .pb-cbr .nm{flex-basis:120px}}

/* ★ THE TOP BAR FITS ITS SCREEN (28-09). views/layouts/main.ejs topbarFit() MEASURES the bar and stamps
   html.tb-c1 / tb-c2 / tb-c3 one step at a time until nothing runs past it — never a breakpoint, because the
   sidebar, the role, the language and the theme all change what the bar holds. A bar that fits gets none of these.
   Nothing but the title may shrink, so an overflow is an overflow (a squeezed Report Day used to wrap to five lines
   instead of being measured as too wide). The phone rule above (max-width 600px) still hides what a phone cannot use. */
.topbar > .topbar-date, .topbar > .topbar-actions, .topbar > .lg-ground { flex-shrink: 0; }
.topbar .topbar-date { white-space: nowrap; }
#topbar-report-day .rd-short { display: none; }
html.tb-c1 #topbar-report-day .rd-full, html.tb-c1 #topbar-report-day .rd-win, html.tb-c1 .topbar .tb-lbl { display: none; }
html.tb-c1 #topbar-report-day .rd-short { display: inline; }
html.tb-c2 .topbar #fs-btn, html.tb-c2 .topbar #lang-btn, html.tb-c2 .topbar .lg-ground { display: none !important; }
html.tb-c3 .topbar .topbar-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
html.tb-c3 #topbar-current-premises .ds-select, html.tb-c3 #topbar-current-premises select { max-width: 150px; }
/* what the bar let go appears in the account menu — and on a phone, where the bar never holds it */
.cacct-pop .pit.tb-moved { display: none; }
html.tb-c2 .cacct-pop .pit.tb-moved { display: flex; }
html:not(:is([data-theme="liquid"],[data-theme="liquid-dark"],[data-theme="plantx"],[data-theme="plantx-dark"])) .cacct-pop .pit.tb-moved.lg-only { display: none; }
.cacct-pop .pit.tb-moved.is-on { color: var(--primary-dark); font-weight: 600; }
html[lang="en"] .cacct-pop .pit .tb-cap { text-transform: capitalize; }
@media (max-width: 600px) {
  .cacct-pop .pit.tb-moved { display: flex; }
  html:not(:is([data-theme="liquid"],[data-theme="liquid-dark"],[data-theme="plantx"],[data-theme="plantx-dark"])) .cacct-pop .pit.tb-moved.lg-only { display: none; }
}

/* ═══ THE RIGHT-HAND DRAWER, THE FACTS GRID AND THE TRAIL (Cover & Work Orders preview, v4 — approved 30-09) ═══════
   A form that belongs beside the page opens from the right and leaves the page visible behind it (Request Cover, Raise
   Requisition); a record's facts read as label-over-value pairs in two columns; a signature trail reads as people with
   a state. Shared, so every page that opens a form or shows a record draws them the same way. ui.drawer / ui.facts /
   ui.trail in public/js/core/ui.js. */
/* its veil, its surface, its edge and its motion are a side page's (§44 - .ox-drawer is a .ui-side); only its frame here */
.ox-drawer-ov { position:fixed; inset:0; z-index:1600; display:none; justify-content:flex-end; }
.ox-drawer { width:600px; max-width:100%; height:100%; display:flex; flex-direction:column; }
.ox-drawer-h { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; padding:14px 18px; border-bottom:1px solid var(--border); }
.ox-drawer-h b { display:block; font-size:1rem; }
.ox-drawer-h small { display:block; font-size:.78rem; color:var(--text-muted); margin-top:2px; }
.ox-drawer-x { background:none; border:none; color:var(--text-muted); cursor:pointer; padding:4px; border-radius:var(--radius-sm); line-height:0; }
.ox-drawer-x:hover, .ox-drawer-x:focus-visible { background:var(--bg); color:var(--text); outline:none; }
.ox-drawer-b { flex:1; overflow:auto; padding:14px 18px; display:grid; gap:12px; align-content:start; }
.ox-drawer-f { display:flex; justify-content:space-between; align-items:center; gap:10px; padding:12px 18px; border-top:1px solid var(--border); background:var(--bg); }
.ox-drawer-f .ox-remark { flex:1; min-width:0; border:none; background:transparent; font-size:.82rem; color:var(--text); padding:6px 0; }
.ox-drawer-f .ox-remark:focus { outline:none; }
.ox-fld { display:grid; grid-template-columns:minmax(0,1fr); gap:5px; min-width:0; }
/* a select sizes itself to its longest option; inside a column it must take the column's width, not push past it */
.ox-fld > .form-control, .ox-fld > .ox-unit > .form-control { width:100%; min-width:0; }
.ox-fld > label { font-size:.76rem; font-weight:600; color:var(--text-muted); }
.ox-fld > label em { font-style:normal; color:var(--danger); }
.ox-hint { font-size:.76rem; color:var(--text-muted); }
.ox-g2 { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }
.ox-g3 { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
.ox-unit { position:relative; }
.ox-unit > .ox-unit-u { position:absolute; right:10px; top:50%; transform:translateY(-50%); font-size:.76rem; color:var(--text-muted); pointer-events:none; }
.ox-unit > .form-control { padding-right:58px; }
.ox-unit > .ox-unit-p { position:absolute; left:10px; top:50%; transform:translateY(-50%); font-size:.86rem; color:var(--text); pointer-events:none; }
.ox-unit.pre > .form-control { padding-left:24px; }
.ox-chips-box { display:flex; flex-wrap:wrap; gap:6px; align-items:center; min-height:38px; border:1px solid var(--border); border-radius:var(--radius-sm); padding:6px 8px; background:var(--card); }
.ox-pick { display:inline-flex; align-items:center; gap:6px; background:var(--primary-light); color:var(--primary-dark); border:1px solid color-mix(in srgb, var(--primary) 30%, transparent); border-radius:999px; padding:2px 6px 2px 10px; font-size:.8rem; font-weight:600; }
.ox-pick button { background:none; border:none; color:inherit; cursor:pointer; padding:0 2px; font-size:.9rem; line-height:1; }
.ox-add-link { display:inline-flex; align-items:center; gap:5px; font-size:.8rem; color:var(--text-muted); background:none; border:none; cursor:pointer; padding:2px 4px; }
.ox-add-link:hover, .ox-add-link:focus-visible { color:var(--primary-dark); outline:none; }
.ox-pick-list { border:1px solid var(--border); border-radius:var(--radius-sm); max-height:190px; overflow:auto; background:var(--card); }
.ox-pick-list button { display:block; width:100%; text-align:left; background:none; border:none; border-bottom:1px solid var(--border); padding:7px 10px; font-size:.84rem; color:var(--text); cursor:pointer; }
.ox-pick-list button:hover, .ox-pick-list button:focus-visible { background:var(--primary-light); outline:none; }
.ox-note-box { border:1px dashed var(--border); border-radius:var(--radius-sm); padding:10px 12px; font-size:.8rem; color:var(--text); background:var(--bg); line-height:1.5; }
.ox-note-box.info { border-style:solid; border-color:color-mix(in srgb, var(--primary) 30%, transparent); background:var(--primary-light); }
.ox-note-box.warn { border-style:solid; border-color:var(--warning); background:var(--warning-light); color:var(--tint-warn-fg, #92400e); }
.ox-note-box.bad { border-style:solid; border-color:color-mix(in srgb, var(--danger) 45%, transparent); background:var(--danger-light, #fef2f2); }
.ox-kv { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px 20px; }
/* direct children only: a value may carry its own inline pieces (a code, a pill) that must stay in the line */
.ox-kv > div > small { display:block; font-size:.72rem; font-weight:600; color:var(--text-muted); margin-bottom:2px; }
.ox-kv > div > span { display:block; font-size:.86rem; line-height:1.45; }
.ox-trail-row { display:grid; grid-template-columns:30px minmax(0,1fr) auto; gap:10px; align-items:center; padding:9px 0; border-top:1px solid var(--border); }
.ox-trail-row:first-child { border-top:0; }
.ox-trail-av { width:30px; height:30px; border-radius:50%; background:var(--primary); color:#fff; display:grid; place-items:center; font-size:.68rem; font-weight:700; }
.ox-trail-av.sys { background:var(--text-muted); }
.ox-trail-row b { display:block; font-size:.86rem; }
.ox-trail-row small { display:block; font-size:.74rem; color:var(--text-muted); }
.ox-2col { display:grid; grid-template-columns:minmax(0,1.45fr) minmax(0,1fr); gap:14px; align-items:start; }
@media (max-width:980px) { .ox-2col { grid-template-columns:minmax(0,1fr); } .ox-g3 { grid-template-columns:minmax(0,1fr); } }
@media (max-width:640px) { .ox-g2, .ox-kv { grid-template-columns:minmax(0,1fr); } }

/* ── THE CONVERSATION ON AN ORDER (public/js/order-thread.js — core/order-thread.js) ─────────────────────────────────
   The vendor's lines sit left on a plain card, the plant's right on the primary tint; a stage, a tracking update and a
   request for status are one quiet line in the middle. Used on the order in Purchasing and the store's drawer. */
.ot-th { display:grid; gap:8px; margin:6px 0 12px; }
.ot-none { font-size:.82rem; color:var(--text-muted); margin:6px 0 10px; }
.ot-tm { max-width:86%; justify-self:start; border:1px solid var(--border); border-radius:var(--radius); padding:8px 12px; background:var(--card); font-size:.85rem; }
.ot-tm.us { justify-self:end; background:var(--primary-light); border-color:color-mix(in srgb, var(--primary) 30%, transparent); }
.ot-tw { font-size:.74rem; color:var(--text-muted); margin-bottom:3px; }
.ot-tw b { color:var(--text); }
.ot-tx { white-space:pre-wrap; overflow-wrap:anywhere; line-height:1.5; }
.ot-topic { display:inline-block; font-size:.68rem; font-weight:700; border-radius:999px; padding:0 7px; background:var(--tint-neutral-bg, #f1f5f9); color:var(--text-muted); }
.ot-ts { justify-self:center; font-size:.76rem; color:var(--text-muted); background:var(--tint-neutral-bg, #f1f5f9); border-radius:999px; padding:3px 12px; text-align:center; }
.ot-ts.warn { background:var(--warning-light); color:var(--warning-dark); }
.ot-docs { display:flex; flex-wrap:wrap; gap:5px; margin-top:6px; }
.ot-doc { display:inline-flex; align-items:center; gap:5px; max-width:100%; font-size:.76rem; font-weight:600; color:var(--primary-dark); text-decoration:none; border:1px solid var(--border); border-radius:999px; padding:2px 9px; background:var(--card); }
.ot-doc span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:220px; }
.ot-doc:hover { border-color:var(--primary); }
.ot-compose { display:grid; gap:8px; border-top:1px solid var(--border); padding-top:10px; }
.ot-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.ot-row.end { justify-content:flex-end; }
.ot-row .ds-sel { width:auto; min-width:200px; }
.ot-pick { position:relative; display:inline-flex; align-items:center; gap:6px; cursor:pointer; }
.ot-pick input[type=file] { position:absolute; width:1px; height:1px; opacity:0; overflow:hidden; }
.ot-pick:focus-within { outline:2px solid var(--primary); outline-offset:2px; }
.ot-chosen { font-size:.74rem; color:var(--text-muted); overflow-wrap:anywhere; }
.ot-stage { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.84rem; margin-bottom:8px; }

/* ═══ THE HOLD SHEET (public/js/core/api.js holdSheet) ════════════════════════════════════════════════════════════
   A statutory refusal from any door: what stops the act, the rows behind it, and - for a hold the reader may release -
   the release. It sits ABOVE the drawer it was raised from (a permit raised in a drawer), so the overlay takes 1700
   where the shared modal takes 1500 and the drawer 1600. A new compound selector, not a second .modal-overlay rule. */
.modal-overlay.modal-over-drawer { z-index: 1700; }
.modal-dialog.st-hold { max-width: 540px; }
.st-hold-b { padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; }
.st-hold-rows { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.st-hold-row { display: block; padding: 10px 12px; color: var(--text); text-decoration: none; border-top: 1px solid var(--border); }
.st-hold-row:first-child { border-top: 0; }
.st-hold-row b { display: block; font-weight: 600; font-size: var(--text-sm, 13.5px); }
.st-hold-row small { display: block; color: var(--text-muted); font-size: 12px; margin-top: 2px; }
a.st-hold-row:hover, a.st-hold-row:focus-visible { background: var(--bg); outline: none; }
.st-hold-row.st-hold-more { color: var(--text-muted); font-size: 12px; }
.st-hold-rel { display: flex; flex-direction: column; gap: 6px; padding-top: 4px; }
.st-hold-relh { font-weight: 600; color: var(--text); }
.st-hold-hint { font-size: 12px; color: var(--text-muted); line-height: 1.45; }
.st-hold-err { font-size: 12.5px; color: var(--danger); }
.st-hold .modal-footer { flex-wrap: wrap; }

/* ============================================================================
   §33 · LIQUID GLASS REFRESH — the pane material, the pill topbar, and the lens's hook.
   §30/§30a/§30b stay GENERATED (do not hand-edit); every correction here follows §31's own rule —
   same specificity, later in the file, so it wins without touching the generated block.
   ============================================================================ */

/* ★ THE REFERENCE'S .top IS A PILL (border-radius:999px), NOT 16px — this app's topbar already
   floats clear of the window edge (margin on every side, same as the reference's own .top), so
   nothing about the layout has to change to carry it; only the corner does. --radius-pill is the
   SAME 999px token every other pill on this theme already reads (chips, the lg-ground switch). */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .topbar { border-radius: var(--radius-pill); }

/* ★ THE SHEEN — a top highlight plus a soft-light-style spot that follows the pointer on the four
   panes public/js/core/liquid-glass.js actually tracks (sidebar, topbar, the segmented controls/
   tabs, the three FABs below); every other pane keeps the SAME two layers at their default
   position (var(--mx,30%) var(--my,-20%) — the reference's own resting spot, top-left of centre),
   which is why this is ONE rule for all nine kinds of pane rather than a tracked and an untracked
   version. Declared as a second `background-image` only — a longhand, so it layers ON TOP of
   whatever `background` (fill colour) the pane's own rule already set, never replacing it. */
:is([data-theme="liquid"],[data-theme="liquid-dark"])
  :is(.card, .topbar, .sidebar, .modal-dialog, .ox-cardpop, .apx-modal, .ox-stat, .wl-fig, .mini) {
  background-image:
    radial-gradient(420px circle at var(--mx,30%) var(--my,-20%), var(--lg-sheen), transparent 45%),
    linear-gradient(180deg, var(--lg-sheen-top), transparent 32%);
}

/* ★ THE LENS'S HOOK — additive only. `var(--lensF,)` is an empty fallback: until
   public/js/core/liquid-glass.js sets --lensF on one of these elements (Chromium, Glass Edge
   Bending on), it contributes nothing and the filter chain is byte-for-byte what §30/§13 already
   declare. When it IS set, it is a `url(#…)` SVG displacement filter that goes FIRST in the chain,
   exactly as the reference's own `.gl.lens` composes it. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .topbar,
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .sidebar {
  -webkit-backdrop-filter: var(--lensF,) saturate(1.85) blur(19px);
  backdrop-filter: var(--lensF,) saturate(1.85) blur(19px);
}
/* `.lg-ground` (the light/dark pill switch) and `.ox-tabs2` (module tabs) carried no blur of
   their own — a segmented control is exactly the kind of small, few-in-number pane the lens is
   for, so it gets one here rather than waiting on a generated rule that never gave it one. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .lg-ground {
  -webkit-backdrop-filter: var(--lensF,) saturate(1.5) blur(10px);
  backdrop-filter: var(--lensF,) saturate(1.5) blur(10px);
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-tabs2 {
  -webkit-backdrop-filter: var(--lensF,) saturate(1.4) blur(8px);
  backdrop-filter: var(--lensF,) saturate(1.4) blur(8px);
}
/* The three FABs are shared with Glass (§13's own rule, further up) — redeclared here at the SAME
   specificity and later in the file so it wins, with the IDENTICAL blur/saturate numbers §13 already
   gives them; Glass never matches this selector and is untouched. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(#comms-fab, #help-fab, #urs-fab) {
  -webkit-backdrop-filter: var(--lensF,) blur(14px) saturate(1.4);
  backdrop-filter: var(--lensF,) blur(14px) saturate(1.4);
}

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   §34 · FLUID (13-10, Vijay: "the fluid transitions you didnot applied?") - the approved Liquid prototype's motion
   (scratchpad build/liquid-reference.html: .seg .blob, .card:hover), for Liquid Glass and PlantX. Everything here is
   a correction AFTER the generated §30 block, never an edit inside it.
   ════════════════════════════════════════════════════════════════════════════════════════════════ */
/* OVERRIDES §30's `.ox-head { padding: var(--space-5) var(--space-6) }` on Liquid: the lab's page head was a glass
   panel, so it was inset; here it carries no surface, and the 24px side inset left the title and the actions 24px
   inside the cards below - the design standard's "actions sit flush right" caught it the day an account on Liquid
   opened Vendors. */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-head { padding-left: 0; padding-right: 0; }

/* the light/dark switch's SLIDING PILL. partials/theme-resolve.ejs paintGround places .lg-blob under the pressed
   button and adds .has-blob; from then on the pill carries the fill and slides with a spring (and squishes as it is
   pressed), and the pressed button changes only its ink. OVERRIDES each theme's own `.lg-ground button.on` fill
   (§30's Liquid gradient, Liquid Dark's #0e7490 -> #3730a3 above, PlantX's accent) - the same fills move to the pill. */
.lg-ground { position: relative; }
.lg-ground > .lg-blob { display: none; }
.lg-ground.has-blob > .lg-blob {
  display: block; position: absolute; left: 0; top: var(--space-1); bottom: var(--space-1);
  border-radius: 999px; pointer-events: none; z-index: 0;
  transition: transform .55s cubic-bezier(.34,1.56,.64,1), width .55s cubic-bezier(.34,1.56,.64,1), scale .25s ease;
}
.lg-ground.has-blob > .lg-blob.squish { scale: 1.08 .86; }
.lg-ground.has-blob > button { position: relative; z-index: 1; transition: color .3s ease; }
html .lg-ground.has-blob > button.on { background: transparent; box-shadow: none; }
:is([data-theme="liquid"]) .lg-ground.has-blob > .lg-blob {
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  box-shadow: 0 3px 10px -2px color-mix(in srgb, var(--primary) 35%, transparent);
}
:is([data-theme="liquid-dark"]) .lg-ground.has-blob > .lg-blob {
  background: linear-gradient(135deg, #0e7490, #3730a3);
  box-shadow: 0 3px 10px -2px rgba(14,116,144,.45);
}
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .lg-ground.has-blob > .lg-blob {
  background: linear-gradient(135deg, var(--px-fill), var(--px-fill-2));
  box-shadow: inset 0 0 0 1px rgba(18,22,10,.10), 0 2px 8px var(--px-glow);
}

/* the ground itself changes as a circle opening from where the person pressed (theme-resolve.ejs fluid(): the View
   Transitions API animates the new ground's clip-path) - so the browser's own cross-fade is switched off for it */
:root.vt-reveal::view-transition-old(root),
:root.vt-reveal::view-transition-new(root) { animation: none; mix-blend-mode: normal; }

/* OVERRIDES `.md-card`'s .15s ease lift on these two themes with the prototype's: a softer, longer settle and 3px */
:is([data-theme="liquid"],[data-theme="liquid-dark"],[data-theme="plantx"],[data-theme="plantx-dark"]) .md-card {
  transition: transform .35s cubic-bezier(.2,.8,.2,1), box-shadow .35s ease, border-color .2s ease;
}
:is([data-theme="liquid"],[data-theme="liquid-dark"],[data-theme="plantx"],[data-theme="plantx-dark"]) .md-card:hover {
  transform: translateY(-3px);
}
@media (prefers-reduced-motion: reduce) {
  .lg-ground.has-blob > .lg-blob, .lg-ground.has-blob > button { transition: none; }
  :is([data-theme="liquid"],[data-theme="liquid-dark"],[data-theme="plantx"],[data-theme="plantx-dark"]) .md-card { transition: none; }
}

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   §35 · VIEW ONLY (14-10) - what views/layouts/main.ejs's view-only guard draws when a reader's grant on the page
   allows no act: the "View Only" mark at the top of a form, dialog or drawer it opened, and its locked fields. The
   tint is the theme's own "info" pair, so every theme draws it in its own words.
   ════════════════════════════════════════════════════════════════════════════════════════════════ */
.vo-mark {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-2) var(--space-3); margin: 0 0 var(--space-3);
  border-radius: var(--radius-sm); background: var(--tint-info-bg, var(--primary-light));
  color: var(--tint-info-fg, var(--primary-ink)); font-size: var(--fs-aux); line-height: 1.4;
}
.vo-mark b { font-weight: 700; }
.vo-mark-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; }
.vo-locked { cursor: default; }
.vo-locked:focus { box-shadow: none; }
/* a row's live clock (ui.liveClocks): its lamp and the job clock side by side (14-10) */
[data-run-since] { display: inline-flex; align-items: center; gap: 8px; }

/* ============================================================================
   §36 · LIQUID GLASS - THE NUMBER BOXES AS THE APPROVED PREVIEW (14-10, Vijay: "the bold numbers in box? the number
   colors, how beautiful there are in preview... why it cant achieve in real", then "ok go with A").
   The Liquid Glass Refresh prototype colours ONLY the figure - bright red, amber, green or blue, in Sora at 800 - on a
   light tint, and leaves the label grey. The port had kept this app's own boxes: the figure AND its label in a dark
   shade of the tint (#7f1616 on red), Inter at 600, a 17% tint and the theme's 26px corner. Measured on the real Home.
   ★ OVERRIDES on purpose, Liquid only: .md-cube (Home's module and register boxes, §tiles above) and .ox-stat (every
   module page's summary boxes). The label stays --text-muted, NOT the prototype's lighter #5a6782 - that grey measured
   under the readable floor on real glass (§30's own note on --text-muted). Every other theme is untouched.
   ============================================================================ */
[data-theme="liquid"] {
  --lg-n-size: 24px;   /* the prototype's own box figure */
  --lg-fig-font: 'Sora', 'Manrope', var(--font-display);
  --lg-n-ink: #0b1424; --lg-n-zero: #97a3b9;
  --lg-n-bad: #e0263c;  --lg-n-bad-bg: rgba(224,38,60,.13);
  --lg-n-warn: #c26a05; --lg-n-warn-bg: rgba(245,158,11,.17);
  --lg-n-ok: #0b8f63;   --lg-n-ok-bg: rgba(16,185,129,.15);
  --lg-n-acc: #1a4fd6;  --lg-n-acc-bg: rgba(47,107,255,.14);
  --lg-n-chip: rgba(255,255,255,.42); --lg-n-rim: rgba(255,255,255,.95);
}
[data-theme="liquid-dark"] {
  --lg-n-size: 24px;   /* the prototype's own box figure */
  --lg-fig-font: 'Sora', 'Manrope', var(--font-display);
  --lg-n-ink: #eef3ff; --lg-n-zero: #5c6984;
  --lg-n-bad: #ff6b7d;  --lg-n-bad-bg: rgba(255,107,125,.15);
  --lg-n-warn: #ffc15a; --lg-n-warn-bg: rgba(255,193,90,.14);
  --lg-n-ok: #43e0a6;   --lg-n-ok-bg: rgba(67,224,166,.13);
  --lg-n-acc: #9dc0ff;  --lg-n-acc-bg: rgba(106,161,255,.16);
  --lg-n-chip: rgba(255,255,255,.07); --lg-n-rim: rgba(255,255,255,.30);
}
/* the box: a light chip with the prototype's rim of light along its top, and its own 15px corner */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .md-cube {
  background: var(--lg-n-chip); box-shadow: inset 0 1px 0 var(--lg-n-rim); border-radius: 15px; color: var(--lg-n-ink);
}
/* the figure: Sora at 800, the box's colour; the label: the theme's muted ink, never the tone's */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-cube .n, .ox-stat b) {
  font-family: var(--lg-fig-font); font-weight: 800; letter-spacing: -.01em; color: var(--lg-n-ink); opacity: 1;
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .md-cube .n { font-size: var(--lg-n-size); line-height: 1.1; }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-cube .l, .ox-stat small) { color: var(--text-muted); opacity: 1; }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .md-cube.bad { background: var(--lg-n-bad-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .md-cube.warn { background: var(--lg-n-warn-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .md-cube.ok { background: var(--lg-n-ok-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-cube.info, .md-cube.plain) { background: var(--lg-n-acc-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat.tint-bad { --stat-tint: var(--lg-n-bad-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat.tint-warn { --stat-tint: var(--lg-n-warn-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat.tint-ok { --stat-tint: var(--lg-n-ok-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-stat.tint-info { --stat-tint: var(--lg-n-acc-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-cube.bad .n, .ox-stat.tint-bad b) { color: var(--lg-n-bad); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-cube.warn .n, .ox-stat.tint-warn b) { color: var(--lg-n-warn); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-cube.ok .n, .ox-stat.tint-ok b) { color: var(--lg-n-ok); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-cube.info .n, .md-cube.plain .n, .ox-stat.tint-info b) { color: var(--lg-n-acc); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .md-cube.zero .n { color: var(--lg-n-zero); }
/* the portfolio's figure boxes (public/css/plan.css .pf-fig) take the same figure and tones (15-10) */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .pf-fig b { font-family: var(--lg-fig-font); color: var(--lg-n-ink); letter-spacing: -.01em; }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .pf-fig.bad { background: var(--lg-n-bad-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .pf-fig.warn { background: var(--lg-n-warn-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .pf-fig.ok { background: var(--lg-n-ok-bg); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .pf-fig.bad b { color: var(--lg-n-bad); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .pf-fig.warn b { color: var(--lg-n-warn); }
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .pf-fig.ok b { color: var(--lg-n-ok); }

/* ============================================================================
   §37 · THE APPEARANCE AXES ON EVERY SKIN (14-10, Vijay: "the font contrast buttons in appearance does work for all
   skins or not?"). Measured on Home, every skin x every setting: Text Size and Typeface worked on all eighteen; these
   three did not. ★ Each fix is (0,2,0) - [data-theme][data-axis] - and sits after every theme block, because a theme's
   own (0,1,0) token block later in the file is exactly what switched the axis off. _e2e_appearance_axes.js holds it.
   ============================================================================ */
/* 1 · Glass Dark and Liquid Dark took the LIGHT grounds' contrast values (OVERRIDES the shared
   :is(glass, glass-dark, liquid, liquid-dark)[data-contrast] token rules above and the rail's "3 · High contrast"): High and
   Extra High painted #0a0d14 and #000000 text on a dark pane - unreadable. On a dark ground more contrast is BRIGHTER ink. */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"])[data-contrast="high"] {
  --glass-brd: rgba(255,255,255,.22);
  --text: #f5f7fb; --text-secondary: #e2e7ef; --text-muted: #c4ccd8;
  --border: rgba(255,255,255,.22); --border-strong: rgba(255,255,255,.34);
  --rail-seam: rgba(255,255,255,.22); --rail-ink: #f5f7fb; --rail-ink-dim: #e2e7ef; --rail-ico: #c4ccd8;
}
:is([data-theme="glass-dark"],[data-theme="liquid-dark"])[data-contrast="xhigh"] {
  --glass-strong: #0b0d14; --glass-brd: rgba(255,255,255,.40);
  --text: #ffffff; --text-secondary: #eef1f6; --text-muted: #dbe1ea;
  --border: rgba(255,255,255,.40); --border-strong: rgba(255,255,255,.55);
  --rail-a: #0b0d14; --rail-b: #0b0d14; --rail-hover: #1a1f2b; --rail-seam: rgba(255,255,255,.40);
  --rail-ink: #ffffff; --rail-ink-dim: #eef1f6; --rail-ico: #dbe1ea;
}
/* 2 · PlantX and PlantX Dark had no contrast settings at all - High did nothing, Extra High only thickened the type. */
[data-theme="plantx"][data-contrast="high"] { --text: #05080c; --text-secondary: #1f2733; --text-muted: #3f4a58; --border: #c9cfd8; }
[data-theme="plantx"][data-contrast="xhigh"] { --text: #000000; --text-secondary: #111827; --text-muted: #1f2937; --border: #8a94a3; }
[data-theme="plantx-dark"][data-contrast="high"] { --text: #f7f9fc; --text-secondary: #e3e8ef; --text-muted: #b4bfcd; --border: #33404f; }
[data-theme="plantx-dark"][data-contrast="xhigh"] { --text: #ffffff; --text-secondary: #eef1f6; --text-muted: #d3dbe5; --border: #4a5869; }
/* 2b · HIGH NEVER LOWERS. Flat's own text is already #09090b, so the shared High (#0b1220) made its main text a shade LIGHTER - High never lowers. */
[data-theme="flat"][data-contrast="high"] { --text: #000000; }
/* the same on two more light skins, caught by the suite: White's own muted grey (#525252) and Glass's (#3a3f4a) are already
   darker than the shared High muted ink, so High made the muted text a shade lighter */
[data-theme="white"][data-contrast="high"] { --text-muted: #3f3f46; }
[data-theme="glass"][data-contrast="high"] { --text-muted: #2f343e; }
/* 3 · Text: Pure Black did nothing on nine skins (their own --text, later in the file than the [data-text="black"] block). On a light skin
   it is pure black; on a dark skin pure white - the rule the Black skin already follows. */
:is([data-theme="soft"],[data-theme="console-day"],[data-theme="glass"],[data-theme="liquid"],[data-theme="plantx"])[data-text="black"] {
  --text: #000000; --text-secondary: #1a1a1a; --text-muted: #3f3f46;
}
:is([data-theme="console"],[data-theme="glass-dark"],[data-theme="liquid-dark"],[data-theme="plantx-dark"])[data-text="black"] {
  --text: #ffffff; --text-secondary: #ededed; --text-muted: #c9c9d1;
}

/* ============================================================================
   §38 · LIQUID GLASS'S ACCENT FOLLOWS ITS SKY (15-10, Vijay: "you still hard coded green color to liquid glass which is
   not changing as per skin colors"). Liquid's accent was ONE teal (#0e7490 into indigo #3730a3) under every Sky preset -
   the department chip, the selected tab, the buttons, "Now" on a plan, the selected menu item - written as tokens in
   some places and as literals in others. Now each Sky carries its own accent family and every one of those reads it:
     --lg-acc      the solid under white ink          --lg-acc-2   the gradient's far end
     --lg-acc-ink  the accent as ink on light glass   --lg-acc-deep  the deepest ink
     --lg-acc-hi   the accent as ink on dark glass    --lg-acc-hi-2  brighter still
   Measured per Sky (python, against each Sky's own ground): white on --lg-acc 5.4-7.6:1, the inks 5.4-12.6:1 on the light
   ground, the bright inks 6.7-13.7:1 on the dark one. The default Sky is the approved prototype's BLUE.
   ★ OVERRIDES on purpose (§28-§31 and §30's generated block are left as they are; this section comes after them): the
   Liquid --primary family, --accent-*, --rail-active, and the literal teal on .ox-scope, the dark avatar / today / primary
   button / current-page chip, an open section's hover, and the dark switch's pill.
   ============================================================================ */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) {
  --lg-acc: #1d4ed8; --lg-acc-2: #4338ca; --lg-acc-ink: #1d4ed8; --lg-acc-deep: #1e3a8a; --lg-acc-hi: #7fa8ff; --lg-acc-hi-2: #a9c4ff;
}
:is([data-theme="liquid"],[data-theme="liquid-dark"])[data-sky="brand"] {
  --lg-acc: #be123c; --lg-acc-2: #a16207; --lg-acc-ink: #9f1239; --lg-acc-deep: #881337; --lg-acc-hi: #fb7185; --lg-acc-hi-2: #fda4af;
}
:is([data-theme="liquid"],[data-theme="liquid-dark"])[data-sky="steel"] {
  --lg-acc: #3d5a80; --lg-acc-2: #334155; --lg-acc-ink: #34507a; --lg-acc-deep: #1f3557; --lg-acc-hi: #9fb9dc; --lg-acc-hi-2: #c4d5ec;
}
:is([data-theme="liquid"],[data-theme="liquid-dark"])[data-sky="aurora"] {
  --lg-acc: #0e7490; --lg-acc-2: #3730a3; --lg-acc-ink: #0b5a6e; --lg-acc-deep: #083d4a; --lg-acc-hi: #22d3ee; --lg-acc-hi-2: #67e8f9;
}
:is([data-theme="liquid"],[data-theme="liquid-dark"])[data-sky="calm"] {
  --lg-acc: #475569; --lg-acc-2: #334155; --lg-acc-ink: #334155; --lg-acc-deep: #1e293b; --lg-acc-hi: #cbd5e1; --lg-acc-hi-2: #e2e8f0;
}
/* the family, read: light ground */
[data-theme="liquid"] {
  --primary: var(--lg-acc); --primary-dark: var(--lg-acc-2); --primary-light: color-mix(in srgb, var(--lg-acc) 12%, transparent);
  --accent-solid: var(--lg-acc); --accent-ink: var(--lg-acc-ink); --accent-ink-deep: var(--lg-acc-deep);
  --rail-active: color-mix(in srgb, var(--lg-acc) 16%, transparent);
  --lg-n-acc: var(--lg-acc-ink); --lg-n-acc-bg: color-mix(in srgb, var(--lg-acc) 14%, transparent);
}
/* dark ground: the ink is the bright end, a fill under white ink stays the solid */
[data-theme="liquid-dark"] {
  --primary: var(--lg-acc-hi); --primary-dark: var(--lg-acc-hi-2); --primary-light: color-mix(in srgb, var(--lg-acc) 32%, transparent);
  --accent-solid: var(--lg-acc); --accent-ink: var(--lg-acc-hi-2);
  --rail-active: color-mix(in srgb, var(--lg-acc-hi) 20%, transparent);
  --lg-n-acc: var(--lg-acc-hi); --lg-n-acc-bg: color-mix(in srgb, var(--lg-acc-hi) 16%, transparent);
}
/* the literals, put onto the family */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) .ox-scope {
  background: color-mix(in srgb, var(--lg-acc) 12%, transparent); border-color: color-mix(in srgb, var(--lg-acc) 28%, transparent);
}
:is([data-theme="liquid-dark"]) :is(.cacct .cav, .cal-num.on, .apx-seg > button.on, .cal-cell.today .cal-num, .ox-page) {
  background: var(--lg-acc); color: #fff;
}
:is([data-theme="liquid-dark"]) :is(.btn-primary, .ox-hero, .ds-btn, .lg-ground button.on) {
  background: linear-gradient(135deg, var(--lg-acc), var(--lg-acc-2)); color: #fff;
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) #td-body .td-sec[open] > .td-sum:hover {
  background-color: color-mix(in srgb, var(--lg-acc) 82%, #000);
}
:is([data-theme="liquid-dark"]) .lg-ground.has-blob > .lg-blob {
  background: linear-gradient(135deg, var(--lg-acc), var(--lg-acc-2));
  box-shadow: 0 3px 10px -2px color-mix(in srgb, var(--lg-acc) 45%, transparent);
}

/* ============================================================================
   §39 · LIQUID GLASS - WHAT SCROLLS UNDER A STICKY HEADER STAYS UNDER IT (15-10, Vijay: "after scroll what is the menu
   data and background page merging the view?"). A plan's Gantt keeps its header row and its activity column on screen
   while the rows and bars scroll (.sq-head / .sq-gh top, .sq-gb left); on Liquid those were the translucent card (38%
   white), so the rows and bars showed straight through the column titles and the dates. They take a SOLID
   ground in the pane's own tint (--lg-solid). The plan's drawer head, look-ahead head and matrix header likewise.
   ============================================================================ */
[data-theme="liquid"] { --lg-solid: #f6f9fe; }
[data-theme="liquid-dark"] { --lg-solid: #121726; }
/* SOLID, not frosted: at the table header's 93% a coloured bar scrolling under the activity column still showed as a ghost */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.sq-head, .sq-gh, .sq-gb, .pp-drh, .la-h, .ph-mx th) {
  background: var(--lg-solid);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}

/* ============================================================================
   §40 · LIQUID GLASS - A BOX ANSWERS THE POINTER ON BOTH GROUNDS (15-10, Vijay: "in dark hover effect is not at all there
   ... hovering on boxes"). Measured: a Liquid card's hover changed nothing at all (the theme's own card shadow is later and
   as specific as `.card:hover`), and on the dark ground a lift alone, with a shadow already black, reads as nothing. A
   box now lifts, its rim brightens and its glass lightens a step - light: a deeper shadow; dark: a glow of the accent.
   ============================================================================ */
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.card, .md-card, .ox-stat, .pf-fig) {
  transition: transform .35s cubic-bezier(.2,.8,.2,1), box-shadow .35s ease, background-color .25s ease, border-color .25s ease;
}
[data-theme="liquid"] :is(.card, .md-card, .ox-stat, .pf-fig):hover {
  border-color: rgba(255,255,255,.95);
  box-shadow: 0 28px 60px -22px rgba(15,23,42,.38), 0 3px 10px rgba(15,23,42,.10), inset 0 1px 0 rgba(255,255,255,.95);
}
[data-theme="liquid-dark"] :is(.card, .md-card, .ox-stat, .pf-fig):hover {
  background-color: rgba(34,44,72,.55); border-color: rgba(255,255,255,.26);
  box-shadow: 0 24px 60px -18px rgba(0,0,0,.70), 0 0 0 1px rgba(255,255,255,.10), 0 0 28px -6px color-mix(in srgb, var(--lg-acc-hi) 40%, transparent),
              inset 0 1px 0 rgba(255,255,255,.28);
}
:is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-card, .ox-stat, .pf-fig):hover { transform: translateY(-3px); }
@media (prefers-reduced-motion: reduce) {
  :is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.card, .md-card, .ox-stat, .pf-fig) { transition: none; }
  :is([data-theme="liquid"],[data-theme="liquid-dark"]) :is(.md-card, .ox-stat, .pf-fig):hover { transform: none; }
}

/* ============================================================================
   §41 · THE SPACE AFTER A WORK ORDER'S BANNER (15-10, Vijay: "why there is not gap after the announcement box?"). The
   banner carried a top margin only, so the two columns of cards sat flush against its bottom edge on every skin.
   ============================================================================ */
#wod-body > .wod-banner { margin-bottom: var(--space-4); }

/* ============================================================================
   §42 · FROSTED (15-10, Vijay: "create a fronsted skin.. let me see how does it look like", then "ok build" on the
   Prism frosted-glass pastel reference, superdesign.dev). White frosted glass on a pastel canvas - peach on the left,
   white in the middle, sky on the right, a touch of mint low down (the colours read off the reference's own image) -
   ink words, slate for the quieter ones, and the act a dark pill with white words; everything else pressed a white
   glass pill. The figures keep their meaning as a pastel: rose needs you, peach is soon, mint is fine, sky is a count.
   ★ DRAWN OVER LIQUID GLASS, NOT COPIED FROM IT. <html> carries data-theme="liquid" data-skin="frosted" (core/appearance.js
   OVER), so every Liquid rule - the panes, the rail, the pills, the type scale - is Frosted's too, and this section only
   lays its own tokens and a few rules on top at ONE MORE attribute of weight ([data-theme][data-skin], (0,2,0) for the
   tokens). ★ OVERRIDES on purpose, Frosted only: Liquid's light token block, §36's number-box tokens, §38's accent family,
   §39's solid, §40's hover shadow, the sky (body::before) and the act's look. Liquid itself is untouched (no rule here
   matches without data-skin). ★ TWO GROUNDS (15-10, "dark view? and switcher?"): the top bar's light/dark switch works on
   Frosted exactly as on Liquid - per screen (partials/theme-resolve.ejs resolves a skin as its machinery does), so the dark
   ground is data-theme="liquid-dark" data-skin="frosted" and has its own block at the end of this section.
   ============================================================================ */
[data-theme="liquid"][data-skin="frosted"] {
  /* the glass: white film, a white hairline, a soft cool shadow, a bright lip */
  --card: rgba(255,255,255,.58); --glass-bg: rgba(255,255,255,.58); --glass-strong: rgba(255,255,255,.74);
  --gl-f1: rgba(255,255,255,.34); --gl-f2: rgba(255,255,255,.44); --gl-f3: rgba(255,255,255,.52); --gl-f4: rgba(255,255,255,.60);
  --gl-f5: rgba(255,255,255,.72); --gl-f6: rgba(255,255,255,.86); --gl-f7: rgba(255,255,255,.96);
  --th-bg: rgba(255,255,255,.50); --tint-neutral-bg: rgba(255,255,255,.55);
  --border: rgba(255,255,255,.78); --border-strong: rgba(255,255,255,.95); --glass-brd: rgba(255,255,255,.85);
  --gl-lip: rgba(255,255,255,.95); --gl-lip-side: rgba(255,255,255,.50);
  --glass-blur: saturate(1.4) blur(24px);
  --glass-shadow: 0 10px 34px -14px rgba(40,50,90,.16), inset 0 1px 0 rgba(255,255,255,.95);
  --el-1: 0 2px 10px -4px rgba(40,50,90,.10); --el-2: 0 10px 34px -14px rgba(40,50,90,.16);
  --el-3: 0 10px 34px -14px rgba(40,50,90,.16); --el-4: 0 24px 60px -24px rgba(40,50,90,.22);
  /* the words: ink and slate */
  --text: #15161b; --text-strong: #15161b; --text-secondary: #3c4150; --text-muted: #5b6172; --text-hint: #656b7a;
  /* the accent IS the ink - a dark pill, a dark underline under the chosen tab */
  --lg-acc: #15161b; --lg-acc-2: #2c2e3b; --lg-acc-ink: #15161b; --lg-acc-deep: #000000;
  --primary: #15161b; --primary-dark: #2c2e3b; --primary-ink: #15161b; --primary-light: rgba(21,22,27,.07);
  --accent-solid: #15161b; --accent-ink: #15161b; --accent-ink-deep: #000000;
  /* the side menu: the same white glass, ink words, the chosen item a white pill */
  --sidebar-bg: rgba(255,255,255,.55); --sidebar-hover: rgba(255,255,255,.80);
  --rail-ink: #15161b; --rail-ink-dim: #3c4150; --rail-ico: #5b6172;
  --rail-active: #ffffff; --rail-group-bg: transparent; --rail-group-brd: transparent;
  /* the canvas under it all, and what a sticky header stands on (§39) */
  --lg-sky-1: #fbfcff; --lg-sky-2: #fbfcff; --lg-solid: #f7f8fc;
  /* the figures (§36): ink, its meaning as a pastel */
  --lg-n-ink: #15161b; --lg-n-zero: #8a90a0;
  --lg-n-bad: #b42328;  --lg-n-bad-bg: rgba(255,207,207,.78);
  --lg-n-warn: #a8481a; --lg-n-warn-bg: rgba(255,216,195,.80);
  --lg-n-ok: #1d7a4f;   --lg-n-ok-bg: rgba(211,245,227,.85);
  --lg-n-acc: #244f92;  --lg-n-acc-bg: rgba(207,227,255,.80);
  --lg-n-chip: rgba(255,255,255,.62); --lg-n-rim: #ffffff;
  /* its tile in Appearance */
  --swatch-bg: linear-gradient(110deg, #fdd2bb 0%, #fbfcff 52%, #c3d9ff 100%); --swatch-card: rgba(255,255,255,.85);
}
/* the canvas: still, no drift - four soft pastel lights on near-white */
[data-theme="liquid"][data-skin="frosted"] body::before {
  background-image:
    radial-gradient(closest-side, #fdd2bb 0%, rgba(253,210,187,.55) 45%, rgba(253,210,187,0) 100%),
    radial-gradient(closest-side, #c3d9ff 0%, rgba(195,217,255,.55) 45%, rgba(195,217,255,0) 100%),
    radial-gradient(closest-side, #dfeaff 0%, rgba(223,234,255,0) 100%),
    radial-gradient(closest-side, #d3f3e2 0%, rgba(211,243,226,0) 100%),
    linear-gradient(var(--lg-sky-1), var(--lg-sky-2));
  background-repeat: no-repeat;
  background-size: 62vw 110vh, 60vw 100vh, 50vw 60vh, 46vw 60vh, 100% 100%;
  background-position: -18vw 8vh, 62vw 22vh, 70vw -20vh, 18vw 70vh, 0 0;
  animation: none;
}
/* the act: a dark pill, white words */
[data-theme="liquid"][data-skin="frosted"] :is(.btn-primary, .ox-hero, .ds-btn) {
  background: #15161b; color: #ffffff; border-color: transparent; border-radius: var(--radius-pill);
  box-shadow: 0 10px 22px -10px rgba(21,22,27,.55);
}
[data-theme="liquid"][data-skin="frosted"] :is(.btn-primary, .ox-hero, .ds-btn):hover {
  background: #000000; box-shadow: 0 12px 26px -10px rgba(21,22,27,.65);
}
/* everything else pressed: a white glass pill */
[data-theme="liquid"][data-skin="frosted"] :is(.ox-chip, .btn-secondary, .ghost-btn) {
  background: rgba(255,255,255,.72); border: 1px solid rgba(255,255,255,.95); color: #15161b;
  border-radius: var(--radius-pill); box-shadow: 0 6px 18px -10px rgba(40,50,90,.22);
}
[data-theme="liquid"][data-skin="frosted"] :is(.ox-chip, .btn-secondary, .ghost-btn):hover { background: #ffffff; }
/* the chosen side-menu item: a white pill */
[data-theme="liquid"][data-skin="frosted"] .sidebar .nav-item.active {
  background: #ffffff; color: #15161b; box-shadow: 0 6px 18px -10px rgba(40,50,90,.25);
}
/* a box: a white chip with a white hairline (its pastel comes from §36's tokens above) */
[data-theme="liquid"][data-skin="frosted"] :is(.md-cube, .ox-stat) {
  border: 1px solid rgba(255,255,255,.95); box-shadow: 0 6px 18px -12px rgba(40,50,90,.18);
}
[data-theme="liquid"][data-skin="frosted"] .md-ic { background: rgba(207,227,255,.80); color: #244f92; }
/* the top bar: its shadow soft, or it lays a grey band over the page below it */
[data-theme="liquid"][data-skin="frosted"] .topbar { box-shadow: 0 8px 24px -16px rgba(40,50,90,.18), inset 0 1px 0 #ffffff; }
/* §40's hover on Frosted: the lift, a softer cool shadow */
[data-theme="liquid"][data-skin="frosted"] :is(.card, .md-card, .ox-stat, .pf-fig):hover {
  border-color: #ffffff;
  box-shadow: 0 22px 48px -24px rgba(40,50,90,.28), 0 2px 8px rgba(40,50,90,.06), inset 0 1px 0 #ffffff;
}
/* the axes (§37) at one more attribute, or this section's own tokens would switch them off */
[data-theme="liquid"][data-skin="frosted"][data-contrast="high"] {
  --text: #05060a; --text-secondary: #1f2430; --text-muted: #353b48; --text-hint: #3f4553;
  --border: rgba(21,22,27,.20); --border-strong: rgba(21,22,27,.32); --rail-ink-dim: #1f2430; --rail-ico: #353b48;
}
[data-theme="liquid"][data-skin="frosted"][data-contrast="xhigh"] {
  --text: #000000; --text-secondary: #111827; --text-muted: #1f2937; --text-hint: #273140;
  --border: rgba(21,22,27,.40); --border-strong: rgba(21,22,27,.55); --rail-ink: #000000; --rail-ink-dim: #111827; --rail-ico: #1f2937;
}
[data-theme="liquid"][data-skin="frosted"][data-text="black"] {
  --text: #000000; --text-secondary: #1a1a1a; --text-muted: #3f3f46;
}
/* the wash at the top of every page is drawn in --primary - on Frosted that is the ink, and it laid a grey smear under the
   title. Frosted's canvas is its own wash, so the page takes none (OVERRIDES .content-area's background-image, Frosted only) */
:is([data-theme="liquid"],[data-theme="liquid-dark"])[data-skin="frosted"] .content-area { background-image: none; }

/* ── FROSTED, DARK GROUND - the same pastel lights turned down on ink, smoked glass, white words, and the act turned over: a
   WHITE pill with ink words. Fills under white words (an avatar, the chosen day, a segmented button) take a muted slate. */
[data-theme="liquid-dark"][data-skin="frosted"] {
  --card: rgba(30,32,44,.55); --glass-bg: rgba(30,32,44,.55); --glass-strong: rgba(30,32,44,.72);
  --gl-f1: rgba(255,255,255,.04); --gl-f2: rgba(255,255,255,.06); --gl-f3: rgba(255,255,255,.08); --gl-f4: rgba(30,32,44,.55);
  --gl-f5: rgba(255,255,255,.12); --gl-f6: rgba(255,255,255,.18); --gl-f7: rgba(22,24,34,.96);
  --th-bg: rgba(255,255,255,.05); --tint-neutral-bg: rgba(255,255,255,.07);
  --border: rgba(255,255,255,.12); --border-strong: rgba(255,255,255,.20); --glass-brd: rgba(255,255,255,.14);
  --gl-lip: rgba(255,255,255,.18); --gl-lip-side: rgba(255,255,255,.06);
  --glass-blur: saturate(1.3) blur(24px);
  --glass-shadow: 0 14px 40px -16px rgba(0,0,0,.60), inset 0 1px 0 rgba(255,255,255,.12);
  --el-1: 0 2px 10px -4px rgba(0,0,0,.40); --el-2: 0 14px 40px -16px rgba(0,0,0,.60);
  --el-3: 0 14px 40px -16px rgba(0,0,0,.60); --el-4: 0 28px 70px -24px rgba(0,0,0,.72);
  --text: #eef0f6; --text-strong: #ffffff; --text-secondary: #d5d9e3; --text-muted: #a9b0c0; --text-hint: #9aa1b2;
  --lg-acc: #4a5068; --lg-acc-2: #33374a; --lg-acc-ink: #ffffff; --lg-acc-deep: #ffffff; --lg-acc-hi: #ffffff; --lg-acc-hi-2: #ffffff;
  --primary: #f3f4f8; --primary-dark: #ffffff; --primary-ink: #ffffff; --primary-light: rgba(255,255,255,.10);
  --accent-solid: #4a5068; --accent-ink: #ffffff; --accent-ink-deep: #ffffff;
  --sidebar-bg: rgba(22,24,34,.55); --sidebar-hover: rgba(255,255,255,.08);
  --rail-ink: #eef0f6; --rail-ink-dim: #cfd4de; --rail-ico: #a9b0c0;
  --rail-active: rgba(255,255,255,.14); --rail-group-bg: transparent; --rail-group-brd: transparent;
  --lg-sky-1: #0d0f17; --lg-sky-2: #0d0f17; --lg-solid: #151824;
  --lg-n-ink: #f3f4f8; --lg-n-zero: #6b7286;
  --lg-n-bad: #ff9a9e;  --lg-n-bad-bg: rgba(255,120,130,.16);
  --lg-n-warn: #ffbe92; --lg-n-warn-bg: rgba(255,170,120,.15);
  --lg-n-ok: #7fe0b0;   --lg-n-ok-bg: rgba(110,220,160,.14);
  --lg-n-acc: #a9c8ff;  --lg-n-acc-bg: rgba(140,180,255,.16);
  --lg-n-chip: rgba(255,255,255,.06); --lg-n-rim: rgba(255,255,255,.16);
  --swatch-bg: linear-gradient(110deg, #3a2a2a 0%, #0d0f17 52%, #1c2a44 100%); --swatch-card: rgba(30,32,44,.85);
}
[data-theme="liquid-dark"][data-skin="frosted"] body::before {
  background-image:
    radial-gradient(closest-side, rgba(253,170,130,.22) 0%, rgba(253,170,130,.10) 45%, rgba(253,170,130,0) 100%),
    radial-gradient(closest-side, rgba(120,160,255,.24) 0%, rgba(120,160,255,.11) 45%, rgba(120,160,255,0) 100%),
    radial-gradient(closest-side, rgba(150,170,255,.12) 0%, rgba(150,170,255,0) 100%),
    radial-gradient(closest-side, rgba(110,220,170,.12) 0%, rgba(110,220,170,0) 100%),
    linear-gradient(var(--lg-sky-1), var(--lg-sky-2));
  background-repeat: no-repeat;
  background-size: 62vw 110vh, 60vw 100vh, 50vw 60vh, 46vw 60vh, 100% 100%;
  background-position: -18vw 8vh, 62vw 22vh, 70vw -20vh, 18vw 70vh, 0 0;
  animation: none;
}
[data-theme="liquid-dark"][data-skin="frosted"] :is(.btn-primary, .ox-hero, .ds-btn) {
  background: #f3f4f8; color: #15161b; border-color: transparent; border-radius: var(--radius-pill);
  box-shadow: 0 10px 24px -12px rgba(0,0,0,.7);
}
[data-theme="liquid-dark"][data-skin="frosted"] :is(.btn-primary, .ox-hero, .ds-btn):hover { background: #ffffff; }
[data-theme="liquid-dark"][data-skin="frosted"] :is(.ox-chip, .btn-secondary, .ghost-btn) {
  background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.14); color: #eef0f6;
  border-radius: var(--radius-pill); box-shadow: none;
}
[data-theme="liquid-dark"][data-skin="frosted"] :is(.ox-chip, .btn-secondary, .ghost-btn):hover { background: rgba(255,255,255,.14); }
[data-theme="liquid-dark"][data-skin="frosted"] .sidebar .nav-item.active { background: rgba(255,255,255,.14); color: #ffffff; box-shadow: none; }
[data-theme="liquid-dark"][data-skin="frosted"] :is(.md-cube, .ox-stat) { border: 1px solid rgba(255,255,255,.10); box-shadow: none; }
[data-theme="liquid-dark"][data-skin="frosted"] .md-ic { background: rgba(140,180,255,.16); color: #a9c8ff; }
[data-theme="liquid-dark"][data-skin="frosted"] .topbar { box-shadow: 0 10px 30px -16px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.10); }
[data-theme="liquid-dark"][data-skin="frosted"] :is(.card, .md-card, .ox-stat, .pf-fig):hover {
  background-color: rgba(40,44,60,.62); border-color: rgba(255,255,255,.22);
  box-shadow: 0 22px 48px -20px rgba(0,0,0,.70), inset 0 1px 0 rgba(255,255,255,.16);
}
[data-theme="liquid-dark"][data-skin="frosted"][data-contrast="high"] {
  --text: #f5f7fb; --text-secondary: #e2e7ef; --text-muted: #c4ccd8; --text-hint: #b8c0cd;
  --border: rgba(255,255,255,.22); --border-strong: rgba(255,255,255,.34); --rail-ink: #f5f7fb; --rail-ink-dim: #e2e7ef; --rail-ico: #c4ccd8;
}
[data-theme="liquid-dark"][data-skin="frosted"][data-contrast="xhigh"] {
  --text: #ffffff; --text-secondary: #eef1f6; --text-muted: #dbe1ea; --text-hint: #cfd6e0;
  --border: rgba(255,255,255,.40); --border-strong: rgba(255,255,255,.55); --rail-ink: #ffffff; --rail-ink-dim: #eef1f6; --rail-ico: #dbe1ea;
}
[data-theme="liquid-dark"][data-skin="frosted"][data-text="black"] {
  --text: #ffffff; --text-secondary: #ededed; --text-muted: #c9c9d1;
}

/* ============================================================================
   §43 · THE HOME BOARD (15-10, core/home-board.js; Vijay on the Komari Glassmorphism dashboard - "a role's point of view
   with respect to module settings, tasks etc"). Summary tiles, then a card per unit or machine of the seat's scope with its
   last 24 hours. Every surface is a .card, so each skin draws it as it draws its own cards (glass, solid, lime); only the
   layout is here. A LEVEL is the plant's standard colour on every skin (--level-ok / --level-warn / --level-alarm).
   ============================================================================ */
.hb { margin: 18px 0 6px; }
.hb-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: 12px; }
.hb-tile { padding: 14px 16px 15px; display: flex; flex-direction: column; gap: 14px; min-width: 0; margin: 0; }
.hb-th { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; font-size: .8rem; font-weight: 500; color: var(--text-muted); }
.hb-th > span:last-child { opacity: .6; }
.hb-tv { font-family: var(--font-display); font-size: 1.75rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.05; color: var(--text); }
.hb-tv small { font-family: var(--font-ui); font-size: .8rem; font-weight: 600; letter-spacing: 0; color: var(--text-muted); }
.hb-tn { font-size: .72rem; color: var(--text-muted); margin-top: 5px; line-height: 1.3; }
.hb-tile.is-bad .hb-tv { color: var(--danger); }
.hb-tile.is-warn .hb-tv { color: var(--warning-dark); }
.hb-bar { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 22px 0 10px; }
.hb-bar h3 { margin: 0; font-size: 1rem; display: flex; align-items: center; gap: 7px; }
.hb-meta { font-size: .78rem; color: var(--text-muted); }
.hb-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.hb-card { display: block; padding: 14px 16px 14px; margin: 0; text-decoration: none; color: inherit; min-width: 0; }
.hb-ct { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: .92rem; color: var(--text); }
.hb-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; background: var(--level-ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--level-ok) 20%, transparent); }
.hb-card.is-warn .hb-dot { background: var(--level-warn); box-shadow: 0 0 0 3px color-mix(in srgb, var(--level-warn) 22%, transparent); }
.hb-card.is-bad .hb-dot { background: var(--level-alarm); box-shadow: 0 0 0 3px color-mix(in srgb, var(--level-alarm) 22%, transparent); }
.hb-nm { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb-crit { flex: 0 0 auto; font-size: .68rem; font-weight: 700; padding: 2px 8px; border-radius: 999px; color: var(--danger-dark);
  border: 1px solid color-mix(in srgb, var(--level-alarm) 40%, transparent); }
.hb-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 11px; }
.hb-chips span, .hb-tags span { font-size: .7rem; font-weight: 600; padding: 2px 9px; border-radius: 999px; color: var(--text-muted);
  background: color-mix(in srgb, var(--text) 5%, transparent); border: 1px solid var(--border); }
.hb-card.is-bad .hb-chips span:first-child { color: var(--danger-dark); }
.hb-card.is-warn .hb-chips span:first-child { color: var(--warning-dark); }
.hb-m { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
.hb-k { display: flex; justify-content: space-between; gap: 6px; font-size: .75rem; color: var(--text-muted); }
.hb-k b { color: var(--text); font-weight: 700; }
.hb-tr { height: 4px; border-radius: 99px; background: color-mix(in srgb, var(--text) 10%, transparent); margin-top: 5px; overflow: hidden; }
.hb-tr i { display: block; height: 100%; border-radius: 99px; background: var(--level-ok); }
.hb-tr i.is-warn { background: var(--level-warn); } .hb-tr i.is-bad { background: var(--level-alarm); } .hb-tr i.is-none { background: transparent; }
.hb-sub { font-size: .68rem; color: var(--text-hint, var(--text-muted)); margin-top: 3px; }
.hb-big { font-family: var(--font-display); font-size: 1.25rem; font-weight: 800; line-height: 1.15; color: var(--text); margin-top: 3px; }
.hb-big.hb-date { font-size: .95rem; padding-top: 3px; }
.hb-big.is-warn { color: var(--warning-dark); }
.hb-strip { margin-top: 12px; }
.hb-strip .hb-k { margin-bottom: 5px; }
.hb-s { display: grid; grid-template-columns: repeat(24, 1fr); gap: 2px; }
.hb-s i { height: 14px; border-radius: 3px; background: var(--level-ok); }
.hb-s i.is-warn { background: var(--level-warn); } .hb-s i.is-bad { background: var(--level-alarm); }
/* a place's activity, hour by hour: a column per hour, its height the hour's share of the busiest - in the theme's accent, not a level */
.hb-act { align-items: end; height: 26px; }
.hb-act i { height: auto; background: color-mix(in srgb, var(--primary) 55%, transparent); border-radius: 2px; }
.hb-act i.is-none { background: color-mix(in srgb, var(--text) 10%, transparent); }
.hb-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.hb-tags span { font-weight: 500; background: transparent; }
.hb-top { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 14px; align-items: stretch; }
.hb-top .hb-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); align-content: start; }
.hb-globe { position: relative; margin: 0; min-height: 300px; overflow: hidden; padding: 0; }
.hb-gl { position: absolute; inset: 0; }
.hb-gcap { position: absolute; left: 10px; top: 10px; z-index: 2; display: flex; flex-wrap: wrap; gap: 12px; font-size: .75rem; color: var(--text-muted);
  padding: 4px 10px; border-radius: 999px; background: var(--card); border: 1px solid var(--border); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.hb-gk { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; vertical-align: 0; background: var(--text); }
.hb-gk.is-in { background: var(--primary); } .hb-gk.is-btw { background: var(--success); } .hb-gk.is-ord { background: var(--text-muted); } .hb-gk.is-out { background: var(--warning); }
.hb-gnote { position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 2; font-size: .7rem; color: var(--text-muted);
  padding: 5px 10px; border-radius: 10px; background: var(--card); border: 1px solid var(--border); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.hb-lbl { font-size: .66rem; font-weight: 600; line-height: 1.2; padding: 3px 7px; border-radius: 999px; white-space: nowrap; transform: translate(-50%, -130%);
  background: var(--card); color: var(--text); border: 1px solid var(--border); box-shadow: 0 4px 12px -6px rgba(0,0,0,.3); pointer-events: none; }
.hb-lbl.is-site { background: var(--text); color: var(--bg); border-color: var(--text); }
.hb-lbl.is-in { color: var(--primary-ink, var(--primary)); }
@media (max-width: 900px) { .hb-top { grid-template-columns: 1fr; } }
@media (max-width: 640px) { .hb-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } .hb-top .hb-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } .hb-cards { grid-template-columns: 1fr; } }

/* ============================================================================
   §44 · SIDE PAGES (15-10, Vijay: "i see why your skins doesnot apply to the side pages", "the transissions are also
   not good when side page opens and closes", then on three previews - a sheet, a floating pane, docked - "give all three
   options as settings"). A side page is a panel that opens from the right over (or beside) the page: ui.drawer's
   `.ox-drawer` (41 screens), Vehicles' `.fl-drawer`, the Secretary Desk's `#sd-drawer`. Each carries `.ui-side`, its veil
   `.ui-side-scrim` (ui.drawer's veil is its own `.ox-drawer-ov`), and opens and closes ONLY through ui.sideOpen /
   ui.sideClose (public/js/core/ui.js), which add and take away `.side-in` around the motion.
   ★ HOW IT SITS IS THE PERSON'S OWN SETTING - Appearance → Side Pages, `<html data-side>`, core/appearance.js SIDES:
     sheet  (the default) full height against the right edge, sliding in on a soft spring, the page under a light veil
     float  a pane with the theme's corners standing 14 px off the edges; the page behind FROSTS over instead of greying
     dock   the page makes room and stays live beside it (wide screens only - below 1101 px it floats)
   ★ WHAT IT IS PAINTED WITH IS THE SKIN'S: on Glass, Liquid and Frosted the side page takes the skin's own canvas
   (`--side-ground`), so its cards are glass on colour exactly as on the page. It used to be a 96% white sheet (the 28-09
   fix for the page showing through it) - readable, but the skin stopped at its edge and everything inside was white on
   white. Opaque, so nothing behind shows through and no backdrop blur is spent on it. Every other theme keeps its sheet.
   ============================================================================ */
/* the panel's width while docked - measured and set on <html> by ui.sideOpen (this is only the value before it is);
   its edge, its veil and the frost behind a floating pane - each skin with a canvas sets its own below, a dark ground its
   darker veil and frost. --side-ground is the theme's card - the sheet - until a skin with a canvas sets its own. */
:root { --side-open-w: 600px; --side-ground: var(--card); --side-edge: var(--border); --side-scrim: rgba(15,23,42,.28); --side-frost: rgba(248,250,252,.32); }
:root:is([data-theme$="-dark"], [data-theme="black"], [data-theme="console"]) { --side-scrim: rgba(4,6,12,.55); --side-frost: rgba(8,10,18,.42); }
.ui-side { background: var(--side-ground);
  border-left: 1px solid var(--side-edge); box-shadow: none;
  transform: translateX(calc(100% + 40px)); transition: transform .26s cubic-bezier(.4,0,.9,.6), box-shadow .26s ease, opacity .2s ease; }
.ui-side.side-in { transform: none; box-shadow: -30px 0 80px -30px rgba(15,23,42,.35); transition: transform .44s cubic-bezier(.22,1,.36,1), box-shadow .44s ease, opacity .28s ease; }
.ui-side-scrim, .ox-drawer-ov { background: var(--side-scrim); opacity: 0; transition: opacity .26s ease; }
.ui-side-scrim.side-in, .ox-drawer-ov.show { opacity: 1; transition: opacity .36s ease; }
/* the skin's canvas, drawn for a panel on the right: a light high on the right, one low on the left, the skin's own base */
:is([data-theme="liquid"],[data-theme="liquid-dark"]):not([data-skin]) {
  --side-ground:
    radial-gradient(closest-side, color-mix(in srgb, var(--lg-a2) 14%, transparent), transparent) right -170px top -150px / 640px 640px no-repeat,
    radial-gradient(closest-side, color-mix(in srgb, var(--lg-a3) 12%, transparent), transparent) left -140px bottom -170px / 580px 500px no-repeat,
    linear-gradient(160deg, var(--lg-sky-1), var(--lg-sky-2));
  --side-edge: var(--glass-brd); --side-scrim: rgba(15,23,42,.20); --side-frost: rgba(255,255,255,.22); }
[data-theme="liquid-dark"]:not([data-skin]) { --side-scrim: rgba(4,6,12,.50); --side-frost: rgba(8,10,18,.42); }
[data-theme="liquid"][data-skin="frosted"] {
  --side-ground:
    radial-gradient(closest-side, rgba(195,217,255,.95) 0%, rgba(195,217,255,.45) 45%, rgba(195,217,255,0) 100%) right -170px top -150px / 640px 640px no-repeat,
    radial-gradient(closest-side, rgba(211,243,226,.95) 0%, rgba(211,243,226,0) 100%) left -140px bottom -170px / 580px 500px no-repeat,
    radial-gradient(closest-side, rgba(253,210,187,.85) 0%, rgba(253,210,187,.35) 45%, rgba(253,210,187,0) 100%) left -280px top 32% / 540px 660px no-repeat,
    linear-gradient(#fbfcff, #f5f7fc);
  --side-edge: rgba(255,255,255,.95); --side-scrim: rgba(24,28,44,.16); --side-frost: rgba(255,255,255,.26); }
[data-theme="liquid-dark"][data-skin="frosted"] {
  --side-ground:
    radial-gradient(closest-side, rgba(120,160,255,.28) 0%, rgba(120,160,255,.10) 45%, rgba(120,160,255,0) 100%) right -170px top -150px / 640px 640px no-repeat,
    radial-gradient(closest-side, rgba(110,220,170,.15) 0%, rgba(110,220,170,0) 100%) left -140px bottom -170px / 580px 500px no-repeat,
    radial-gradient(closest-side, rgba(253,170,130,.20) 0%, rgba(253,170,130,.08) 45%, rgba(253,170,130,0) 100%) left -280px top 32% / 540px 660px no-repeat,
    linear-gradient(#12141d, #0d0f17);
  --side-edge: rgba(255,255,255,.14); --side-scrim: rgba(4,6,12,.50); --side-frost: rgba(8,10,18,.42); }
[data-theme="glass"] {
  --side-ground:
    radial-gradient(closest-side, rgba(165,180,252,.40), transparent) right -170px top -150px / 640px 640px no-repeat,
    radial-gradient(closest-side, rgba(125,211,252,.30), transparent) left -140px bottom -170px / 580px 500px no-repeat,
    linear-gradient(#ebeff9, #e4e9f6);
  --side-edge: var(--glass-brd); --side-scrim: rgba(15,23,42,.22); --side-frost: rgba(255,255,255,.24); }
[data-theme="glass-dark"] {
  --side-ground:
    radial-gradient(closest-side, rgba(99,102,241,.24), transparent) right -170px top -150px / 640px 640px no-repeat,
    radial-gradient(closest-side, rgba(14,165,233,.16), transparent) left -140px bottom -170px / 580px 500px no-repeat,
    linear-gradient(#0c0e18, #05060b);
  --side-edge: var(--glass-brd); --side-scrim: rgba(4,6,12,.55); --side-frost: rgba(8,10,18,.45); }
/* a canvas only where the skin is drawing one: glass switched off, a plain white page, an OS that asked for less
   transparency or a browser that cannot frost - the side page keeps the sheet those give every surface */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]):is([data-glass="off"],[data-bg="plain"]) { --side-ground: initial; }
@media (prefers-reduced-transparency: reduce) { :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) { --side-ground: initial; } }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { :is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) { --side-ground: initial; } }
/* ★ OVERRIDES §27's `.ox-drawer` on the glass grounds (--glass-over with its backdrop blur): a side page there is the
   skin's opaque canvas now. Named once more (.ui-side twice) so it outweighs that rule's two attributes. */
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ui-side.ui-side {
  background: var(--side-ground, var(--glass-over)); -webkit-backdrop-filter: var(--over-blur); backdrop-filter: var(--over-blur); }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]):not([data-glass="off"]):not([data-bg="plain"]) .ui-side.ui-side { -webkit-backdrop-filter: none; backdrop-filter: none; }
:is([data-theme="glass"],[data-theme="glass-dark"],[data-theme="liquid"],[data-theme="liquid-dark"]) .ui-side .ox-drawer-f { background: var(--card); }
/* ★ OVERRIDES §27's dark veil for `.ox-drawer-ov` on the dark grounds: the veil is each skin's --side-scrim */
:is([data-theme="glass-dark"],[data-theme="liquid-dark"]) :is(.ox-drawer-ov, .ui-side-scrim) { background: var(--side-scrim); }

/* FLOAT - a pane standing off the edges, the page behind frosted. Also what DOCK is on a screen too narrow to dock. */
html[data-side="float"] .ui-side { top: 14px; right: 14px; bottom: 14px; max-width: calc(100vw - 28px); border-radius: var(--radius); overflow: hidden;
  border: 1px solid var(--side-edge); box-shadow: 0 30px 80px -24px rgba(15,23,42,.40);
  opacity: 0; transform: translateX(56px) scale(.97); transform-origin: right center; transition: opacity .2s ease, transform .24s cubic-bezier(.4,0,1,1); }
html[data-side="float"] .ui-side.side-in { opacity: 1; transform: none; transition: opacity .28s ease, transform .5s cubic-bezier(.16,1.1,.3,1); }
html[data-side="float"] :is(.ui-side-scrim, .ox-drawer-ov) { background: var(--side-frost); -webkit-backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2); transition: opacity .22s ease; }
html[data-side="float"] :is(.ui-side-scrim.side-in, .ox-drawer-ov.show) { transition: opacity .4s ease; }
html[data-side="float"] .ox-drawer-ov { padding: 14px; }
@media (max-width: 1100px) {
  html[data-side="dock"] .ui-side { top: 14px; right: 14px; bottom: 14px; max-width: calc(100vw - 28px); border-radius: var(--radius); overflow: hidden;
    border: 1px solid var(--side-edge); box-shadow: 0 30px 80px -24px rgba(15,23,42,.40);
    opacity: 0; transform: translateX(56px) scale(.97); transform-origin: right center; transition: opacity .2s ease, transform .24s cubic-bezier(.4,0,1,1); }
  html[data-side="dock"] .ui-side.side-in { opacity: 1; transform: none; transition: opacity .28s ease, transform .5s cubic-bezier(.16,1.1,.3,1); }
  html[data-side="dock"] :is(.ui-side-scrim, .ox-drawer-ov) { background: var(--side-frost); -webkit-backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2); }
  html[data-side="dock"] .ox-drawer-ov { padding: 14px; }
}
/* DOCK - nothing covered: the page narrows by the panel's width (ui.sideOpen measures it into --side-open-w and marks
   <html class="side-docked">) and stays live beside it; no veil, and ui.drawer's overlay lets every press through */
@media (min-width: 1101px) {
  html[data-side="dock"] .ui-side { top: 14px; right: 14px; bottom: 14px; max-width: 44vw; border-radius: var(--radius); overflow: hidden;
    border: 1px solid var(--side-edge); box-shadow: 0 20px 60px -30px rgba(15,23,42,.32);
    transform: translateX(calc(100% + 30px)); transition: transform .3s cubic-bezier(.4,0,1,1); }
  html[data-side="dock"] .ui-side.side-in { transform: none; transition: transform .46s cubic-bezier(.22,1,.36,1); }
  html[data-side="dock"] .ui-side-scrim { display: none !important; }
  html[data-side="dock"] .ox-drawer-ov { background: transparent; padding: 14px; pointer-events: none; }
  html[data-side="dock"] .ox-drawer-ov .ox-drawer { pointer-events: auto; }
  html[data-side="dock"] .main-content { transition: margin-left .25s ease, margin-right .3s cubic-bezier(.4,0,1,1); }
  html[data-side="dock"].side-docked .main-content { margin-right: calc(var(--side-open-w) + 14px); transition: margin-left .25s ease, margin-right .46s cubic-bezier(.22,1,.36,1); }
}
/* the system asked for less motion: no slide, no settle - a quick fade, whichever way it sits */
@media (prefers-reduced-motion: reduce) {
  html .ui-side.ui-side { transform: none; opacity: 0; transition: opacity .12s linear; }
  html .ui-side.ui-side.side-in { opacity: 1; }
  html :is(.ui-side-scrim, .ox-drawer-ov) { transition: opacity .12s linear; }
  html[data-side="dock"] .main-content { transition: none; }
}

/* ============================================================================
   §45 · PLANTX: EVERY DATA ROW ANSWERS THE MOUSE (15-10, Vijay: "in plantx skin dark mode why there is limited mouse hover
   effects... in some module data hover working in some module data its not working"). Measured on every module page as a
   plant head, mouse over a row of every kind:
     - the shared row (`.ox-row:hover { background: var(--bg) }`, 3161) paints the row with the PAGE's colour. PlantX's page
       is darker than its card on the dark ground (#0a0d12 under #121821 - a row turning darker, 1.09:1, a hole nobody sees)
       and the SAME white as its card on the light one (nothing at all);
     - Work Orders' priority rows carry a tint of their own at (1,2,0), which out-ranks the hover - those never answered;
     - Stores, Accounting, HR Overview, Muster and Production draw tables of their own classes (st-rcv, ac-fs-t, hro-table,
       mu-table, pr-grid) that no hover rule names.
   One answer for the skin, on both grounds: the hovered row is LAID OVER with a tint (a background IMAGE - a row's own colour,
   a priority tint or a muster day's state, stays underneath and readable), lighter on the dark ground, darker on the light,
   on any `.ox-row` and on every body row of any table on the page or in a side page. A form's grid keeps its own (§ form-table).
   ★ OVERRIDES the shared rows' --bg hover (.ox-row 3161, .data-table 1420-1422, .dtable 2327, .admin-table 1648) on this skin
   only - the colour they set is taken off so the tint is what shows.
   ============================================================================ */
/* --row-hover is the same answer as one solid colour, for a page's own row that paints its hover itself (the asset register's
   .as-row): the page colour everywhere, as it always was, and the tinted card on this skin */
:root { --row-hover: var(--bg); }
[data-theme="plantx"]      { --px-row-hover: rgba(18,22,10,.055);  --row-hover: color-mix(in srgb, #12160a 5.5%, var(--card)); }
[data-theme="plantx-dark"] { --px-row-hover: rgba(255,255,255,.075); --row-hover: color-mix(in srgb, #ffffff 7.5%, var(--card)); }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .ox-row:hover {
  background-color: transparent; background-image: linear-gradient(var(--px-row-hover), var(--px-row-hover)); }
/* a priority row keeps its own tint (its (1,2,0) colour out-ranks the line above) - the overlay is added on top of it */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) #wo-root .ox-row[data-prio]:hover { background-image: linear-gradient(var(--px-row-hover), var(--px-row-hover)); }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) :is(.data-table, .dtable, .admin-table) tbody tr:hover { background-color: transparent; }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) :is(.data-table, .dtable, .admin-table) tbody tr:nth-child(even):hover { background-color: var(--tint-neutral-bg); }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) :is(.content-area, .ui-side) table:not(.form-table) > tbody > tr:hover > :is(td, th) {
  background-image: linear-gradient(var(--px-row-hover), var(--px-row-hover)); }
/* ★ OVERRIDES `.form-table tr:hover td` (633) on this skin for a plain row - the shared form grid that PM's schedule, its
   history and every report grid draw (measured: PM's rows 1.09:1 darker on the dark ground, nothing on the light). A
   section header, a sub-header and a totals row keep their own hover. */
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .form-table tr:not(.section-header):not(.sub-header):not(.totals-row):hover > td {
  background-color: transparent; background-image: linear-gradient(var(--px-row-hover), var(--px-row-hover)); }
:is([data-theme="plantx"], [data-theme="plantx-dark"]) .form-table tr:not(.section-header):not(.sub-header):not(.totals-row):hover > td.row-label {
  background-color: var(--th-bg); }

/* ============================================================================
   §46 · THE PAY SIDE PAGE (16-10, billing frontend - public/js/core/app.js payPanel). Paying an order was a one-line confirm
   box; it is a side page now (ui.drawer, so it sits as each person chose and takes the skin): what the order carries, its
   figure in words, how it is paid, then what the payment did. Its own classes only (pp-), every colour a token.
   ============================================================================ */
.pp-cap { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); margin: 16px 0 6px; }
.pp-lines .pp-cap { margin-top: 0; }
.pp-row { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13.5px; }
.pp-row > span { color: var(--text); min-width: 0; }
.pp-row > span small { display: block; color: var(--text-muted); font-size: 12px; margin-top: 2px; }
.pp-row > b { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text); }
.pp-row.pp-sub > span { color: var(--text-muted); }
.pp-row.pp-tot { border-bottom: 0; border-top: 2px solid var(--border); margin-top: 4px; padding-top: 10px; font-size: 15px; }
.pp-row.pp-tot > b { font-size: 18px; }
.pp-why { font-size: 13px; line-height: 1.55; color: var(--text); margin: 0; }
.pp-hint { font-size: 12px; color: var(--text-muted); margin: 8px 0 0; line-height: 1.5; }
.pp-warn { background: var(--tint-warn-bg); color: var(--tint-warn-fg); border-radius: var(--radius-sm); padding: 10px 12px; font-size: 13px; line-height: 1.5; margin: 14px 0 0; }
.pp-done { display: flex; align-items: center; gap: 8px; background: var(--tint-ok-bg); color: var(--tint-ok-fg); border-radius: var(--radius-sm); padding: 10px 12px; font-size: 13px; font-weight: 600; margin-top: 14px; }
/* ★ the agreement accepted at the checkout (16-10, core/legal.js) - one box, ticked before Pay, only while the company has not
   accepted the version in force; afterwards one quiet line naming what it accepted */
.pp-terms { display: flex; align-items: flex-start; gap: 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 11px 12px; margin: 14px 0 0; font-size: 12.5px; line-height: 1.55; cursor: pointer; color: var(--text); }
.pp-terms input { width: auto; margin: 2px 0 0; flex: none; }
.pp-terms a, .pp-legal a { color: var(--primary-ink, var(--primary-dark)); font-weight: 600; }
.pp-terms small { display: block; color: var(--text-muted); margin-bottom: 4px; }
.pp-legal { font-size: 12px; color: var(--text-muted); margin: 10px 0 0; line-height: 1.5; }
.pp-by { margin-bottom: 12px; }
.pp-by button[disabled] { opacity: .5; cursor: not-allowed; }
.pp-steps { display: flex; flex-direction: column; gap: 2px; }
.pp-steps > div { display: flex; align-items: center; gap: 10px; padding: 8px 0; font-size: 13.5px; border-bottom: 1px solid var(--border); }
.pp-dot { width: 20px; height: 20px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; flex: none; }
.pp-dot.on { background: var(--tint-ok-bg); color: var(--tint-ok-fg); }
.pp-dot.off { background: var(--tint-danger-bg); color: var(--tint-danger-fg); }
@media (prefers-reduced-motion: no-preference) { .pp-steps > div { animation: pp-in .32s ease both; } .pp-steps > div:nth-child(2) { animation-delay: .08s; } .pp-steps > div:nth-child(3) { animation-delay: .16s; } .pp-steps > div:nth-child(n+4) { animation-delay: .24s; } }
@keyframes pp-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* ============================================================================
   §47 · THE RENEWAL BOARD AND THE STORE'S CART (16-10, billing frontend - views/company/account.ejs, the approved preview's
   Billing A): the four figures (ui.statCards), who is paid until when (a twelve-month timeline - a lapsed or never-paid premises
   the striped bar), a card per premises folded to what is on it, each sanctioned project with its users and storage as meters,
   the orders waiting, every invoice; the Module Store in four tabs with a cart that prices to the renewal date. bb- / sc- only.
   ============================================================================ */
.bb-sec { font-size: 13px; font-weight: 700; color: var(--text); margin: 4px 0 10px; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.bb-sec small { font-weight: 500; color: var(--text-muted); font-size: 12px; }
.bb-tl { overflow-x: auto; }
.bb-tl-grid { min-width: 640px; }
.bb-tl-row { display: grid; grid-template-columns: 170px 1fr; align-items: center; gap: 12px; padding: 7px 0; }
.bb-tl-row + .bb-tl-row { border-top: 1px dashed var(--border); }
.bb-tl-name { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bb-tl-name small { display: block; font-weight: 500; font-size: 11.5px; color: var(--text-muted); }
.bb-tl-row.is-proj .bb-tl-name { padding-left: 12px; font-weight: 500; }
.bb-tl-months { display: grid; grid-template-columns: repeat(12, 1fr); font-size: 11px; color: var(--text-muted); font-weight: 600; letter-spacing: .03em; }
.bb-tl-months span { padding-left: 4px; border-left: 1px solid var(--border); }
.bb-tl-track { position: relative; height: 22px; border-radius: 6px; background: var(--tint-neutral-bg); overflow: hidden; }
.bb-tl-bar { position: absolute; top: 0; bottom: 0; border-radius: 6px; display: flex; align-items: center; padding: 0 8px; font-size: 11px; font-weight: 700; white-space: nowrap; overflow: hidden; }
.bb-tl-bar.paid { background: var(--tint-ok-bg); color: var(--tint-ok-fg); box-shadow: inset 3px 0 0 var(--level-ok); }
.bb-tl-bar.soon { background: var(--tint-warn-bg); color: var(--tint-warn-fg); box-shadow: inset 3px 0 0 var(--level-warn); }
.bb-tl-bar.unpaid { left: 0; right: 0; color: var(--tint-danger-fg);
  background: repeating-linear-gradient(135deg, var(--tint-danger-bg) 0 8px, color-mix(in srgb, var(--tint-danger-bg) 55%, transparent) 8px 16px); }
.bb-tl-today { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--text); opacity: .55; }
.bb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; margin-bottom: 18px; }
.bb-card { display: flex; flex-direction: column; gap: 10px; }
.bb-card-h { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.bb-card-h b { font-size: 15px; color: var(--text); display: block; }
.bb-card-h small { font-size: 12px; color: var(--text-muted); display: block; margin-top: 3px; line-height: 1.45; }
.bb-year { height: 6px; border-radius: 99px; background: var(--tint-neutral-bg); overflow: hidden; }
.bb-year > i { display: block; height: 100%; border-radius: 99px; background: var(--level-ok); }
.bb-year.soon > i { background: var(--level-warn); }
.bb-on summary { cursor: pointer; font-size: 12.5px; font-weight: 600; color: var(--primary-ink, var(--primary)); list-style: none; }
.bb-on summary::-webkit-details-marker { display: none; }
.bb-on[open] summary { margin-bottom: 6px; }
.bb-on .bb-g { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted); margin: 8px 0 2px; }
.bb-on .bb-l { display: flex; justify-content: space-between; gap: 10px; font-size: 12.5px; padding: 3px 0; color: var(--text); }
.bb-on .bb-l b { font-variant-numeric: tabular-nums; font-weight: 600; }
.bb-acts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: auto; }
.bb-meter { display: grid; grid-template-columns: 1fr auto; gap: 4px 10px; align-items: center; font-size: 12px; color: var(--text-muted); min-width: 150px; }
.bb-meter > i { grid-column: 1 / -1; height: 6px; border-radius: 99px; background: var(--tint-neutral-bg); overflow: hidden; position: relative; }
.bb-meter > i > em { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 99px; background: var(--level-ok); }
.bb-meter.warn > i > em { background: var(--level-warn); }
.bb-meter.over > i > em { background: var(--level-alarm); }
.bb-meter b { color: var(--text); font-variant-numeric: tabular-nums; }
.bb-proj { display: grid; grid-template-columns: minmax(180px, 1.4fr) minmax(120px, 1fr) minmax(150px, 1fr) minmax(150px, 1fr) auto; gap: 14px; align-items: center; padding: 12px 0; border-top: 1px solid var(--border); }
.bb-proj:first-of-type { border-top: 0; }
.bb-proj-n b { display: block; font-size: 13.5px; color: var(--text); }
.bb-proj-n small { font-size: 12px; color: var(--text-muted); }
@media (max-width: 860px) { .bb-proj { grid-template-columns: 1fr 1fr; } }
.bb-filt { display: flex; gap: 6px; flex-wrap: wrap; }
.bb-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* the Module Store: a premises picked once, four tabs, and the cart beside them */
.sc-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 18px; align-items: start; }
@media (max-width: 1000px) { .sc-wrap { grid-template-columns: 1fr; } }
.sc-cart { position: sticky; top: 76px; }
.sc-cart .pp-row { font-size: 13px; }
.sc-empty { font-size: 13px; color: var(--text-muted); padding: 6px 0 2px; }
.sc-term { font-size: 12.5px; color: var(--text-muted); line-height: 1.5; margin: 8px 0 12px; }
.sc-tabs { margin-bottom: 16px; }

/* ============================================================================
   §48 · THE PROJECTS BRANCH AND THE DRAFTED TEAM (16-10, positions step 3 - core/project-draft.js; views/admin/plants.ejs
   renderBranchProject, public/js/plan/project-team.js, public/js/org-chart.js). A seat's project job is a DASHED chip under its
   box on the org chart - a matrix line, never its reporting line; each open project its positions as a reports-to tree, a vacant
   one marked, a draft's lines dashed. ph- / oc-carry / pt-draft only, every colour a token.
   ============================================================================ */
.oc-carry { margin-top: 4px; font-size: 10.5px; font-weight: 600; color: var(--primary-ink); border: 1px dashed var(--primary); border-radius: 999px; padding: 1px 7px; display: inline-block; }
.ph-branch { border: 1px dashed var(--border); border-radius: var(--radius); padding: 12px 14px; background: var(--bg); margin-top: 14px; display: flex; flex-direction: column; gap: 10px; }
.ph-proj { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
.ph-proj-h { display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; align-items: flex-start; }
.ph-proj-h b { font-size: 13.5px; color: var(--text); }
.ph-sub { font-size: 12px; color: var(--text-muted); }
.ph-tree { margin-left: 6px; border-left: 2px solid var(--border); padding-left: 12px; display: flex; flex-direction: column; }
.ph-node { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; padding: 4px 0; font-size: 12.5px; }
.ph-t { font-weight: 700; color: var(--text); }
.ph-h { color: var(--text-muted); font-size: 12px; }
.ph-draft { opacity: .8; }
.ph-draft .ph-t { border-bottom: 1px dashed var(--text-muted); }
.ph-vacant { font-size: 11px; font-weight: 700; padding: 1px 8px; border-radius: 999px; background: var(--tint-danger-bg); color: var(--tint-danger-fg); }
.pt-draft { margin-bottom: 14px; }
.pt-draft .pt-dbanner { display: flex; gap: 10px; align-items: flex-start; padding: 12px 14px; border-radius: var(--radius-sm); background: var(--tint-info-bg); color: var(--tint-info-fg); font-size: 13px; margin-bottom: 10px; }
.pt-dl { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, .9fr) minmax(0, 2.4fr) 96px; gap: 8px 12px; align-items: center; padding: 9px 0; border-top: 1px solid var(--border); font-size: 13px; }
/* every row on the same four columns (17-10): the state column was `auto`, so each row - its own grid - sized it to its own pill and
   Grade and Carried By drifted from row to row. The heading row names them, as the approved preview did */
.pt-dl > :last-child { justify-self: end; }
.pt-dl.pt-dh { padding: 6px 0; font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); border-top: 0; }
.pt-dl > div > .form-control { width: 100%; }
.pt-dl:first-of-type { border-top: 0; }
.pt-dl b { display: block; color: var(--text); }
.pt-dl small { color: var(--text-muted); font-size: 12px; }
@media (max-width: 760px) { .pt-dl { grid-template-columns: 1fr 1fr; } }
.pt-dacts { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; margin-top: 10px; }

/* ============================================================================
   §49 · HELP - A BOOK PER MODULE, A TOPIC PER TASK (16-10, public/js/core/help.js; config/help/<book>/; the approved preview
   2DFCJudNKZvBaDfS4CASnk). A side page with Contents · Index · Search on the left and the topic on the right; /help/<book>
   draws the whole book as one page. hp- only, every colour a token, the font the page's own. The side page takes the class
   `hp-side` from ui.drawer's `cls` (set per call, so the next drawer drops it).
   ============================================================================ */
.ui-side.hp-side .ox-drawer-b { padding: 0; overflow: hidden; display: flex; }
.ui-side.hp-side .ox-drawer-b > .hp { flex: 1; }
.hp { display: grid; grid-template-columns: 300px minmax(0, 1fr); height: 100%; min-height: 0; }
.hp-nav { border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; }
.hp-tabs { padding: 12px; border-bottom: 1px solid var(--border); }
.hp-tabs .ox-views { width: 100%; display: flex; }
.hp-tabs .ox-views button { flex: 1; }
.hp-list { overflow: auto; padding: 6px 8px 16px; min-height: 0; flex: 1; }
.hp-part > summary { list-style: none; cursor: pointer; padding: 8px; font-weight: 700; font-size: .82rem; display: flex; gap: 8px; align-items: center; border-radius: 8px; color: var(--text); }
.hp-part > summary::-webkit-details-marker { display: none; }
.hp-part > summary:hover { background: var(--bg); }
.hp-part > summary .n { color: var(--text-muted); font-weight: 600; min-width: 18px; }
.hp-t { display: block; width: 100%; text-align: left; border: 0; background: none; font: inherit; font-size: .84rem; color: var(--text); padding: 6px 8px 6px 34px; border-radius: 8px; cursor: pointer; }
.hp-t:hover { background: var(--bg); }
.hp-t.on { background: color-mix(in srgb, var(--primary) 12%, transparent); font-weight: 600; }
.hp-t:focus-visible, .hp-idx button:focus-visible, .hp-hit:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.hp-body { overflow: auto; padding: 22px 28px 40px; min-height: 0; color: var(--text); }
.hp-crumb { font-size: .76rem; color: var(--text-muted); margin-bottom: 6px; }
.hp-body h2 { margin: 0 0 8px; font-size: 1.35rem; color: var(--text); }
.hp-sum { color: var(--text-muted); max-width: 72ch; margin: 0 0 14px; line-height: 1.55; }
.hp-facts { margin: 0 0 12px; }
.hp-you { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin: 0 0 18px; }
.hp-body h3 { font-size: .8rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); margin: 22px 0 8px; }
.hp-body ol, .hp-body ul { margin: 0; padding-left: 22px; display: grid; gap: 6px; line-height: 1.55; max-width: 78ch; }
.hp-body a[data-go] { color: var(--primary-ink); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
.hp-tbl { overflow-x: auto; }
.hp-body table.data-table { table-layout: fixed; width: 100%; }
.hp-body table.data-table thead th { position: static; white-space: normal; }
.hp-body table.data-table td { vertical-align: top; font-size: .84rem; white-space: normal; overflow-wrap: anywhere; }
.hp-body code { font-size: .8rem; }
.hp-api { display: grid; gap: 4px; }
.hp-api code { display: block; padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); white-space: pre-wrap; }
.hp-rel { display: flex; flex-wrap: wrap; gap: 6px; }
.hp-note { font-size: .8rem; color: var(--text-muted); margin-top: 10px; max-width: 78ch; }
.hp-idx { padding: 4px 8px; }
.hp-idx h5 { margin: 12px 0 4px; font-size: .78rem; color: var(--text-muted); }
.hp-idx button { display: block; border: 0; background: none; font: inherit; font-size: .84rem; padding: 4px 6px; cursor: pointer; color: var(--text); text-align: left; border-radius: 6px; }
.hp-idx button:hover { background: var(--bg); }
.hp-idx button small { color: var(--text-muted); }
.hp-srch { padding: 4px 8px; }
.hp-srch .form-control { margin-bottom: 10px; }
.hp-hit { display: block; width: 100%; text-align: left; border: 1px solid var(--border); background: var(--card); color: var(--text); border-radius: 10px; padding: 8px 10px; margin-bottom: 8px; cursor: pointer; font: inherit; }
.hp-hit b { display: block; font-size: .86rem; }
.hp-hit span { font-size: .78rem; color: var(--text-muted); }
.hp-hit mark { background: color-mix(in srgb, var(--warning) 40%, transparent); color: inherit; }
.hp-foot { margin-right: auto; font-size: .8rem; color: var(--text-muted); }
/* the book's own page to read (HELP.page, 29-10): the search and the contents left, one topic right, each scrolling by itself */
.card.hp-page { padding: 0; overflow: hidden; height: calc(100vh - 200px); min-height: 480px; }
.hp-find { padding: 12px 12px 0; }
.hp-hit.on { border-color: var(--primary); }
/* the whole book as one page (/help/<book>) */
.hp-manual { max-width: 1080px; margin: 0 auto; padding: 8px 0 60px; color: var(--text); }
.hp-manual h1 { margin: 6px 0 4px; font-size: 1.9rem; }
.hp-toc { padding: 16px 18px; margin: 18px 0 8px; }
.hp-toc h3 { margin: 0 0 10px; font-size: .8rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.hp-toc-cols { columns: 3 240px; column-gap: 24px; }
.hp-toc-part { break-inside: avoid; margin-bottom: 12px; }
.hp-toc-part b { display: block; font-size: .86rem; margin-bottom: 4px; }
.hp-toc-part a { display: block; font-size: .8rem; color: var(--text-muted); text-decoration: none; padding: 2px 0; }
.hp-toc-part a:hover { color: var(--primary-ink); }
.hp-parth { margin: 34px 0 6px; padding-bottom: 6px; border-bottom: 1px solid var(--border); font-size: 1.15rem; }
.hp-manual .hp-sect { overflow: visible; padding: 18px 0 8px; border-bottom: 1px dashed var(--border); scroll-margin-top: 70px; }
.hp-manual .hp-sect h2 { scroll-margin-top: 70px; }
/* a printed book under its product and the platform (29-10, help.js framed/coverHtml): the product heads every printed page, the
   platform's branding foots it - a table's header and footer groups, which the browser repeats on each page - and the cover is a
   page of its own */
.hp-frame { width: 100%; border-collapse: collapse; }
.hp-frame > thead > tr > td, .hp-frame > tfoot > tr > td, .hp-frame > tbody > tr > td { padding: 0; vertical-align: top; }
.hp-run { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; margin-bottom: 16px; border-bottom: 1px solid var(--border); font-size: .82rem; color: var(--text-muted); }
.hp-run.hp-run-f { margin: 22px 0 0; border-bottom: 0; border-top: 1px solid var(--border); }
.hp-run-space { display: none; }
.hp-run-l { display: inline-flex; align-items: center; gap: 10px; }
.hp-run-logo { height: 26px; width: auto; }
.hp-foot-logo { height: 22px; width: auto; }
.hp-run-prod { color: var(--text); font-weight: 800; font-size: 1.05rem; }
.hp-run-what { padding-left: 10px; border-left: 1px solid var(--border); }
.hp-cover { padding: 40px 0 34px; margin-bottom: 18px; border-bottom: 1px solid var(--border); }
.hp-cover-prod { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.hp-cover-logo { height: 64px; width: auto; }
.hp-cover-word { font-size: 2.7rem; font-weight: 800; color: var(--text); line-height: 1; letter-spacing: -.01em; }
.hp-cover-kind { font-size: .82rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); margin-top: 14px; }
.hp-manual .hp-cover h1 { margin: 48px 0 8px; font-size: 2.2rem; }
.hp-cover-meta { font-size: .9rem; color: var(--text-muted); }
.hp-cover-plat { display: grid; justify-items: start; gap: 6px; margin-top: 64px; padding-top: 22px; border-top: 1px solid var(--border); }
.hp-plat-logo { height: 40px; width: auto; }
.hp-cover-plat small { font-size: .74rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); }
.hp-cover-plat span, .hp-cover-plat a { font-size: .9rem; color: var(--text-muted); text-decoration: none; }
@media (max-width: 760px) {
  .hp { grid-template-columns: 1fr; grid-template-rows: minmax(0, 40%) minmax(0, 1fr); }
  .hp-nav { border-right: 0; border-bottom: 1px solid var(--border); }
  .hp-body { padding: 16px 16px 32px; }
}
@media print {
  .hp-manual .hp-sect { break-inside: avoid-page; border-bottom: 0; }
  .hp-toc-part a { color: var(--text); }
  .hp-printed .hp-cover { break-after: page; border-bottom: 0; }
  .hp-printed .hp-toc { break-after: page; box-shadow: none; border: 0; padding: 0; }
  /* OVERRIDES .hp-manual .hp-sect's avoid-page for the printed book: a topic longer than what is left of a page was pushed whole
     to the next, leaving pages blank under a part's heading - each part starts a page instead, and a heading keeps its text */
  .hp-printed .hp-sect { break-inside: auto; }
  .hp-printed .hp-parth { break-before: page; break-after: avoid; }
  .hp-printed .hp-sect h2, .hp-printed .hp-sect h3 { break-after: avoid; }
  .hp-printed tr { break-inside: avoid; }
  /* the foot sits at the bottom of EVERY printed page (fixed repeats per page in print); the footer group keeps its room */
  .hp-printed .hp-run-f { position: fixed; left: 0; right: 0; bottom: 0; margin: 0; background: var(--card); }
  .hp-printed .hp-run-space { display: block; height: 60px; }
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
   §50 · THE PLATFORM CONSOLE (17-10, core/platform-console.js, docs/PLATFORM-CONSOLE.md; views/platform/dashboard.ejs,
   companies.ejs, rates.ejs). Vijay: "why the platform admin console ui soo weak and basic", then on the three previews "build
   all 3, give a setting for view type". The Overview is ONE reading drawn three ways - .pc-board (Revenue Board), .pc-queue
   (Work Queue), .pc-port (Company Portfolio) - and the person's choice is theirs (console_prefs). Every surface is a .card and
   every colour a token, so each skin draws it; nothing here redefines a shared component - all of it is under .pc.
   ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.pc-viewbar { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin:0 0 16px; }
.pc-viewbar .pc-vl { display:flex; align-items:center; gap:10px; font-size:13px; color:var(--text-muted); font-weight:600; }
.pc-viewbar .pc-when { font-size:12px; color:var(--text-hint); }
.pc-row2 { display:grid; grid-template-columns:repeat(auto-fit, minmax(380px, 1fr)); gap:18px; margin:18px 0; }
.pc .card-header span.pc-cap { font-size:12px; color:var(--text-muted); font-weight:500; }
.pc-chart text { fill:var(--text-muted); }
.pc-chart text.pc-val { fill:var(--text); font-weight:700; }
.pc-chart .pc-grid { stroke:var(--border); }
.pc-chart .pc-bar { fill:var(--primary); }
.pc-chart .pc-bar.is-zero { fill:var(--border); }
.pc-legend { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px 18px; margin-top:14px; }
.pc-lg { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--text); }
.pc-lg i { width:10px; height:10px; border-radius:3px; flex:none; }
.pc-lg b { margin-left:auto; font-variant-numeric:tabular-nums; }
.pc-sw-paid { background:var(--success); } .pc-sw-lapsed { background:var(--danger); } .pc-sw-link { background:var(--primary); } .pc-sw-never { background:var(--text-hint); }
.pc-facts3 { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:12px; margin-top:18px; padding-top:14px; border-top:1px solid var(--border); }
.pc-facts3 small { display:block; font-size:12px; color:var(--text-muted); font-weight:600; }
.pc-facts3 b { display:block; font-size:20px; font-weight:800; color:var(--text); font-variant-numeric:tabular-nums; }
.pc-facts3 em { display:block; font-style:normal; font-size:11.5px; color:var(--text-muted); }
.pc-need { display:flex; align-items:center; gap:12px; padding:12px 18px; border-top:1px solid var(--border); }
.pc-need:first-child { border-top:0; }
.pc-ni { width:34px; height:34px; border-radius:9px; display:flex; align-items:center; justify-content:center; flex:none; }
.pc-ni > span, .pc-ni svg { width:17px; height:17px; }
.pc-t-warn { background:var(--tint-warn-bg); color:var(--tint-warn-fg); }
.pc-t-bad { background:var(--danger-light); color:var(--danger-dark); }
.pc-t-info { background:var(--tint-info-bg); color:var(--tint-info-fg); }
.pc-t-mute { background:var(--tint-neutral-bg); color:var(--tint-neutral-fg, var(--text-muted)); }
.pc-t-ok { background:var(--tint-ok-bg); color:var(--tint-ok-fg); }
.pc-nt { flex:1 1 auto; min-width:0; }
.pc-nt b { display:block; font-size:13.5px; font-weight:600; color:var(--text); }
.pc-nt small { display:block; font-size:12px; color:var(--text-muted); margin-top:2px; }
.pc-amt { font-weight:800; font-variant-numeric:tabular-nums; font-size:14px; white-space:nowrap; color:var(--text); }
.pc-tl { padding:6px 18px 16px; }
.pc-tlh, .pc-tlr { display:grid; grid-template-columns:minmax(140px, 220px) 86px minmax(0, 1fr); align-items:center; gap:12px; }
.pc-tlh { font-size:11px; font-weight:700; color:var(--text-hint); text-transform:uppercase; letter-spacing:.06em; padding:8px 0; }
.pc-months { display:grid; grid-template-columns:repeat(12, minmax(0, 1fr)); }
.pc-months span { border-left:1px solid var(--border); padding-left:4px; }
.pc-tlr { padding:7px 0; border-top:1px solid var(--border); font-size:13px; }
.pc-tlr .pc-nm b { display:block; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:var(--text); }
.pc-tlr .pc-nm small { color:var(--text-muted); font-size:11.5px; }
.pc-tlr .pc-mo { font-variant-numeric:tabular-nums; font-weight:700; text-align:right; color:var(--text); }
.pc-track { position:relative; height:16px; background:var(--tint-neutral-bg); border-radius:5px; overflow:hidden; }
.pc-track .pc-paid { position:absolute; top:0; bottom:0; background:var(--success); border-radius:5px; }
.pc-track .pc-lapsed { position:absolute; inset:0; background:repeating-linear-gradient(135deg, var(--danger-light) 0 6px, transparent 6px 12px); }
.pc-track .pc-today { position:absolute; top:-2px; bottom:-2px; width:2px; background:var(--primary); }
.pc-more { font-size:12.5px; color:var(--text-muted); padding:10px 0 0; }
/* the Work Queue - its lines, the items of the line picked, the item picked */
.pc-strip { display:flex; gap:22px; flex-wrap:wrap; font-size:12.5px; color:var(--text-muted); margin:0 0 14px; }
.pc-strip b { font-size:15px; color:var(--text); font-variant-numeric:tabular-nums; margin-right:4px; }
.pc-strip .is-ok b { color:var(--success-dark); } .pc-strip .is-bad b { color:var(--danger-dark); } .pc-strip .is-warn b { color:var(--warning-dark); }
.pc-q { display:grid; grid-template-columns:minmax(220px, 260px) minmax(0, 1fr) minmax(280px, 360px); gap:16px; align-items:start; }
@media (max-width: 1100px) { .pc-q { grid-template-columns:minmax(0, 1fr); } }
.pc-qlines { padding:10px; display:flex; flex-direction:column; gap:2px; }
.pc-qline { display:flex; align-items:center; gap:10px; width:100%; text-align:left; border:0; background:transparent; border-radius:var(--radius-sm); padding:10px; font:inherit; font-size:13.5px; font-weight:600; color:var(--text); cursor:pointer; }
.pc-qline:hover { background:var(--bg); }
.pc-qline.on { background:var(--primary-light); color:var(--primary-ink); }
.pc-qline .pc-bar4 { width:4px; height:22px; border-radius:2px; flex:none; }
.pc-qline .pc-c { margin-left:auto; font-size:12px; font-weight:800; border-radius:999px; padding:2px 9px; font-variant-numeric:tabular-nums; }
.pc-items { display:flex; flex-direction:column; gap:10px; }
.pc-item { display:flex; align-items:center; gap:12px; width:100%; text-align:left; border:1px solid var(--border); background:var(--card); border-radius:var(--radius); padding:13px 14px; font:inherit; color:var(--text); cursor:pointer; box-shadow:var(--el-1); }
.pc-item:hover { border-color:var(--primary); }
.pc-item.on { border-color:var(--primary); box-shadow:0 0 0 3px var(--primary-light); }
.pc-age { font-size:11.5px; font-weight:700; border-radius:999px; padding:3px 9px; white-space:nowrap; }
.pc-detail { padding:20px; display:flex; flex-direction:column; gap:14px; }
.pc-detail .pc-dk { font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--text-muted); }
.pc-detail h3 { margin:4px 0 2px; font-size:18px; font-weight:800; line-height:1.3; color:var(--text); }
.pc-detail .pc-ds { font-size:13px; color:var(--text-muted); line-height:1.5; }
.pc-big { font-size:28px; font-weight:800; letter-spacing:-.02em; font-variant-numeric:tabular-nums; color:var(--text); }
.pc-why { background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-sm); padding:12px 14px; font-size:12.5px; line-height:1.55; color:var(--text); }
.pc-acts { display:flex; flex-direction:column; gap:8px; }
.pc-acts .pc-sec { display:flex; gap:8px; flex-wrap:wrap; }
.pc-acts .pc-sec > * { flex:1 1 auto; justify-content:center; }
/* the Company Portfolio - a card each */
.pc-filters { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin:0 0 14px; }
.pc-filters .pc-sort { margin-left:auto; display:flex; align-items:center; gap:8px; font-size:12.5px; color:var(--text-muted); }
.pc-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(270px, 1fr)); gap:14px; }
.pc-co { display:flex; flex-direction:column; gap:12px; padding:15px 16px 14px; text-align:left; font:inherit; color:var(--text); cursor:pointer; width:100%; box-sizing:border-box; border:1px solid var(--border); }
.pc-co:hover { border-color:var(--primary); }
.pc-cot { display:flex; align-items:center; gap:11px; }
.pc-ini { width:38px; height:38px; border-radius:10px; background:var(--primary-light); color:var(--primary-ink); font-weight:800; font-size:13px; display:flex; align-items:center; justify-content:center; flex:none; }
.pc-con { flex:1 1 auto; min-width:0; }
.pc-con b { display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; font-size:14px; font-weight:700; line-height:1.3; }
.pc-con small { display:block; font-size:11.5px; color:var(--text-muted); }
.pc-mid { display:flex; align-items:center; gap:14px; }
.pc-ring circle.pc-rg { stroke:var(--tint-neutral-bg); }
.pc-ring text { fill:var(--text); font-weight:800; }
.pc-val b { display:block; font-size:22px; font-weight:800; letter-spacing:-.02em; font-variant-numeric:tabular-nums; }
.pc-val span { font-size:12px; color:var(--text-muted); }
.pc-meta { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:6px; border-top:1px solid var(--border); padding-top:10px; }
.pc-meta div { font-size:11px; color:var(--text-muted); font-weight:600; }
.pc-meta b { display:block; font-size:14px; color:var(--text); font-weight:700; font-variant-numeric:tabular-nums; }
.pc-foot { display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:12px; color:var(--text-muted); }
.pc-foot strong { color:var(--primary-ink); font-weight:700; }
/* the Companies register - its paid-until bar */
.pc-span { display:flex; flex-direction:column; gap:4px; min-width:150px; }
.pc-span small { font-size:11.5px; color:var(--text-muted); }
.pc-mini { position:relative; height:8px; width:150px; background:var(--tint-neutral-bg); border-radius:4px; overflow:hidden; }
.pc-mini > i { position:absolute; left:0; top:0; bottom:0; border-radius:4px; }
.pc-mini > i.pc-paid { background:var(--success); }
.pc-mini > i.pc-lapsed { width:100%; background:repeating-linear-gradient(135deg, var(--danger) 0 4px, var(--danger-light) 4px 8px); }
/* the Rate Card - its tabs, a line's price, the save bar that stays */
.pc-rc { display:flex; flex-wrap:wrap; gap:18px; align-items:flex-start; }
.pc-rc-left { flex:999 1 560px; min-width:0; display:flex; flex-direction:column; gap:12px; }
.pc-rc-right { flex:1 1 300px; max-width:100%; display:flex; flex-direction:column; gap:14px; }
.pc-tabs { display:flex; gap:4px; flex-wrap:wrap; border-bottom:1px solid var(--border); }
.pc-tab { border:0; background:transparent; font:inherit; font-size:13px; font-weight:600; color:var(--text-muted); padding:10px 12px; border-bottom:3px solid transparent; margin-bottom:-1px; cursor:pointer; display:inline-flex; align-items:center; gap:7px; }
.pc-tab:hover { color:var(--text); }
.pc-tab.on { color:var(--primary-ink); border-bottom-color:var(--primary); }
.pc-tab em { font-style:normal; font-size:11px; font-weight:800; background:var(--tint-neutral-bg); color:var(--text-muted); border-radius:999px; padding:1px 7px; }
.pc-tab.is-warn em { background:var(--tint-warn-bg); color:var(--tint-warn-fg); }
.pc-note { display:flex; gap:10px; align-items:flex-start; background:var(--tint-warn-bg); color:var(--tint-warn-fg); border-radius:var(--radius); padding:12px 14px; font-size:13px; line-height:1.5; }
.pc-line { display:grid; grid-template-columns:minmax(0, 1fr) 150px 90px; gap:14px; align-items:center; padding:12px 16px; border-top:1px solid var(--border); }
.pc-line:first-child { border-top:0; }
.pc-line b { display:block; font-size:13.5px; font-weight:600; color:var(--text); }
.pc-line small { display:block; font-size:12px; color:var(--text-muted); margin-top:2px; line-height:1.45; }
.pc-price { display:flex; align-items:center; border:1px solid var(--border); border-radius:var(--radius-sm); overflow:hidden; background:var(--card); }
.pc-price span { padding:0 9px; color:var(--text-muted); font-size:13px; background:var(--bg); align-self:stretch; display:flex; align-items:center; border-right:1px solid var(--border); }
.pc-price input { border:0; outline:0; width:100%; padding:8px 10px; font:inherit; font-size:14px; font-weight:700; font-variant-numeric:tabular-nums; color:var(--text); background:transparent; min-width:0; }
.pc-price.is-unset { border:1px dashed var(--warning); background:var(--tint-warn-bg); }
.pc-price.is-changed { border-color:var(--primary); box-shadow:0 0 0 3px var(--primary-light); }
.pc-bill { font-size:12px; color:var(--text-muted); }
.pc-kv { display:grid; grid-template-columns:minmax(0, 1fr) auto; gap:8px 12px; font-size:13px; }
.pc-kv dt { color:var(--text-muted); } .pc-kv dd { margin:0; font-weight:700; font-variant-numeric:tabular-nums; text-align:right; color:var(--text); }
.pc-state { display:flex; gap:10px; align-items:flex-start; padding:12px 0; font-size:12.5px; line-height:1.5; color:var(--text); border-top:1px solid var(--border); }
.pc-state:first-child { border-top:0; padding-top:0; }
.pc-dot { width:9px; height:9px; border-radius:50%; margin-top:5px; flex:none; }
.pc-dot.is-bad { background:var(--danger); } .pc-dot.is-warn { background:var(--warning); } .pc-dot.is-ok { background:var(--success); }
.pc-save { position:sticky; bottom:0; z-index:5; margin-top:18px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--el-2); padding:12px 16px; display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
.pc-save span { font-size:13px; color:var(--text-muted); flex:1 1 auto; }
.pl-table td { white-space:normal; vertical-align:top; }
.pl-table td.pl-when { white-space:nowrap; font-size:12.5px; }
.pl-table td.pl-when small { display:block; color:var(--text-muted); font-size:11.5px; margin-top:2px; }
.pl-table td.pl-det { font-size:12.5px; line-height:1.5; }
.pl-detbox { min-width:300px; max-width:560px; white-space:normal; overflow-wrap:anywhere; }
.pl-kv { display:inline-block; margin:0 16px 2px 0; color:var(--text); }
.pl-kv > span { color:var(--text-muted); font-weight:600; margin-right:6px; }
.pl-table td.pl-by { font-size:12.5px; color:var(--text-muted); min-width:120px; overflow-wrap:anywhere; }
/* a module not on the company's plan - shown, greyed, never a door (17-10) */
.nav-item.is-locked { opacity:.55; cursor:default; }
a.nav-item.is-locked { cursor:pointer; }
.nav-item.is-locked .nav-lock { margin-left:auto; display:inline-flex; color:var(--text-hint); }
.card.md-locked { border-style:dashed; background:repeating-linear-gradient(135deg, var(--card) 0 10px, var(--bg) 10px 20px); box-shadow:none; }
.md-locked .md-lockline { display:flex; align-items:center; gap:6px; font-size:12px; font-weight:700; color:var(--warning-dark); }

/* ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
   §47 · A TAB RAIL THAT SCROLLS HAS AN ARROW AT EACH END (19-10, Vijay: "build left right arrows in both ends this kind of
   scrolls.. its is becoming diffult to scroll when horizantal menu is bigg"). public/js/core/ui.js wraps every .ox-tabs2 in
   .ox-rail and shows an arrow only while there is more that way (.can-l / .can-r); a press moves the rail most of its width.
   The arrow is a solid round chip in the skin's own ink, so every skin draws it; it sits over the rail, never beside it, so a rail
   that fits looks exactly as before.
   ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.ox-rail { position:relative; min-width:0; max-width:100%; }
.ox-rail-arrow { position:absolute; top:0; bottom:1px; width:46px; display:none; align-items:center; justify-content:center;
  cursor:pointer; z-index:3; color:var(--bg); }
.ox-rail-arrow.l { left:0; }
.ox-rail-arrow.r { right:0; }
.ox-rail.can-l .ox-rail-arrow.l, .ox-rail.can-r .ox-rail-arrow.r { display:flex; }
/* solid in every skin (19-10, "increase those left right button sizes and reduce transparancy"): the skin's own ink as the
   chip and its page ground as the arrow - never a card colour, which is see-through on the glass skins */
.ox-rail-arrow svg { width:34px; height:34px; padding:7px; box-sizing:border-box; border-radius:50%; background:var(--text);
  stroke-width:2.6; box-shadow:0 2px 8px rgba(0,0,0,.28); transition:transform .12s, box-shadow .12s; }
.ox-rail-arrow:hover svg { transform:scale(1.08); box-shadow:0 3px 12px rgba(0,0,0,.34); }

/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
   §51 · PLANTX FACTORY LIVE'S PLATFORM CONSOLE (29-10, core/fl-console.js, public/js/platform/fl-console.js, docs/PLATFORM-CONSOLE.md
   §6, the approved preview KsbZnuS76siRJeRfMn8irg). Built on §50's console and the app's own surfaces (.card, .ox-stats, .data-table,
   .pc-need, .pc-legend) - what is its own is here, all of it under .fl-pc, every colour a token, so each skin draws it.
   ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.fl-pc .fl-gap { height:18px; }
.fl-pc .fl-stack { display:flex; flex-direction:column; gap:18px; min-width:0; }
.fl-pc .fl-two { align-items:start; }
.fl-pc .fl-n { color:var(--text-muted); font-weight:800; margin-left:2px; }
.fl-pc .fl-sub { display:block; font-size:12px; color:var(--text-muted); font-weight:400; margin-top:2px; line-height:1.45; }
.fl-pc .fl-muted { color:var(--text-muted); }
.fl-pc .fl-bad { color:var(--danger-dark); font-weight:600; }
.fl-pc small.fl-bad { display:block; font-size:12px; margin-top:3px; }
.fl-pc .fl-warn { color:var(--warning-dark); font-weight:600; }
.fl-pc .fl-ok { color:var(--success-dark); font-weight:600; }
.fl-pc .fl-nowrap { white-space:nowrap; }
.fl-pc .fl-pad { padding:10px 0; font-size:13px; }
.fl-pc .fl-ended td { opacity:.6; }
.fl-pc .fl-filters { padding:12px 14px; margin:0; border-bottom:1px solid var(--border); display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between; }
.fl-pc .fl-q { max-width:340px; }
.fl-pc .fl-foot { padding:12px 16px; font-size:12px; color:var(--text-muted); border-top:1px solid var(--border); }
.fl-pc .fl-mini { width:120px; margin-top:6px; }
.fl-pc .fl-count { font-weight:800; font-size:15px; font-variant-numeric:tabular-nums; }
/* a figure's share - its words, its number, its bar */
.fl-pc .fl-line { display:flex; flex-direction:column; gap:6px; margin:0 0 12px; }
.fl-pc .fl-line:last-child { margin-bottom:0; }
.fl-pc .fl-line > div:first-child { display:flex; justify-content:space-between; gap:10px; font-size:13px; align-items:baseline; }
.fl-pc .fl-line b { font-variant-numeric:tabular-nums; white-space:nowrap; }
.fl-pc .fl-track { height:8px; border-radius:999px; background:var(--tint-neutral-bg); overflow:hidden; }
.fl-pc .fl-track.fl-tall { height:12px; }
.fl-pc .fl-track > i { display:block; height:100%; border-radius:999px; background:var(--primary); }
.fl-pc .fl-track > i.fl-last { background:var(--success); }
/* one company's facts, Edges, rows, record */
.fl-pc .fl-facts { display:grid; grid-template-columns:repeat(auto-fit, minmax(120px, 1fr)); gap:10px; }
.fl-pc .fl-facts > div { border:1px solid var(--border); border-radius:10px; padding:10px 12px; background:var(--bg); }
.fl-pc .fl-facts b { display:block; font-size:20px; font-weight:800; font-variant-numeric:tabular-nums; }
.fl-pc .fl-facts small { font-size:12px; color:var(--text-muted); }
.fl-pc .fl-sec { font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--text-muted); margin:16px 0 8px; }
.fl-pc .fl-edge { display:flex; align-items:center; gap:10px; flex-wrap:wrap; border:1px solid var(--border); border-radius:10px; padding:9px 12px; margin-bottom:8px; font-size:13px; }
.fl-pc .fl-edge small { color:var(--text-muted); }
.fl-pc .fl-sp { flex:1 1 auto; }
.fl-pc .fl-dot { display:inline-block; width:9px; height:9px; border-radius:50%; margin-right:8px; background:var(--text-hint); vertical-align:middle; }
.fl-pc .fl-dot.fl-online { background:var(--success); }
.fl-pc .fl-dot.fl-late { background:var(--warning); }
.fl-pc .fl-dot.fl-quiet { background:var(--danger); }
.fl-pc .fl-row { display:flex; align-items:center; gap:12px; justify-content:space-between; flex-wrap:wrap; padding:12px 16px; border-top:1px solid var(--border); font-size:14px; }
.fl-pc .fl-row:first-child { border-top:0; }
.fl-pc .fl-row > div { display:flex; flex-direction:column; gap:2px; min-width:0; flex:1 1 220px; }
.fl-pc .fl-row small { font-size:12px; color:var(--text-muted); }
.fl-pc .fl-inv { font-size:13px; font-weight:600; color:var(--primary-ink, var(--primary)); }
.fl-pc .fl-kv { display:flex; justify-content:space-between; align-items:center; gap:12px; padding:8px 0; border-top:1px solid var(--border); font-size:14px; }
.fl-pc .fl-kv:first-child { border-top:0; }
.fl-pc .fl-rec { display:grid; grid-template-columns:86px 1fr; gap:10px; font-size:13px; padding:6px 0; line-height:1.5; }
.fl-pc .fl-rec > span:first-child { color:var(--text-muted); }
/* prices */
.fl-pc .fl-note { display:flex; gap:12px; align-items:flex-start; background:var(--tint-warn-bg); color:var(--tint-warn-fg); border-radius:var(--radius); padding:14px 18px; margin:0 0 18px; font-size:14px; line-height:1.55; }
.fl-pc .fl-note > span, .fl-pc .fl-note svg { width:20px; height:20px; flex:none; }
.fl-pc .fl-price { display:inline-flex; align-items:center; gap:6px; }
.fl-pc .fl-price > span { color:var(--text-muted); }
.fl-pc .fl-price .form-control { width:96px; }
.fl-pc .fl-num { width:80px; text-align:right; }
/* the newest Edge */
.fl-pc .fl-release { display:flex; align-items:center; gap:14px; flex-wrap:wrap; padding:16px 20px; margin:0 0 18px; }
.fl-pc .fl-release > div:nth-child(2) { flex:1 1 300px; }
.fl-pc .fl-rel-ic { width:44px; height:44px; border-radius:12px; background:var(--tint-info-bg); color:var(--tint-info-fg); display:flex; align-items:center; justify-content:center; }
.fl-pc .fl-rel-ic > span, .fl-pc .fl-rel-ic svg { width:22px; height:22px; }
.fl-pc .fl-sha { font-size:12px; background:var(--tint-neutral-bg); border-radius:6px; padding:3px 8px; }
/* consultants */
.fl-pc .fl-step { display:flex; gap:12px; align-items:flex-start; font-size:14px; line-height:1.5; margin:0 0 12px; }
.fl-pc .fl-step > span { flex:none; width:24px; height:24px; border-radius:50%; background:var(--tint-info-bg); color:var(--tint-info-fg); font-size:12px; font-weight:700; display:flex; align-items:center; justify-content:center; }
.fl-pc .fl-danger { color:var(--danger-dark); }
/* support - the questions and one thread */
.fl-pc .fl-qs { display:flex; flex-direction:column; }
.fl-pc .fl-q-item { font:inherit; text-align:left; border:0; border-top:1px solid var(--border); background:transparent; color:var(--text); padding:13px 18px; cursor:pointer; display:flex; flex-direction:column; gap:4px; }
.fl-pc .fl-q-item:first-child { border-top:0; }
.fl-pc .fl-q-item.on { background:var(--tint-info-bg); }
.fl-pc .fl-q-item small, .fl-pc .fl-q-top { font-size:12px; color:var(--text-muted); }
.fl-pc .fl-q-top { display:flex; justify-content:space-between; gap:10px; }
.fl-pc .fl-q-subj { margin:0 0 12px; font-size:16px; }
.fl-pc .fl-thread { display:flex; flex-direction:column; gap:12px; margin:0 0 16px; }
.fl-pc .fl-msg { max-width:86%; display:flex; flex-direction:column; gap:4px; }
.fl-pc .fl-msg > small { font-size:12px; color:var(--text-muted); }
.fl-pc .fl-msg > div { font-size:14px; line-height:1.55; padding:10px 14px; border-radius:12px; border:1px solid var(--border); white-space:pre-wrap; }
.fl-pc .fl-msg.fl-theirs { align-self:flex-start; }
.fl-pc .fl-msg.fl-ours { align-self:flex-end; }
.fl-pc .fl-msg.fl-ours > div { background:var(--tint-info-bg); border-color:transparent; }
.fl-pc .fl-btns { display:flex; gap:8px; justify-content:flex-end; flex-wrap:wrap; margin-top:10px; }
/* the side pages it opens (ui.drawer lives outside .fl-pc) */
.fl-how { font-size:14px; line-height:1.6; color:var(--text); margin:0 0 14px; }
.fl-pw { font-size:16px; font-weight:700; letter-spacing:.04em; }
.fl-form { display:flex; flex-direction:column; gap:6px; }
.fl-form .form-control { margin-bottom:8px; }
/* ★ Ask PlantX (public/js/live/ask-plantx.js) - the foot of a help topic and its side pages */
.ap-foot { display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap; margin:28px 0 4px; padding:14px 16px; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg); }
.ap-foot small { display:block; font-size:12.5px; color:var(--text-muted); margin-top:2px; }
.ap-form { display:flex; flex-direction:column; gap:6px; }
.ap-form .form-control { margin-bottom:8px; }
.ap-hint { display:block; font-size:12px; color:var(--text-muted); margin-top:6px; line-height:1.5; }
.ap-list { display:flex; flex-direction:column; }
.ap-item { font:inherit; color:var(--text); text-align:left; background:transparent; border:0; border-top:1px solid var(--border); padding:12px 2px; display:flex; align-items:center; justify-content:space-between; gap:12px; cursor:pointer; }
.ap-item:first-child { border-top:0; }
.ap-item small { display:block; font-size:12px; color:var(--text-muted); margin-top:2px; }
.ap-thread { display:flex; flex-direction:column; gap:12px; margin:0 0 16px; }
.ap-msg { max-width:88%; display:flex; flex-direction:column; gap:4px; }
.ap-msg > small { font-size:12px; color:var(--text-muted); }
.ap-msg > div { font-size:14px; line-height:1.55; padding:10px 14px; border-radius:12px; border:1px solid var(--border); white-space:pre-wrap; }
.ap-msg.ap-us { align-self:flex-end; }
.ap-msg.ap-them { align-self:flex-start; }
.ap-msg.ap-them > div { background:var(--tint-info-bg); border-color:transparent; }
/* OVERRIDES .data-table tbody td's nowrap (§ data tables) inside this console only: its words wrap so a long line never pushes a
   figure or an act out of sight; .fl-nowrap keeps a figure, a date or an act on one line */
.fl-pc .data-table tbody td { white-space:normal; }
.fl-pc .data-table tbody td.fl-nowrap { white-space:nowrap; }
.fl-pc .data-table td.fl-wrap { min-width:240px; }
/* Features And Plans (30-10): a group's own row in the one table of every feature, and a tick in a side page's form */
.fl-pc .data-table tbody tr.fl-grp td { background:var(--bg); color:var(--text-muted); font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; padding-top:10px; padding-bottom:6px; }
.fl-check { display:flex; gap:8px; align-items:center; font-size:13px; margin-bottom:8px; }
.fl-check input { accent-color:var(--primary); }
