/* ============================================================
   Clerim — canonical design tokens (appearance ONLY).
   This file + primitives.css are the design round-trip surface:
   restyling the product means editing these two files in Claude
   design and pasting them back (docs/design/ROUNDTRIP.md).
   Light mode is a DERIVED token set, never a filter. Default
   follows prefers-color-scheme; [data-theme] overrides per user.
   Ported from the v1.0 prototype's responsive/tokens.css.

   CONTRAST IS TESTED, NOT EYEBALLED. test/contrast.structural.test.ts
   composites every text and pill token over the ground it is actually
   painted on and fails below WCAG 2.1 AA (4.5:1 — --text-muted carries
   10–12px copy, so none of these get the large-text exemption). The
   light secondary/muted pair is OPAQUE on purpose: as translucent ink
   its ratio moved with whatever sat underneath, and on the page
   gradient's darkest stop it was 3.26:1 and 2.26:1 (GUI review,
   2026-08-12). Change a value here and the number moves — run the test.
   ============================================================ */

/* ---- the two reading fonts, self-hosted ---------------------------------
   Settings offers a low-vision face and a dyslexia face. Both are OFL and both
   are SERVED FROM HERE — the app downloads nothing at a shared desk, and
   `font-src 'self'` in the CSP (src/index.ts) would refuse a CDN anyway. The
   licences sit beside the files in web/assets/fonts/.

   A @font-face rule costs nothing until something is painted with the family,
   so the 235KB of OpenDyslexic is fetched only by the people who chose it.

   The urls are STYLESHEET-RELATIVE, not root-relative. A url() resolves against
   the stylesheet that contains it, and this file is copied verbatim to a second
   place: the public demo serves it from /livedemo/live/assets/tokens.css, where
   a root-relative /assets/fonts/... asks the marketing origin for a path that
   does not exist there and both faces fall back in silence. `fonts/...` is
   correct from both. Guarded by test/reading-fonts.structural.test.ts, which
   resolves the url against each copy on disk. */
@font-face {
  font-family: "Atkinson Hyperlegible"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("fonts/atkinson-hyperlegible-latin-400.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Atkinson Hyperlegible"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("fonts/atkinson-hyperlegible-latin-ext-400.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Atkinson Hyperlegible"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("fonts/atkinson-hyperlegible-latin-700.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Atkinson Hyperlegible"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("fonts/atkinson-hyperlegible-latin-ext-700.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "OpenDyslexic"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("fonts/opendyslexic-latin-400.woff2") format("woff2");
}
@font-face {
  font-family: "OpenDyslexic"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("fonts/opendyslexic-latin-700.woff2") format("woff2");
}

:root {
  color-scheme: dark;
  /* base surfaces */
  --page: #111318;
  --page-gradient: linear-gradient(145deg, #1A1D23 0%, #0F1318 30%, #1C2233 60%, #141820 100%);
  --surface-0: rgba(255,255,255,0.06);
  --surface-1: rgba(255,255,255,0.08);
  --surface-2: rgba(30,34,42,0.92);
  --field-bg: #23262E;
  --glass: rgba(255,255,255,0.10);
  --glass-strong: rgba(30,34,42,0.92);
  --glass-border: rgba(255,255,255,0.18);
  --glass-highlight: rgba(255,255,255,0.35);
  --border: rgba(255,255,255,0.10);
  --border-strong: rgba(255,255,255,0.16);
  --text: #F0EEE9;
  --text-secondary: rgba(240,238,233,0.7);
  --text-muted: rgba(240,238,233,0.60);
  /* semantic hues */
  --action: #6B9FD4;
  --action-hover: #85B2DE;
  --on-action: #0F1318;
  --action-tint: rgba(107,159,212,0.15);
  --action-tint-text: #9DC2E8;
  --ok-bg: rgba(94,168,120,0.15);   --ok-fg: #7DCFA0;
  --wait-bg: rgba(212,180,90,0.15); --wait-fg: #E0C96A;
  --warn-bg: rgba(212,110,100,0.15); --warn-fg: #EC958C;
  --violet-bg: rgba(200,168,232,0.15); --violet-fg: #C8A8E8; /* prototype violet — mockups lack it */
  --done: #6AAD7A;
  /* white-alpha ramp */
  --wa03: rgba(255,255,255,0.03);
  --wa04: rgba(255,255,255,0.04);
  --wa05: rgba(255,255,255,0.05);
  --wa06: rgba(255,255,255,0.06);
  --wa08: rgba(255,255,255,0.08);
  --wa10: rgba(255,255,255,0.10);
  --wa12: rgba(255,255,255,0.12);
  --wa14: rgba(255,255,255,0.14);
  --wa15: rgba(255,255,255,0.15);
  --wa18: rgba(255,255,255,0.18);
  --wa2: rgba(255,255,255,0.2);
  --wa5: rgba(255,255,255,0.5);
  /* ---- the count lens (chrome redesign 2026-08-13) ----------------------
     A count on a bell is not an alarm. The badge used to be a solid
     --status-warn disc with near-black numerals — red on a glyph that means
     "there are three of these", and an opaque hole punched in the icon
     underneath it. What replaces it is a frosted lens: two stops of the SAME
     translucency, running OPPOSITE the ground, with the numeral in --text.

     It cannot come off the alpha ramp. The ramp is re-derived per theme to
     sit ON the ground (white ink on dark, dark ink on light) and the lens has
     to do the reverse — darken a dark chrome, lighten a light one — so it is
     its own pair, stated per theme like every other derived value here. */
  --lens-1: rgba(6,8,12,0.62);
  --lens-2: rgba(6,8,12,0.30);
  --dot-core: #fff;
  /* type + radii */
  --font-body: -apple-system, "SF Pro Display", "SF Pro Text", ui-sans-serif, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: "SF Mono", ui-monospace, Menlo, monospace;
  --r-sm: 12px; --r-md: 16px; --r-lg: 20px; --r-xl: 24px; --r-pill: 999px;
  /* The reader's own text scale — Settings moves it on .app-shell; see the
     READING PREFERENCES block at the foot of this file. */
  --ts: 1;
  /* composition */
  /* The rail is icon-only (chrome redesign 2026-08-13) and, since the visual
     pass of 2026-09-03, wears the mockup geometry: a 44px button carrying a
     20px glyph in a 52px column, the name revealed on hover rather than set
     under the glyph. ONE definition, because shell.css spends --rail-w twice
     — on the rail itself and in the shell-body grid template — and a rail
     that is 52px wide inside a 72px column is a 20px gutter nobody asked
     for (the export root token says 72; VISUAL-INPUT-LOCK.md records it). */
  --rail-w: 52px;
  /* D34's ordinary four-column reading begins at the 720px pane step, so the
     designed outer pair must leave a real working set there. */
  --navigator-w: 240px;
  --record-w: 320px;
  /* THE READING MINIMUMS (founder, 2026-09-04). A column is never RENDERED
     narrower than its minimum: a drag stops here, and a drag PAST it folds
     the column to its recovery strip instead of wrapping its rows letter by
     letter. lib/columns.ts carries the same numbers and a test holds the two
     equal; the working set's own minimum is --workset-min below. */
  --navigator-min: 200px;
  --record-min: 280px;
  /* THE RECORD COLUMN FOLLOWS ITS TEMPLATE (ADR-0015 D49 Phase 2): the width
     column four is designed at for each of D3's templates, inside D12's
     500–700 band. The host names the section's template and the shell reads
     the width; a stored preference still wins. lib/recordTemplate.ts carries
     the same numbers and record-contract.structural holds them equal. */
  --record-w-list-detail: 500px;
  --record-w-queue-inspector: 560px;
  --record-w-editor-preview: 650px;
  --record-w-calendar-inspector: 560px;
  --record-w-settings: 500px;
  /* THE WORKING SET'S HARD FLOOR (founder, 2026-09-03). Column three never
     reads narrower than this; past it the shell scrolls sideways rather than
     folding the pane. Chat's floor is wider so its filter counters sit in two
     rows, never three. */
  --workset-min: 320px;
  --workset-min-chat: 560px;
  /* A FOLDED COLUMN's track: wide enough for the 34px control that brings it
     back, and no wider — a folded column is a way back, not a summary. */
  /* A collapsed column is its recovery control's reserved hit area. Density
     profiles below raise this with --tap so the target never spills into the
     neighbouring pane. */
  --strip-w: 44px;
  --col-max: 820px;
  --tap: 44px;
  /* ---- DENSITY (review item 9) ------------------------------------------
     The shell adapts to VIEWPORT via data-mode and, until now, never to ROLE:
     a valet at a car door and an administrator at a desk got identical control
     sizes and identical amounts of secondary prose. These four are what a role
     profile actually moves; shell.css overrides them per data-density.

     The values HERE are the office/default set, deliberately identical to what
     shipped before, so a surface with no density attribute renders exactly as
     it did. And no profile makes a target SMALLER than this: density earns its
     scanning speed from rhythm and prose size, never from shrinking something
     a thumb has to hit. */
  --row-pad-y: 8px;
  --row-gap: 6px;
  --t-row-sub: 10.5px;
  --t-row: 12.5px;
  --brand: #2E6BA6;
  /* ---- names the primitives layer reaches for, resolved here ------------
     These were USED and never DEFINED. `color: var(--text-primary)` with no
     fallback is not a soft failure: the declaration is invalid at
     computed-value time and the property unsets, so the rule simply does not
     exist. That is why `.dock-btn.alert` had no attention treatment at all —
     both its colour and its border resolved to nothing, and the Emergency
     button looked exactly like Chat and Report — and why the unread badge had
     no background to sit on. Aliases rather than a rename, so the ramp stays
     one set of values: a theme redefines --text and --warn-fg on the same
     element, and these follow. -- */
  --text-primary: var(--text);
  --accent: var(--action);
  --status-ok: var(--ok-fg);
  --status-wait: var(--wait-fg);
  --status-warn: var(--warn-fg);
  --hairline: var(--border);
  /* TINTED BORDERS — the accent mixed into the hairline, and the status tints
     on a callout. These were spelled inline in primitives.css, which was fine
     for CSS and silently wrong for the RN tree: react-native-web runs every
     colour-valued property through normalizeColor, whose isWebColor gate
     passes only `currentColor`, `inherit` and a value STARTING with `var(`.
     A `color-mix(...)` fails that gate, falls through to processColor, which
     cannot parse it and returns null — so normalizeColor returns undefined and
     the declaration is DROPPED. What is left is react-native-web's own reset,
     `border: 0 solid black`, so a converted card painted a 1px PURE BLACK
     border where a brand-tinted hairline belonged. Behind a token the value is
     a bare var() again, which the gate passes verbatim, and the native target
     gets one name to resolve instead of a CSS function it has no engine for.
     Same failure family as the aliases above, one layer out. */
  --brand-border: color-mix(in oklch, var(--brand) 34%, var(--border));
  --action-border: color-mix(in oklch, var(--action) 34%, var(--border));
  /* The adopted button's three exact blue-alpha values. They used to be
     repeated as literals in primitives.css; Button's RN conversion needs
     var()-shaped values so react-native-web's colour normalizer keeps them
     instead of dropping them to its black border reset. */
  --btn-primary-border: rgba(107,159,212,0.35);
  --btn-primary-highlight: rgba(107,159,212,0.15);
  --btn-primary-hover: rgba(107,159,212,0.28);
  --warn-border: color-mix(in srgb, var(--warn-fg) 35%, transparent);
  --ok-border: color-mix(in srgb, var(--ok-fg) 30%, transparent);
  --wait-border: color-mix(in srgb, var(--wait-fg) 30%, transparent);
  /* Cards LIFT off the page. The alpha ramp is re-derived on dark ink in light
     mode, which is right for borders and hovers and wrong for a card fill: a
     card painted with --wa06 sits DARKER than the ground it floats on, and
     light mode read as pale grey cards on a pale grey ground with the depth
     cue pointing backwards (GUI review 2026-08-12). One token, so each theme
     can say which direction "raised" is. */
  --surface-card: var(--wa06);
  /* prototype migration aliases — old names resolve to merged tokens */
  --c-ink: var(--text);
  --c-blue-fg: var(--action-tint-text);
  --c-amber-fg: var(--wait-fg);
  --c-green-fg: var(--ok-fg);
  --c-red-fg: var(--warn-fg);
  --c-violet-fg: var(--violet-fg);
  --c-brand: var(--brand);
  /* ---- design-system v2 additions (clerim-design skill, 2026-08-08):
     motion, blur/elevation, spacing + type scales, and the marketing
     vocabulary (marketing surfaces self-host their webfonts). ---- */
  --page-gradient-marketing: linear-gradient(162deg,#1A1D23 0%,#0F1318 27%,#161C2B 58%,#111520 100%);
  --glass-border-strong: rgba(255,255,255,0.26);
  --font-marketing: 'Hanken Grotesk',ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --font-marketing-mono: 'IBM Plex Mono',ui-monospace,"SF Mono",Menlo,Consolas,monospace;
  --t-display: 28px;
  --t-h2: 18px;
  --t-h3: 15px;
  --t-body: 13px;
  --t-small: 12px;
  --t-micro: 11px;
  --t-label: 11px;
  --tracking-heading: -0.02em;
  --tracking-label: 0.06em;
  --leading-body: 1.5;
  --t-hero: clamp(38px,5.2vw,64px);
  --t-section: clamp(28px,3.6vw,44px);
  --t-sub: clamp(26px,3.2vw,40px);
  --t-lead: 17px;
  --t-kicker: 12px;
  --tracking-hero: -0.04em;
  --tracking-section: -0.03em;
  --tracking-kicker: 0.14em;
  --r-marketing-card: 24px;
  --r-marketing-panel: 28px;
  --r-marketing-hero: 32px;
  --s-1: 6px;
  --s-2: 8px;
  --s-3: 10px;
  --s-4: 12px;
  --s-5: 14px;
  --s-6: 18px;
  --s-7: 24px;
  --marketing-max: 1180px;
  --blur-surface: blur(20px);
  --blur-card: blur(12px);
  --blur-overlay: blur(32px) saturate(200%);
  --elev-card: inset 0 1px 0 var(--wa06);
  --elev-overlay: inset 0 1px 0 var(--glass-highlight),0 24px 80px rgba(0,0,0,0.45);
  /* The auth card's own lift. A token rather than a literal in the rule
     because the RN tree has to mirror it and cannot: web/app is held to
     `no colour literal` by test/design-contract.structural.test.ts, which is
     the rule working — a shadow spelled twice is a shadow that drifts. Same
     reasoning as the tinted borders above. */
  --elev-auth: inset 0 1px 0 var(--glass-highlight),0 8px 32px rgba(0,0,0,0.22);
  /* The compact shell toast's lower lift, shared with its RN recipe. */
  --elev-toast: inset 0 1px 0 var(--glass-highlight),0 4px 14px rgba(0,0,0,0.25);
  /* The ink ON the brand square (the 24px auth mark). Deliberately not the
     on-action token, which follows the ACTION colour per theme and is
     near-black on dark; the mark sits on the brand colour in every theme and
     is always white. (Keep `--name` out of a comment followed by a colon —
     scripts/gen-theme.mjs matches declarations by text and reads one.) */
  --on-brand: #fff;
  --elev-marketing: inset 0 1px 0 rgba(255,255,255,0.30),0 24px 60px rgba(0,0,0,0.40);
  --ease: cubic-bezier(.5,0,.2,1);
  --dur-fast: .18s;
  --dur-pane: .18s;
}
[data-theme="light"] {
  color-scheme: light;
  --page: #F2F2F7;
  --page-gradient: linear-gradient(160deg, #FBFCFE 0%, #F2F3F7 45%, #EDEFF4 100%);
  --surface-0: rgba(255,255,255,0.70);
  --surface-1: rgba(255,255,255,0.88);
  --surface-2: #FFFFFF;
  --field-bg: #FFFFFF;
  --glass: rgba(255,255,255,0.72);
  --glass-strong: rgba(255,255,255,0.92);
  --glass-border: rgba(0,0,0,0.08);
  --glass-highlight: rgba(255,255,255,0.90);
  --border: rgba(0,0,0,0.08);
  --border-strong: rgba(0,0,0,0.14);
  --text: #1C1C1E;
  --text-secondary: #55555C;
  --text-muted: #676770;
  --action: #007AFF;
  --action-hover: #0A84FF;
  --on-action: #FFFFFF;
  --action-tint: rgba(0,122,255,0.12);
  --action-tint-text: #0063CC;
  --ok-bg: rgba(52,199,89,0.16);   --ok-fg: #1E7A34;
  --wait-bg: rgba(255,196,0,0.22); --wait-fg: #7A5800;
  --warn-bg: rgba(255,59,48,0.14); --warn-fg: #B22218;
  --violet-bg: rgba(91,74,138,0.12); --violet-fg: #5B4A8A;
  --done: #34C759;
  /* alpha ramp re-derived on dark ink */
  --wa03: rgba(0,0,0,0.03);
  --wa04: rgba(0,0,0,0.035);
  --wa05: rgba(0,0,0,0.04);
  --wa06: rgba(0,0,0,0.05);
  --wa08: rgba(0,0,0,0.06);
  --wa10: rgba(0,0,0,0.07);
  --wa12: rgba(0,0,0,0.08);
  --wa14: rgba(0,0,0,0.09);
  --wa15: rgba(0,0,0,0.09);
  --wa18: rgba(0,0,0,0.11);
  --wa2: rgba(0,0,0,0.12);
  --wa5: rgba(0,0,0,0.28);
  /* the count lens, running opposite a light ground */
  --lens-1: rgba(255,255,255,0.78);
  --lens-2: rgba(255,255,255,0.40);
  --dot-core: var(--text);
  /* Light mode raises a card toward white and separates the layers a little
     harder — pale on pale with a downward depth cue was the whole complaint. */
  --surface-card: rgba(255,255,255,0.86);
  --border: rgba(0,0,0,0.10);
  --border-strong: rgba(0,0,0,0.18);
  /* design-system v2 additions — light-derived values */
  --page-gradient-marketing: linear-gradient(160deg,#FBFCFE 0%,#F2F3F7 45%,#EDEFF4 100%);
  --glass-border-strong: rgba(0,0,0,0.14);
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]), [data-theme]:not([data-theme="dark"]) {
  color-scheme: light;
  --page: #F2F2F7;
  --page-gradient: linear-gradient(160deg, #FBFCFE 0%, #F2F3F7 45%, #EDEFF4 100%);
  --surface-0: rgba(255,255,255,0.70);
  --surface-1: rgba(255,255,255,0.88);
  --surface-2: #FFFFFF;
  --field-bg: #FFFFFF;
  --glass: rgba(255,255,255,0.72);
  --glass-strong: rgba(255,255,255,0.92);
  --glass-border: rgba(0,0,0,0.08);
  --glass-highlight: rgba(255,255,255,0.90);
  --border: rgba(0,0,0,0.08);
  --border-strong: rgba(0,0,0,0.14);
  --text: #1C1C1E;
  --text-secondary: #55555C;
  --text-muted: #676770;
  --action: #007AFF;
  --action-hover: #0A84FF;
  --on-action: #FFFFFF;
  --action-tint: rgba(0,122,255,0.12);
  --action-tint-text: #0063CC;
  --ok-bg: rgba(52,199,89,0.16);   --ok-fg: #1E7A34;
  --wait-bg: rgba(255,196,0,0.22); --wait-fg: #7A5800;
  --warn-bg: rgba(255,59,48,0.14); --warn-fg: #B22218;
  --violet-bg: rgba(91,74,138,0.12); --violet-fg: #5B4A8A;
  --done: #34C759;
  /* alpha ramp re-derived on dark ink */
  --wa03: rgba(0,0,0,0.03);
  --wa04: rgba(0,0,0,0.035);
  --wa05: rgba(0,0,0,0.04);
  --wa06: rgba(0,0,0,0.05);
  --wa08: rgba(0,0,0,0.06);
  --wa10: rgba(0,0,0,0.07);
  --wa12: rgba(0,0,0,0.08);
  --wa14: rgba(0,0,0,0.09);
  --wa15: rgba(0,0,0,0.09);
  --wa18: rgba(0,0,0,0.11);
  --wa2: rgba(0,0,0,0.12);
  --wa5: rgba(0,0,0,0.28);
  /* the count lens, running opposite a light ground */
  --lens-1: rgba(255,255,255,0.78);
  --lens-2: rgba(255,255,255,0.40);
  --dot-core: var(--text);
  --surface-card: rgba(255,255,255,0.86);
  --border: rgba(0,0,0,0.10);
  --border-strong: rgba(0,0,0,0.18);
  --page-gradient-marketing: linear-gradient(160deg,#FBFCFE 0%,#F2F3F7 45%,#EDEFF4 100%);
  --glass-border-strong: rgba(0,0,0,0.14);
  }
}
@media (prefers-color-scheme: light) {
  [data-theme="dark"] {
  color-scheme: dark;
  /* base surfaces */
  --page: #111318;
  --page-gradient: linear-gradient(145deg, #1A1D23 0%, #0F1318 30%, #1C2233 60%, #141820 100%);
  --surface-0: rgba(255,255,255,0.06);
  --surface-1: rgba(255,255,255,0.08);
  --surface-2: rgba(30,34,42,0.92);
  --field-bg: #23262E;
  --glass: rgba(255,255,255,0.10);
  --glass-strong: rgba(30,34,42,0.92);
  --glass-border: rgba(255,255,255,0.18);
  --glass-highlight: rgba(255,255,255,0.35);
  --border: rgba(255,255,255,0.10);
  --border-strong: rgba(255,255,255,0.16);
  --text: #F0EEE9;
  --text-secondary: rgba(240,238,233,0.7);
  --text-muted: rgba(240,238,233,0.60);
  /* semantic hues */
  --action: #6B9FD4;
  --action-hover: #85B2DE;
  --on-action: #0F1318;
  --action-tint: rgba(107,159,212,0.15);
  --action-tint-text: #9DC2E8;
  --ok-bg: rgba(94,168,120,0.15);   --ok-fg: #7DCFA0;
  --wait-bg: rgba(212,180,90,0.15); --wait-fg: #E0C96A;
  --warn-bg: rgba(212,110,100,0.15); --warn-fg: #EC958C;
  --violet-bg: rgba(200,168,232,0.15); --violet-fg: #C8A8E8; /* prototype violet — mockups lack it */
  --done: #6AAD7A;
  /* white-alpha ramp */
  --wa03: rgba(255,255,255,0.03);
  --wa04: rgba(255,255,255,0.04);
  --wa05: rgba(255,255,255,0.05);
  --wa06: rgba(255,255,255,0.06);
  --wa08: rgba(255,255,255,0.08);
  --wa10: rgba(255,255,255,0.10);
  --wa12: rgba(255,255,255,0.12);
  --wa14: rgba(255,255,255,0.14);
  --wa15: rgba(255,255,255,0.15);
  --wa18: rgba(255,255,255,0.18);
  --wa2: rgba(255,255,255,0.2);
  --wa5: rgba(255,255,255,0.5);
  /* the count lens, running opposite a dark ground */
  --lens-1: rgba(6,8,12,0.62);
  --lens-2: rgba(6,8,12,0.30);
  --dot-core: #fff;
  }
}

/* ═══ ROLE DENSITY (review item 9) ═══════════════════════════════════════════
   The shell has always adapted to the VIEWPORT — data-mode desktop/tablet/mobile,
   measured off its own width. It never adapted to the ROLE, so a valet holding a
   phone at a car door and an administrator at a desk got the same control sizes
   and the same amount of secondary prose. One of those people is standing up.

   Three profiles, not four. The review named four groups, but the technician's
   distinguishing need — current job, next action, evidence capture — is a
   STRUCTURE, not a density, and pretending otherwise would ship a fourth profile
   that differs only cosmetically. Technicians take the field profile; the
   job-first structure is recorded in Doctrine §2.5 as its own work.

   INVARIANT: no profile makes a target smaller than the 44px default. Density
   buys scanning speed from rhythm and type, never from shrinking something a
   thumb has to hit — that trade would be an accessibility regression wearing a
   product decision's clothes. test/density.structural.test.ts holds the line. */
/* The nav rail's FULL mode (ADR-0015 D2): the same rail, wide enough to lay
   its labels out beside the glyphs instead of floating them. A token value
   conditional on a shell attribute, which is why it lives here beside the
   density profiles rather than in shell.css — this file is the only definer.
   Everything that reads --rail-w follows for free: the grid template, and the
   column handles, whose offsets and room arithmetic are computed from it. */
.app-shell[data-nav-mode="full"] {
  /* 250px is the requested maximum. Between 800px and 850px the rail gives
     back exactly the space the four-column minimum needs, preserving the
     former 200px fit at the narrow end without exceeding 250px when wide. */
  --rail-w: clamp(200px, calc(100% - 600px), 250px);
}
.app-shell[data-density="office"] {
  /* Mouse and keyboard, many rows at once, reading before acting. */
  --tap: 44px;
  --strip-w: 44px;
  --row-pad-y: 7px;
  --row-gap: 5px;
  --t-row-sub: 10.5px;
}
.app-shell[data-density="field"] {
  /* Standing, moving, often one-handed, sometimes gloved. */
  --tap: 52px;
  --strip-w: 52px;
  --row-pad-y: 13px;
  --row-gap: 8px;
  --t-row: 13px;
  --t-row-sub: 11.5px;
}
.app-shell[data-density="resident"] {
  /* Not staff, not trained, here occasionally. Fewer things, said larger. */
  --tap: 50px;
  --strip-w: 50px;
  --row-pad-y: 14px;
  --row-gap: 10px;
  --t-row: 13.5px;
  --t-row-sub: 12px;
}

/* ═══ READING PREFERENCES (chrome redesign 2026-08-13) ═══════════════════════
   Density is chosen FOR a person by the role they hold. These two are chosen
   BY them, in Settings, and they are device preferences rather than building
   facts — localStorage beside the theme, not events (see initSettings in
   clerim.js, and BACKLOG for the person-scoped version if it is ever wanted).

   ONE multiplier carries the text scale. Every literal type size in
   primitives.css and shell.css is written calc(<the same literal> * var(--ts)),
   so a screen at "Larger" is the same design at a different size rather than a
   second layout — which is what a per-size stylesheet always becomes.

   The default lives on :root rather than on .app-shell, because the auth pages
   have no shell: an undefined --ts makes every one of those calc()s invalid at
   computed-value time, and a font-size that unsets does not fall back to the
   literal — it inherits, and the login screen loses its type scale entirely. */
.app-shell[data-text-scale="sm"] { --ts: 0.9; }
.app-shell[data-text-scale="lg"] { --ts: 1.15; }
.app-shell[data-text-scale="xl"] { --ts: 1.3; }

/* Two faces, both OFL, both served from web/assets/fonts (see the @font-face
   block at the top of this file). Hyperlegible separates the letters that look
   alike; OpenDyslexic weights their bottoms so they cannot flip. */
.app-shell[data-reading-font="hyperlegible"] {
  --font-body: "Atkinson Hyperlegible", Verdana, Tahoma, sans-serif;
}
.app-shell[data-reading-font="dyslexic"] {
  --font-body: "OpenDyslexic", Verdana, Tahoma, sans-serif;
  /* OpenDyslexic runs large. Without this a row grows by a line at every scale
     and the density profile above stops meaning anything. */
  --ts: 0.92;
}
/* Stated per combination because the two selectors above tie on specificity,
   and a tie is settled by source order — which would make "Larger" mean
   nothing at all in the dyslexic face. */
.app-shell[data-reading-font="dyslexic"][data-text-scale="sm"] { --ts: calc(0.9 * 0.92); }
.app-shell[data-reading-font="dyslexic"][data-text-scale="lg"] { --ts: calc(1.15 * 0.92); }
.app-shell[data-reading-font="dyslexic"][data-text-scale="xl"] { --ts: calc(1.3 * 0.92); }
