/* ============================================================
   RaizIntel — Shared Brand Styles
   ============================================================ */

@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&family=Source+Sans+3:wght@300;400;600;700&display=swap');

/* ── RESET & BASE ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --black:#0C0B0A;
  --near-black:#14120F;
  --dark:#1C1914;
  --dark-mid:#24201A;
  --mid:          #3a3a3a;
  --muted:        #666666;
  --border:#2F2A21;
  --border-light: #e0e0e0;
  --surface:#17140F;
  --surface-2:#1C1914;
  --surface-3:#24201A;


  --white:        #ffffff;
  --off-white:    #f4f4f4;
  --text-primary:#F6F1E7;
  --text-secondary:#B8AE9C;
  /* S0c: raised from #6a6a6a (3.35:1-class, the punchlist "lettering gets
     missed" tier) to the computed AA-clearing minimum with margin —
     4.62:1 on the worst surface, still below --text-secondary (hierarchy). */
  --text-dim:#9D937F;

  --success:      #2d7a3a;
  --warning:#F2A47A;
  --error:        #c41e1e;
  --info:         #1e5fa0;

  --font-display: 'Oswald', 'Arial Narrow', Arial, sans-serif;
  --font-body:    'Source Sans 3', 'Segoe UI', Arial, sans-serif;

  /* LEGACY radius trio — REPOINTED at the semantic scale (E-81 S1, the app-wide
     rollout the premium pass deferred). These three are SIZE-named (sm/default/lg);
     the scale below is ROLE-named (control/surface/overlay), so the translation is
     by what each one actually styles, measured before it was made:

       --radius-sm  -> control   .btn, every input/select/textarea, .alert,
                                 .toggle-btn, .brand-badge, .btn-logout, .section-num
       --radius     -> surface   measured across 69 call sites it is overwhelmingly
                                 boxes and panels — .panel-body, .exhibit-area,
                                 .cert-block, .modal-box, .upload-zone, .file-item
       --radius-lg  -> surface   the large containers it already meant

     Relative hierarchy is preserved (sm < default = lg). The trio is kept, not
     deleted, because 39 files still read it; it retires when the last one stops.
     Nothing may re-freeze these to a literal — a per-screen override would rebuild
     the exact sprawl this retires. A screen that reads bulbous at 10px demotes the
     ELEMENT to --radius-control in its own wave; it does not re-pin the token. */
  --radius:       var(--radius-surface);
  --radius-sm:    var(--radius-control);
  --radius-lg:    var(--radius-surface);

  /* SEMANTIC radius scale (premium pass, owner direction A). Named by ROLE, not by
     size, so a surface picks the one that matches what it is. Used by the case area
     and by every shared component built from here on (RaizModal). */
  --radius-control: 6px;    /* buttons, inputs, chips, badges */
  --radius-surface: 10px;   /* cards, panels, list containers */
  --radius-overlay: 14px;   /* modals, popovers */
  --radius-pill:    999px;

  --shadow:       0 2px 12px rgba(0,0,0,0.5);
  --shadow-heavy: 0 4px 24px rgba(0,0,0,0.7);

  --transition:   0.18s ease;

  /* ── SEMANTIC ROLE TOKENS (theme campaign S0a) ──────────────────────────
     Role-named tokens are the themable surface: the html[data-theme] blocks
     below override THESE (plus the pre-existing role-named tokens above:
     surface / border / the text tiers / success/warning/error/info / shadow),
     never the
     value-named legacy tokens (--black/--near-black/--dark/--mid/--red...),
     which keep today's dark values for pages not yet swept onto roles.
     These :root fallbacks are byte-equal to today's dark rendering; the
     theme-contrast-contract test pins :root == [data-theme="dark"]. */
  /* ▶ THE NATIVE PICKERS FOLLOW THE THEME (2026-09-24). color-scheme is
     inherited, so declaring it beside the theme's own tokens makes the browser
     draw the date calendar, the time wheel, the segment highlight and the
     scrollbars in the same scheme as the page. Before this it was declared in
     exactly ONE file in the product — audit/index.html, for its toolbar — so
     every other dark screen opened a WHITE calendar over a black page. */
  color-scheme: dark;
  --bg:#0C0B0A;
  --surface-raised:#14120F;
  --surface-inset:#1C1914;
  /* S10 dark tune: raised #3a3a3a -> #6f6f6f — clears the 3:1 non-text floor on
     every surface (worst 3.09 on surface-3) while keeping white chip text at
     5.0:1. Deletes the six border-strong contract pins. */
  --border-strong: #776D5C;
  /* S2 row-separator hairline (table rows, list rows), softer than the plain
     border token. Decorative, so it sits outside the contract's tested matrix
     by design. (No token-name-plus-colon text in comments — the contract's
     parser reads declarations without stripping comments.) */
  --border-soft:rgba(201,164,92,0.10);
  --text-bright:#FFFFFF;
  --text-on-brand:#17140F;
  /* S10 dark tune: brand raised #c41e1e -> #d62828 — the minimum lift that
     clears the 3:1 accent floor on every surface (worst 3.10 on surface-3);
     hue unchanged, still the brand red. Deletes the six brand contract pins.
     Fills keep white text at 5.0:1. Brand TINTS below deliberately keep the
     deeper base (richer veils; tints are not gated). */
  --brand:#C9A45C;
  --brand-strong:#B08C48;
  --brand-soft:rgba(201,164,92,0.14);
  --status-ok-fg:      #7dcf8a;
  --status-ok-bg:      rgba(45,122,58,0.18);
  --status-ok-border:  rgba(45,122,58,0.5);
  --status-warn-fg:#F2A47A;
  --status-warn-bg:rgba(154,52,18,0.20);
  --status-warn-border:rgba(242,164,122,0.45);
  --status-error-fg:   #e87a7a;
  --status-error-bg:   rgba(196,30,30,0.18);
  --status-error-border: rgba(196,30,30,0.5);
  --status-info-fg:    #7ab4e8;
  --status-info-bg:    rgba(30,95,160,0.18);
  --status-info-border: rgba(30,95,160,0.45);
  --status-neutral-fg: #aaaaaa;
  --status-neutral-bg: rgba(106,106,106,0.18);
  /* S3: fifth categorical accent — derived/special record classes (supplement
     report type, visibility events). Purple, distinct from the four status hues. */
  --status-special-fg: #c39fe0;
  --status-special-bg: rgba(150,90,200,0.10);
  --status-special-border: rgba(150,90,200,0.45);
  --veil-hover:        rgba(255,255,255,0.03);
  --veil-selected:rgba(201,164,92,0.10);
  --overlay:           rgba(0,0,0,0.6);
  --overlay-heavy:     rgba(0,0,0,0.75);
  /* Theme-INVARIANT (owner ruling): agency logos always render on a white chip. */
  --logo-surface:      #ffffff;
  /* Was a phantom (referenced by 3 .mono utilities, never defined). Pinned to the
     previous effective fallback so nothing moves; promoting it to Courier is an
     S10 decision, not a sweep side effect. */
  --font-mono:         monospace;
  /* Type ramp — the Q4 "do not migrate" hold was LIFTED by the owner for the
     case-area facelift campaign, which is this ramp's intended consumer. */
  --fs-2xs: 0.62rem; --fs-xs: 0.7rem; --fs-sm: 0.78rem; --fs-base: 0.85rem;
  --fs-md: 0.95rem; --fs-lg: 1.1rem; --fs-xl: 1.5rem; --fs-2xl: 2rem;

  /* ══ PREMIUM PASS — the craft tokens (owner direction A, "quiet instrument") ══
     The diagnosis these answer, measured on the case page before this campaign:
       · 22 distinct spacing values  · 24 filled-red buttons  · 2 elevation steps
       · 4px radius  · one 0.18s duration driving `transition: all`
     Space, depth and motion were hand-picked per element; that is what read as
     "cheap". These are the system. Extend them — never invent a per-screen value. */

  /* SPACE — one 8pt rhythm. 22 hand-picked values collapse onto these 8. */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;

  /* MOTION — three speeds, three curves. Enter decelerates, leave accelerates.
     Never `transition: all` (it animates layout properties too). */
  --dur-fast: 120ms; --dur-base: 180ms; --dur-slow: 240ms;
  --ease-standard:   cubic-bezier(.2, 0, 0, 1);
  --ease-decelerate: cubic-bezier(0, 0, 0, 1);
  --ease-accelerate: cubic-bezier(.3, 0, 1, 1);

  /* RHYTHM + FOCUS. Uppercase tracking is for TITLES and BADGES only now —
     sentence-case field labels carry no tracking (owner CP2a). */
  --lh-tight: 1.25; --lh-base: 1.5; --lh-relaxed: 1.65;
  --tracking-caps: 0.08em;
  /* ▶ FOCUS IS NOT AN ERROR (owner-ruled 2026-09-22; he flagged the same thing
     on signup before). The ring was --brand, and --brand is #d62828 — the same
     red as a validation failure — so every focused field looked like a field
     somebody had got wrong. Blue is the near-universal focus convention and
     cannot be read as a refusal, which is the whole requirement here. */
  --focus-accent:#C9A45C;
  --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--focus-accent);
  /* Same ring, for a control sitting on a RAISED surface (the sticky top bar) rather
     than on the page. The first layer is a GAP that must match the backdrop it is
     drawn on, or it stops reading as empty space and becomes a visible band —
     --bg on --surface-raised differs in BOTH themes. Reach for this whenever the
     focused control's backdrop is not --bg. */
  --focus-ring-raised: 0 0 0 2px var(--surface-raised), 0 0 0 4px var(--focus-accent);

  /* ELEVATION — five steps, two-layer (tight contact + soft ambient). This is the
     replacement for "every card wears the same 1px border": depth carries the
     hierarchy, so resting cards can drop their borders entirely.
     The :root fallbacks below are the DARK set; light gets its own far softer set
     in the theme block (dark-theme shadows on a light surface are the classic
     cheap tell — they read as grey smudge, not depth). */
  --elev-0: none;
  --elev-1: 0 1px 2px rgba(0,0,0,.28);
  --elev-2: 0 2px 6px rgba(0,0,0,.30), 0 1px 2px rgba(0,0,0,.22);
  --elev-3: 0 8px 24px rgba(0,0,0,.38), 0 2px 6px rgba(0,0,0,.26);
  --elev-4: 0 24px 64px rgba(0,0,0,.48), 0 8px 20px rgba(0,0,0,.28);

  /* A separator that is felt, not seen. Row rules and divider gaps use this
     instead of a full-strength --border line. */
  --hairline:rgba(246,241,231,0.08);

  /* Z-INDEX SCALE — retires the ad-hoc 200/300/310/400/500/600 sprawl
     (findings-ledger entry 22). Every layered surface picks from here. */
  --z-sticky: 50; --z-header: 100; --z-drawer: 300;
  --z-modal-backdrop: 500; --z-modal: 501;
  /* A confirm launched FROM a modal (evidence transition, correction, visibility…) must
     sit above it. Without this the legacy overlays — which sit at 310 and are hidden by
     opacity, not display — resolve in the DOM but are unclickable behind the backdrop. */
  --z-modal-nested: 600;
  --z-toast: 700;
  /* black and gold (2026-10-26) */
  --danger: #C41E1E; --danger-strong: #9B1818; --danger-soft: rgba(196,30,30,0.18); --action-bg: #C9A45C; --action-fg: #17140F; --action-hover: #D6B472;
  /* attention: needs an eye, not a warning (gold) */
  --status-attn-fg: #C9A45C; --status-attn-bg: rgba(201,164,92,0.14); --status-attn-border: rgba(201,164,92,0.45);

}

/* ── THEMES ────────────────────────────────────────────────────────────────
   One name, two value sets. The boot snippet stamps data-theme on <html>;
   pages without the snippet (not yet swept) get NO attribute and render from
   the :root fallbacks — i.e. exactly today's dark app. The contrast-contract
   test (tests/backup/theme-contrast-contract.test.js) enforces AA floors on
   both blocks and :root==dark parity; edit values only with it green. */
:root[data-theme="dark"] {
  --bg:#0C0B0A; --surface-raised:#14120F; --surface:#17140F;
  --surface-inset:#1C1914; --surface-2:#1C1914; --surface-3:#24201A;
  --border:#2F2A21; --border-strong: #776D5C; --border-soft:rgba(201,164,92,0.10);
  --text-primary:#F6F1E7; --text-secondary:#B8AE9C; --text-dim:#9D937F;
  --text-bright:#FFFFFF; --text-on-brand:#17140F;
  --brand:#C9A45C; --brand-strong:#B08C48; --brand-soft:rgba(201,164,92,0.14);
  --success: #2d7a3a; --warning:#F2A47A; --error: #c41e1e; --info: #1e5fa0;
  --status-ok-fg: #7dcf8a; --status-ok-bg: rgba(45,122,58,0.18); --status-ok-border: rgba(45,122,58,0.5);
  --status-warn-fg:#F2A47A; --status-warn-bg:rgba(154,52,18,0.20); --status-warn-border:rgba(242,164,122,0.45);
  --status-error-fg: #e87a7a; --status-error-bg: rgba(196,30,30,0.18); --status-error-border: rgba(196,30,30,0.5);
  --status-info-fg: #7ab4e8; --status-info-bg: rgba(30,95,160,0.18); --status-info-border: rgba(30,95,160,0.45);
  --status-neutral-fg: #aaaaaa; --status-neutral-bg: rgba(106,106,106,0.18);
  --status-special-fg: #c39fe0; --status-special-bg: rgba(150,90,200,0.10); --status-special-border: rgba(150,90,200,0.45);
  --veil-hover: rgba(255,255,255,0.03); --veil-selected:rgba(201,164,92,0.10);
  --overlay: rgba(0,0,0,0.6); --overlay-heavy: rgba(0,0,0,0.75);
  --shadow: 0 2px 12px rgba(0,0,0,0.5); --shadow-heavy: 0 4px 24px rgba(0,0,0,0.7);
  --logo-surface: #ffffff;
  /* Premium pass — dark elevation (byte-equal to the :root fallbacks, per the
     contract's :root == [data-theme=dark] parity pin). */
  --elev-1: 0 1px 2px rgba(0,0,0,.28);
  --elev-2: 0 2px 6px rgba(0,0,0,.30), 0 1px 2px rgba(0,0,0,.22);
  --elev-3: 0 8px 24px rgba(0,0,0,.38), 0 2px 6px rgba(0,0,0,.26);
  --elev-4: 0 24px 64px rgba(0,0,0,.48), 0 8px 20px rgba(0,0,0,.28);
  --hairline:rgba(246,241,231,0.08);
  /* black and gold (2026-10-26) */
  --focus-accent: #C9A45C; --danger: #C41E1E; --danger-strong: #9B1818; --danger-soft: rgba(196,30,30,0.18); --action-bg: #C9A45C; --action-fg: #17140F; --action-hover: #D6B472;
  /* attention: needs an eye, not a warning (gold) */
  --status-attn-fg: #C9A45C; --status-attn-bg: rgba(201,164,92,0.14); --status-attn-border: rgba(201,164,92,0.45);
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg:#FBF8F2; --surface-raised:#FFFFFF; --surface:#FFFDF8;
  --surface-inset:#F3EEE4; --surface-2:#FFFFFF; --surface-3:#EFE9DD;
  --border:#E3DBCA; --border-strong:#8C826F; --border-soft:rgba(23,20,15,0.10);
  --text-primary:#17140F; --text-secondary:#4A443A; --text-dim:#5C5548;
  --text-bright:#0C0B0A; --text-on-brand:#FFFFFF;
  --brand:#7E5F27; --brand-strong:#664D1F; --brand-soft:rgba(133,101,42,0.10);
  --success: #1e6b34; --warning:#9A3412; --error: #b31b1b; --info: #14589c;
  --status-ok-fg: #1e6b34; --status-ok-bg: rgba(30,107,52,0.10); --status-ok-border: rgba(30,107,52,0.5);
  --status-warn-fg:#9A3412; --status-warn-bg:rgba(154,52,18,0.08); --status-warn-border:rgba(154,52,18,0.45);
  --status-error-fg: #a52121; --status-error-bg: rgba(179,27,27,0.08); --status-error-border: rgba(179,27,27,0.5);
  --status-info-fg: #14589c; --status-info-bg: rgba(20,88,156,0.10); --status-info-border: rgba(20,88,156,0.45);
  --status-neutral-fg: #595959; --status-neutral-bg: rgba(89,89,89,0.10);
  --status-special-fg: #6b3fa0; --status-special-bg: rgba(107,63,160,0.08); --status-special-border: rgba(107,63,160,0.45);
  --veil-hover: rgba(0,0,0,0.04); --veil-selected:rgba(201,164,92,0.16);
  --overlay: rgba(0,0,0,0.5); --overlay-heavy: rgba(0,0,0,0.6);
  --shadow: 0 1px 6px rgba(0,0,0,0.12); --shadow-heavy: 0 4px 18px rgba(0,0,0,0.18);
  --logo-surface: #ffffff;
  /* Premium pass — LIGHT elevation. A separate, far softer set on purpose: reusing
     the dark shadows on a light surface is the classic cheap tell (they read as a
     grey smudge rather than depth). Lower alphas, tighter ambient blur, and a faint
     warm-neutral cast that matches the light surface family. */
  --elev-1: 0 1px 2px rgba(27,27,27,.06);
  --elev-2: 0 1px 3px rgba(27,27,27,.08), 0 1px 2px rgba(27,27,27,.05);
  --elev-3: 0 6px 16px rgba(27,27,27,.10), 0 2px 4px rgba(27,27,27,.06);
  --elev-4: 0 20px 48px rgba(27,27,27,.16), 0 6px 14px rgba(27,27,27,.08);
  --hairline:rgba(23,20,15,0.09);
  /* black and gold (2026-10-26) */
  --focus-accent: #7E5F27; --danger: #B31B1B; --danger-strong: #8F1414; --danger-soft: rgba(179,27,27,0.08); --action-bg: #17140F; --action-fg: #D9BC7E; --action-hover: #000000;
  /* attention: needs an eye, not a warning (gold) */
  --status-attn-fg: #775A25; --status-attn-bg: rgba(133,101,42,0.10); --status-attn-border: rgba(133,101,42,0.45);
}

html {
  font-size: 16px;
  -webkit-tap-highlight-color: transparent;
}

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text-primary);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

/* ── TYPOGRAPHY ── */
h1, h2, h3, h4, h5 {
  font-family: var(--font-display);
  letter-spacing: 0.04em;
  line-height: 1.1;
}

/* ── PORTAL TOPBAR ── */
.portal-topbar {
  background: var(--surface-raised);
  border-bottom: 2px solid var(--brand);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px;
  height: 52px;
  position: sticky;
  top: 0;
  z-index: 100;
}

.portal-topbar .brand {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}

.portal-topbar .brand-wordmark {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text-bright);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* 0e-5 FOUC guard — the static pages ship a baked-in "RaizIntel" wordmark, but the
   real per-agency name is painted by RaizNav.render() only AFTER the session resolves.
   This rule (loaded in <head>, so it applies at first paint, before nav.js runs) hides
   the wordmark until render() sets the correct text and adds .rms-wordmark-ready — so a
   paying agency NEVER sees the platform name flash over its own branding. `visibility`
   (not display) reserves the layout box, so there is no shift when the name appears.
   The brand "RMS" badge is unaffected. Operator + blank-name fallbacks still resolve to
   "RaizIntel" (that is the correct answer for those, not a flicker). */
.portal-topbar .brand-wordmark { visibility: hidden; }
.portal-topbar .brand-wordmark.rms-wordmark-ready { visibility: visible; }

/* THE REPORT HEADING FOUC GUARD — the same idiom, for the same reason, and the
   owner ruled the same way about it. The report editor cannot know which type it
   is opening until the row and the type registry have both resolved, so anything
   painted before then is a guess. It used to guess "Surveillance Report" — from a
   baked-in literal AND a hard-coded labelFor('surveillance') at shell-init — and
   then correct itself a second or two later once the real type arrived.

   ▶ A SHORTER FLASH WAS EXPLICITLY NOT THE FIX. The app must not show one thing
   and then swap it: the same principle that made the form locked-by-default
   rather than dimmed-then-swapped, and made report views open straight to the
   exhibit with no intermediate screen. On a court-facing product a page that
   corrects itself reads as a mistake even when the end state is right.

   So the heading is INVISIBLE until setReportHeading() paints a resolved value
   and adds .rms-heading-ready. `visibility` (not `display`) reserves the layout
   box, so revealing it shifts nothing. The markup literal is now the neutral
   "Report" — true of every type — so even the failure path, which reveals
   deliberately rather than hanging blank, cannot show something wrong. */
#reportPageTitle { visibility: hidden; }
#reportPageTitle.rms-heading-ready { visibility: visible; }

.portal-topbar .brand-badge {
  font-size: 0.6rem;
  font-weight: 700;
  background: var(--brand);
  color: var(--text-on-brand);
  padding: 2px 7px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border-radius: var(--radius-sm);
}

.portal-topbar .topbar-right {
  display: flex;
  align-items: center;
  gap: 12px;
}

.portal-topbar .investigator-name {
  font-size: 0.8rem;
  color: var(--text-secondary);
  font-weight: 600;
}

.btn-logout {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--text-dim);
  background: transparent;
  border: 1px solid var(--border);
  padding: 5px 12px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}

.btn-logout:hover {
  color: var(--status-error-fg);
  border-color: var(--brand);
}

/* ── SIDEBAR NAV (canonical / shared) ──
   Single source for the RaizNav sidebar appearance so every internal page
   renders identical gray nav links (NON-NEGOTIABLE UI RULE 11 — Consistent
   Layout). Copied verbatim from public/home/index.html. Pages that still carry
   their own inline .nav-item block keep it (identical values; their inline copy
   wins by cascade order) — this default reaches pages that omitted it, which
   previously fell back to browser-default blue links. */

/* ▶ AND THE RAIL ITSELF, WHICH WAS LEFT BEHIND BY THAT VERY SWEEP (owner walk,
   2026-10-18). Everything INSIDE the rail lived here — .nav-item, the section
   label, the footer — and the container it all sits in did not. So `.sidebar` was
   copied by hand into 32 pages, identically, and SIX pages never got it:
   views/help/section.html (the one he saw) and operator activity, platform,
   purged, twins and operator-account.

   What that looks like is exactly what he reported: with no background of its own
   the rail is transparent, so the page's darker body colour shows through and the
   Help rail reads grey-brown where every other rail reads white. It is not a
   colour that was chosen wrongly — it is a colour that was never set, which is why
   no theme or token could have been at fault, and why it is wrong in BOTH modes.
   The same omission also left the rail visible at phone width on those six, beside
   the drawer nav.js puts in the top bar, so the media rule belongs here too.

   The 32 inline copies stay and still win by cascade order; they hold the same
   values. This reaches the pages that omitted it — the same sentence as above, one
   element out. */
.sidebar {
  width: 220px; min-width: 220px;
  background: var(--surface-raised);
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column;
  padding-top: var(--sp-2);
}
@media (max-width: 768px) { .sidebar { display: none; } }

.nav-section-label {
  font-family: var(--font-display); font-size: 0.62rem; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-dim); padding: 16px 16px 6px;
}
.nav-item {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 16px; font-size: 0.85rem; font-weight: 600;
  color: var(--text-secondary); cursor: pointer;
  border-left: 3px solid transparent;
  transition: color var(--dur-fast) var(--ease-standard), background var(--dur-fast) var(--ease-standard), border-left-color var(--dur-fast) var(--ease-standard); text-decoration: none; user-select: none;
}
.nav-item:hover { color: var(--text-primary); background: var(--veil-hover); }
.nav-item.active { color: var(--text-bright); border-left-color: var(--brand); background: var(--veil-selected); }
.nav-item .nav-icon { font-size: 0.8rem; width: 16px; text-align: center; opacity: 0.7; }
.sidebar-footer { margin-top: auto; padding: 12px 16px; border-top: 1px solid var(--border); }

/* ── FORM COMPONENTS ── */
.form-section {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 12px;
  overflow: hidden;
}

.form-section-header {
  background: var(--surface-inset);
  border-bottom: 1px solid var(--border);
  padding: 10px 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}

.form-section-header .section-num {
  font-family: var(--font-display);
  font-size: 0.7rem;
  font-weight: 700;
  background: var(--brand);
  color: var(--text-on-brand);
  padding: 2px 7px;
  letter-spacing: 0.1em;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}

.form-section-header .section-title {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-primary);
  flex: 1;
}

.form-section-header .section-chevron {
  color: var(--text-dim);
  font-size: 1rem;
  transition: transform var(--transition);
  flex-shrink: 0;
}

.form-section-header.collapsed .section-chevron {
  transform: rotate(-90deg);
}

.form-section-body {
  padding: 14px;
}

.form-section-body.hidden { display: none; }

/* ── FIELD ROWS ── */
/* ▶ minmax(0, 1fr) AND NOT 1fr (owner walk 2026-10-12, defect 1). `1fr` is
   shorthand for `minmax(auto, 1fr)`, and `auto` as a minimum means the track can
   never be narrower than its content's own minimum — so one field with an
   intrinsic floor (every date and time input had 158px of one) makes the whole
   grid wider than the box it sits in, and the row runs off the right edge
   instead of the fields getting narrower. `minmax(0, 1fr)` lets the track shrink
   and hands the decision back to the container, which is the only thing that
   knows how much room there actually is.

   This is the row-level half of the fix; the field-level half is the capped date
   floor further down, and `min-width: 0` on .field-group below. All three are
   needed: a track that can shrink does nothing if the field inside it cannot. */
.field-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 10px;
  margin-bottom: 10px;
}

.field-row.triple {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.field-row.quad {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.field-row.full {
  grid-template-columns: minmax(0, 1fr);
}

/* ── UX audit F-23 — SINGLE COLUMN BELOW 768px ─────────────────────────────
   This block used to collapse three- and four-column rows to TWO columns at
   480px, and stop there. Two columns at 390px is ~175px per field, which is
   how "SURVEILLANCE END DATE" clipped to "SURVEILLANCE EN" with its input
   running off the screen, and how the page came to scroll sideways.

   The audit's finding is Critical and its reasoning is the product's whole
   premise: "Surveillance write-ups happen in a vehicle, on a phone, often at
   the end of a ten-hour shift. If it can't be completed from a driver's seat,
   the PI writes the report in a notebook and retypes it at 11pm - which is
   exactly the workflow RaizIntel is selling against."

   768px rather than 480px because the clipping starts long before 480: a
   two-column date row needs roughly 700px before the labels fit. A phone in
   landscape and a small tablet are both on the wrong side of that line. */
@media (max-width: 768px) {
  .field-row,
  .field-row.triple,
  .field-row.quad,
  .field-row.full {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Helper text ran as a six-line ribbon in a half-width column beside empty
     space. With one column it simply has the width of its own field. */
  .form-hint, .field-hint { max-width: none; }
}

/* ▶ THE BACKSTOP, AND WHY IT IS NOT THE FIX. A page that scrolls sideways is
   unusable one-handed, and any single wide child causes it. The rules above
   remove the CAUSES; this stops one new one from silently reintroducing the
   symptom across the whole product. It is deliberately on the ROOT elements
   rather than on form containers, so a genuinely wide child (a table, a code
   block) can still scroll INSIDE its own box. */
@media (max-width: 768px) {
  /* CLIP, NOT HIDDEN (punch list 2026-11-02 #45). `hidden` on both html and body makes body a scroll
     container of its own, so the sticky top bar stuck to a box that never scrolls and rode away with
     the page. `clip` cuts the overflow the same way without making a scroll container. `hidden` stays
     first only for browsers that do not know `clip`. */
  html, body { max-width: 100%; overflow-x: hidden; overflow-x: clip; }
}

/* ▶ THE MAIN COLUMN NEVER SCROLLS SIDEWAYS (owner, punch list 2026-11-02, S11). On a laptop window
     about 900–1024 CSS pixels wide (a 1366 screen at 150% display scaling is ~910), the client page's
     row of tabs was wider than the column, and because .main-content scrolls (overflow-y: auto makes
     overflow-x auto too) the whole column scrolled sideways inside itself: the right edge cut off, and
     the left too once it had been nudged. Measured, not guessed: scrollWidth 801 in a 789/665 column.
     Two rules, once, for every page:
       · a flex child may shrink below its widest content (min-width: 0), so one long row cannot widen
         the column;
       · a row of tabs scrolls inside its own box rather than widening the page — the case page already
         did this on phones only. */
.main-content { min-width: 0; }
.workspace-tabs { max-width: 100%; min-width: 0; overflow-x: auto; scrollbar-width: thin; }
.workspace-tabs > * { flex: none; }

/* Wide tables: scroll horizontally instead of overflowing the viewport.
   No-op on desktop (only scrolls when content exceeds the container width). */
.table-scroll { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Phone/tablet touch sizing. Scoped to <=768px so desktop/laptop is untouched.
   16px inputs prevent iOS Safari from auto-zooming on field focus. */
@media (max-width: 768px) {
  /* tel and url join for the same reason they join the INPUTS block below: a
     16px font is what stops iOS Safari auto-zooming on focus, and a phone
     number is one of the likeliest fields to be typed on a phone. */
  input[type="text"], input[type="email"], input[type="password"], input[type="number"],
  input[type="search"], input[type="date"], input[type="tel"], input[type="url"],
  select, textarea { font-size: 16px; }
  .btn { padding: 13px 20px; }
  .btn-sm { padding: 10px 16px; }
  .btn-logout { padding: 9px 14px; }
  .toggle-btn { padding: 10px 8px; }
}

/* ▶ min-width: 0 (owner walk 2026-10-12, defect 1). A grid or flex child's
   automatic minimum is its min-content width, so a .field-group holding a date
   field, a long select option or an unbroken reference number refuses to be made
   narrower than that content — and pushes the row out of its container. With
   this, the field-group yields and the control inside it (capped at 100%) yields
   with it. `max-width: 100%` on the controls is the belt: it catches a control
   given an explicit pixel width by a layout written for a wider screen, such as
   the timeline's 168px date and 132px time boxes. */
.field-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.field-group > input,
.field-group > select,
.field-group > textarea {
  max-width: 100%;
}

.field-group label {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.field-group label .required {
  color: var(--status-error-fg);
  margin-left: 2px;
}

/* ── INPUTS ── */
/* ▶ type="tel" AND type="url" WERE MISSING FROM THIS LIST, AND THAT IS WHY A
   PHONE FIELD HAD NO STYLE AND A DOUBLE BORDER (owner, on adding a new client:
   "adding a new client phone has double border and no style").

   A selector list styles the types it NAMES. An input whose type is absent gets
   no background, no border, no radius, no padding and no font from here, so the
   browser's own control chrome shows through — and because `input:focus` and
   `input[aria-invalid="true"]` below are NOT type-qualified, the field then
   picks up a brand border-colour on focus and a 2px red border when flagged,
   sitting on top of native chrome. That is the second border.

   ▶ THE CAUSE WAS A FIX. Before L-68 every phone field was `type="text"` and
   inherited this block correctly. L-68 gave the product one phone format by
   converting those fields to `type="tel"` — which is right, and is what the
   formatter's selector keys on — and in the same move moved all nineteen of
   them OUT of this rule. Formatting was fixed and styling was broken by the
   same edit, on the same fields, and the two were never connected.

   ▶ ENUMERATED, NOT PATCHED AT THE ONE FIELD HE FOUND. Every input type used
   in the product was diffed against this list: tel (19 uses) and url (2) are
   styled NOWHERE. `search` is deliberately NOT added — it is styled by its own
   .list-search class and is not part of this defect. checkbox / radio / file
   are not text-like and must never join this rule. The url case is not
   hypothetical padding of the fix: `f-website` sits on the SAME new-client form,
   two rows above the phone box he reported.

   Guarded by a source golden so a new text-like type cannot be introduced
   without joining this list. */
input[type="text"],
input[type="date"],
input[type="time"],
input[type="number"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="url"],
select,
textarea {
  width: 100%;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 0.95rem;
  padding: 9px 10px;
  transition: border-color var(--transition), background var(--transition);
  -webkit-appearance: none;
  appearance: none;
  outline: none;
}

/* ── DATE AND TIME FIELDS: THE NATIVE PICKER, MADE USABLE ────────────────────
   Owner ruling (Case Finances walk, 2026-09-23): "a calendar icon, click it,
   pick a date… SITEWIDE, every date field in the product." The control already
   had a calendar; two things stopped it reading as one, and both are CSS.

   1. THE ICON WAS BEING CLIPPED. He read "09/22/202" with the calendar button
      overlapping the year. The native control's internal segments plus its
      button do not fit the widths this product was giving them, and the only
      place that had been noticed was `.qf-date { width:158px }` in
      cases/detail.html — one field, fixed by hand, with a comment saying 112px
      clipped it. A floor here means no date field in the product can be born
      too narrow again. It is a MIN-width, so the grid and flex layouts that
      already size these fields keep their say.

   2. THE PICKER OPENED IN THE WRONG THEME. `color-scheme` was declared in ONE
      file (audit/index.html). Everywhere else the browser drew a white calendar
      over a dark page, which reads as something broken rather than as ours.
      `color-scheme` is inherited, so declaring it where the theme is declared
      lets the native calendar, the segment highlight and the spin controls all
      follow the theme automatically.

   `-webkit-appearance: none` above strips the icon on some WebKit builds; it is
   put back for these two types only, so the affordance is actually visible. */
/* ▶ THE FLOOR IS CAPPED AT THE CONTAINER (owner walk 2026-10-12, defect 1).
   The line below used to read `min-width: 158px`, and the comment above it said
   "it is a MIN-width, so the grid and flex layouts that already size these fields
   keep their say". THAT WAS WRONG, and it is the whole mechanism of the defect:
   min-width BEATS width and beats a flex or grid track, so a layout could no
   longer make a date field narrower than 158px for any reason — including not
   fitting on the screen.

   WHAT HE SAW. Report Date, Date of Loss and Date Reported cut off at the right
   edge of a claims report; the timeline's start and end boxes outside the box
   they sit in; and a page that could still be dragged sideways on an iPhone with
   the zoom all the way out. Two date fields in a 1fr 1fr row need 158+158+gap =
   330px before anything else, and three need 490px. The form column on a computer
   sits beside a 340px rail, and a phone has ~340px in total, so both are under it.

   `min()` keeps the ruling that earned the floor — a date field is never born too
   narrow to show its own calendar button — and gives the container the last word
   when there is genuinely not 158px to be had. */
input[type="date"],
input[type="time"],
input[type="datetime-local"] {
  min-width: min(158px, 100%);
  /* ▶ THE LINE THE OWNER'S PHONE CHOSE (2026-10-12). See the long note below for
     the measurements. In one sentence: iOS sizes these controls CONTENT-BOX no
     matter what it is told, so 'width: 100%' put the border box 22px outside its
     container. '-webkit-fill-available' asks for the container's width INCLUDING
     the padding and border, which is the one request that does not depend on
     box-sizing being honoured.

     ▶ TWO DECLARATIONS, AND THE ORDER IS THE SAFETY. An engine that does not know
     the keyword drops the second and keeps the first, so the failure mode is the
     old behaviour rather than a new one.

     ▶ AND !important, WHICH IS NOT A SHORTCUT HERE — IT IS THE POINT.
     (Owner re-walk, 2026-10-12: the report's own date boxes came good and the
     ACTIVITY TIMELINE's did not.)

     The reason was one line in public/report/index.html:

         .timeline-entry .entry-when input { width: 100%; }

     at specificity (0,2,1), against this rule's (0,1,1). It won, put 'width: 100%'
     straight back, and with it the 22px of overflow — on iOS only, where no engine
     on a development machine can see it.

     ▶ IT IS NOT ONE PLACE. 'node scripts/date-width-overrides.js' is committed and
     answers this by machine across every stylesheet and every <style> block: on the
     day this was written, TWENTY-FOUR rules set a width on a selector that can reach
     a date or time box at or above this rule's specificity. The ones that hold real
     date boxes are the report timeline, the admin modal panel, the audit date range,
     the case quick-filter bar, the attest table and — at (1,1,1), an id — the LEDGER
     ENTRY FORM, which is where time is logged. Fixing them one at a time means being
     right about every container today and every container anybody adds later.

     ▶ WHAT !important CAN AND CANNOT REACH, because a blunt instrument has to be
     aimed. The selector is 'input[type=date|time|datetime-local]', so this cannot
     touch any other control: not the quantity box at 7ch, not a 22px checkbox, not a
     search field. Across the whole census not one rule sets a deliberate FIXED width
     on a date or time input itself — every one of them is a generic 'width: 100%' or
     'width: auto' on 'input'. The nearest thing, '#ledger-entry-form .qf-date', sizes
     the WRAPPER to 158px and the control fills it, which is exactly what this does.

     The invariant is "a date box is never sized by a rule that iOS will read as a
     content width". No test on this machine can catch a regression of it, because
     neither local engine draws the control. A rule that any new stylesheet can
     silently defeat WILL be defeated — the timeline defeated it within hours of it
     shipping. This is the case !important exists for. */
  width: 100% !important;
  width: -webkit-fill-available !important;
  max-width: 100%;
  -webkit-appearance: auto;
  appearance: auto;
}

/* ══ THE SAME CONTROL ON A REAL iPHONE — SOLVED BY MEASUREMENT ═══════════
   Owner walk 2026-10-12: "Every date and time box runs past the right edge of its
   container... On a computer using the browser's phone emulation it looks fine, so
   the emulation does not reproduce it."

   ▶ NO ENGINE ON A DEVELOPMENT MACHINE CAN SHOW THIS. Probed, not assumed
   (scripts/probe-date-input-engines.js): Playwright's WebKit does not implement
   input[type=date] at all on this platform — the element reports type === "text"
   and the engine draws a plain text field. Chromium DOES implement it and sizes it
   correctly at every container width from 340px down to 90px. Two fixes were
   shipped on reasoning alone and both failed, so the owner's phone was turned into
   the instrument: a temporary page put eight candidates in the product's own
   containers and measured them on his iPhone 15 Pro Max.

   ▶ WHAT HIS PHONE RETURNED, and it is the whole mechanism:

       baseline ................. OVER BY 22px   (date and time alike)
       appearance-none .......... fits
       appearance-none + height . fits
       fill-available ........... fits
       flex ..................... fits
       combo .................... fits
       border-box ............... OVER BY 22px   ← the iPhone KEPT content-box
       no-padding ............... OVER BY 2px    ← exactly the two 1px borders

   Those last two are the proof, not the fix. Restating 'box-sizing: border-box'
   directly on the control changed NOTHING, so iOS is not failing to receive the
   instruction — it is declining it for these native controls. And taking the 24px
   of padding away left exactly 2px, the borders. The border box is therefore
   'content + padding + border' however it is addressed, which is content-box
   sizing, and 'width: 100%' was setting the CONTENT width.

   ▶ WHY fill-available AND NOT ONE OF THE OTHER FOUR THAT FIT. The owner ruled
   that the iPhone's own appearance and its date and time wheels stay unchanged,
   preferring fill-available or flex. Of the five:

     • appearance-none, appearance-none+height and combo all switch the native
       rendering off. The wheel would survive — it belongs to the input TYPE, not to
       the appearance — but the control would be drawn by us instead of by Apple.
       Excluded by the ruling, and rightly.
     • flex fits, but it is not one declaration: it is a change to the CONTAINER of
       every date field in the product. .field-group is already a column flex box;
       the candidate made the host a ROW flex box with the control as a flex item.
       Applying that everywhere means auditing every container a date box sits in —
       table cells, modals, the timeline's three fixed-width groups — and several
       would not survive it. A fix whose blast radius is every layout is the wrong
       fix when a one-line one measures the same.
     • fill-available is a single declaration on the control. It touches no
       container, leaves 'appearance: auto' alone so the native look and the wheel
       are untouched, and degrades to the current behaviour where it is unknown.

   ▶ AND THE PREVIOUS ATTEMPT'S RULES ARE GONE RATHER THAN LEFT LYING AROUND. The
   fix before this one constrained '::-webkit-date-and-time-value' and
   '::-webkit-datetime-edit' on the theory that a correctly-sized field was letting
   its VALUE paint outside it. His screenshots disproved that — an EMPTY Date of
   Loss was exactly as wide as a filled Report Date, so the width never came from
   the content — and the field itself was the wrong size all along. Those rules
   also set 'text-align: left' on the value, which is a change to the native
   appearance the owner has now ruled stays unchanged. Dead speculation with a
   visible side effect does not get to stay in a stylesheet. */

@media (max-width: 768px) {
  /* Rule 3. See the note above: the floor guards a calendar button that iOS does
     not draw, so on a phone it is pure risk. The field still has width: 100% of
     whatever holds it, so nothing becomes unusably narrow — it becomes exactly as
     wide as there is room for, which is the whole of the owner's instruction:
     "nothing may overflow its container". */
  input[type="date"],
  input[type="time"],
  input[type="datetime-local"] {
    min-width: 0;
  }
}
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.85;
}
input[type="date"]:hover::-webkit-calendar-picker-indicator,
input[type="datetime-local"]:hover::-webkit-calendar-picker-indicator {
  opacity: 1;
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--brand);
  background: var(--surface-3);
}

/* ── THE INVALID FIELD (L-67) ────────────────────────────────────────────────
   Keyed on aria-invalid, deliberately, and not on a class. A class marks a
   field for the EYE; aria-invalid marks it for the eye AND for a screen reader,
   so there is one act instead of two and no way to do half of it.

   TWO SIGNALS, NOT ONE. Colour alone was the defect being fixed - the two
   pre-existing invalid styles both changed border-color and left the width at
   1px, which is a difference some readers cannot see at all and most cannot see
   at arm's length. The border doubles in width as well as changing hue, and a
   tinted background carries it further.

   Reserved for ERRORS. The product's focus ring is the brand colour and its
   selected chips are the brand colour (F-33); red here means "this is wrong",
   and it is the only thing that means that. */
input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"] {
  border: 2px solid var(--status-error-fg);
  background: var(--status-error-bg, var(--surface-2));
  /* The 1px -> 2px border would otherwise move the text and shift the row. */
  padding: 8px 9px;
}

input[aria-invalid="true"]:focus,
select[aria-invalid="true"]:focus,
textarea[aria-invalid="true"]:focus {
  border-color: var(--status-error-fg);
  background: var(--surface-3);
}

/* The message that says WHY, immediately under the field it belongs to. A
   banner at the top of a form tells you something is wrong; this tells you
   which thing. */
.field-error {
  display: none;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--status-error-fg);
  margin-top: var(--sp-1);
  line-height: 1.45;
}
.field-error.show { display: block; }

input::placeholder,
textarea::placeholder {
  color: var(--text-dim);
  font-style: italic;
}

select {
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23666' d='M6 8L0 0h12z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  padding-right: 28px;
}

textarea {
  resize: vertical;
  min-height: 90px;
  line-height: 1.5;
}

/* Disabled state */
input:disabled, select:disabled, textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ── TOGGLE / RADIO GROUP ── */
.toggle-group {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.toggle-btn {
  flex: 1;
  min-width: 60px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: 0.82rem;
  font-weight: 600;
  padding: 8px 4px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-align: center;
  transition: background var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard);
  -webkit-user-select: none;
  user-select: none;
}

.toggle-btn:active { transform: scale(0.97); }

.toggle-btn.active {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--text-on-brand);
}

.toggle-btn.active-green {
  background: var(--success);
  border-color: var(--success);
  color: var(--text-on-brand);
}

/* ── BUTTONS ──
   SENTENCE CASE as of E-81 S1. Uppercase + tracking is reserved for TITLES and
   BADGES (the premium pass said so at --tracking-caps; buttons were the last
   place still contradicting it). Note this is not only a tone change: caps plus
   0.06em tracking made every button in the product ~10-15% wider than its own
   text needs, which is why it landed last and alone. */
.btn {
  font-family: var(--font-display);
  font-size: 0.9rem;
  font-weight: 600;
  padding: 11px 20px;
  border-radius: var(--radius-sm);
  border: none;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  -webkit-tap-highlight-color: transparent;
}

.btn:active { transform: scale(0.97); }

/* L-42 / F-34 - AN ANCHOR STYLED AS A BUTTON MUST NOT ALSO BE A LINK.
   The audit read the case header's three primary actions as three different
   affordances: "+ Create Report (outlined, underlined), + Add Evidence
   (outlined), View Activity (plain text)". The first two carry the SAME class.
   What differed was that one is an <a> and one is a <button>, and .btn had
   never cleared text-decoration - so all 51 anchor-buttons in the product wore
   an underline their <button> neighbours did not. The role system was being
   applied correctly and undone by the element type. */
a.btn, a.btn:hover, a.btn:focus { text-decoration: none; }

.btn-primary {
  background: var(--action-bg);
  color: var(--action-fg);
}

.btn-primary:hover { background: var(--action-hover); }

/* .btn-secondary is no longer defined here — E-81 S1 pointed it at the role it
   should always have had. It now shares the .btn-secondary-strong rule below, so
   the two can never drift apart while both names are in the tree. It is a
   TRANSITIONAL alias: each screen's own wave remaps its buttons to the right role
   (-strong / -quiet / -destructive), and the alias is dropped from that selector
   once the last of the 166 call sites is gone. */

.btn-success {
  background: var(--success);
  color: var(--text-on-brand);
}

.btn-outline-red {
  background: transparent;
  color: var(--status-error-fg);
  border: 1px solid var(--danger);
}

.btn-outline-red:hover {
  background: var(--danger-soft);
}

/* ══ PREMIUM PASS — THE BUTTON ROLE SYSTEM (owner CP3a) ══
   Four roles, and exactly ONE filled red per screen. The census before this
   campaign found 24 filled-red buttons on the case page alone, which is why the
   owner read "Create Report" and "Continue draft" as competing: they were
   identical. Colour now means "this is THE action here".

   .btn-primary          filled brand      — one per screen, inside the current tab
   .btn-secondary-strong surface + strong border, bright text — common actions that
                         must still have real presence (NOT a ghost button)
   .btn-quiet            text only         — tertiary/inline: View, Cancel, Close
   .btn-destructive      outline brand     — Revoke, Delete, Correct, Detach

   These were ADDITIVE while the case area was the only swept surface. As of
   E-81 S1 the legacy .btn-secondary is an ALIAS of -strong rather than a fourth,
   weaker look: it was surface-2 + --text-secondary + --border, which is the ghost
   button the role system exists to retire. Aliasing it lifts all 166 remaining
   call sites at once without pre-deciding 166 per-button questions — those are a
   judgement each screen's own wave makes with that screen in front of it. */
.btn-secondary,
.btn-secondary-strong {
  background: var(--surface-2);
  color: var(--text-primary);
  border: 1px solid var(--border-strong);
  font-weight: 600;
}
.btn-secondary:hover,
.btn-secondary-strong:hover {
  background: var(--surface-3);
  color: var(--text-bright);
  border-color: var(--text-dim);
}

.btn-quiet {
  background: transparent;
  color: var(--text-secondary);
  border: 1px solid transparent;
}
.btn-quiet:hover {
  background: var(--veil-hover);
  color: var(--text-bright);
}

.btn-destructive {
  background: transparent;
  color: var(--status-error-fg);
  border: 1px solid var(--status-error-border);
}
.btn-destructive:hover {
  background: var(--danger);
  color: #FFFFFF;
  border-color: var(--danger);
}

.btn-sm {
  font-size: 0.78rem;
  padding: 7px 14px;
}

/* ══ PREMIUM PASS — GLOBAL FOCUS RING ══
   Keyboard focus was largely invisible before this. :focus-visible only, so a
   mouse click never paints a ring but tabbing always does. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-control);
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

.btn-full { width: 100%; }

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

/* ── ALERTS ── */
.alert {
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  font-size: 0.85rem;
  font-weight: 600;
  border-left: 3px solid;
  margin-bottom: 12px;
}

.alert-error {
  background: var(--status-error-bg);
  border-color: var(--danger);
  color: var(--status-error-fg);
}

.alert-success {
  background: var(--status-ok-bg);
  border-color: var(--success);
  color: var(--status-ok-fg);
}

.alert-info {
  background: var(--status-info-bg);
  border-color: var(--info);
  color: var(--status-info-fg);
}

/* ── LOADING SPINNER ── */
.spinner {
  width: 18px;
  height: 18px;
  border: 2px solid rgba(255,255,255,0.2);
  border-top-color: white;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  display: inline-block;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ── UTILITY ── */
.text-muted { color: var(--text-dim); }
.text-red   { color: var(--status-error-fg); }
.text-sm    { font-size: 0.8rem; }
.mt-8       { margin-top: 8px; }
.mt-12      { margin-top: 12px; }
.mt-16      { margin-top: 16px; }
.hidden     { display: none !important; }

/* ── STATUS BADGES (canonical / shared) ──
   Phase 4 Batch 4-1: single source of truth for status pills. Page-level
   <style> blocks load after this file, so existing pages keep their own
   geometry via the cascade — these defaults apply to new markup and to the
   opt-in client-visibility classes below. Geometry matches the majority of
   pages (0.62rem text, 5px dot, gap 5px, padding 3px 8px). */
.status-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border-radius: 4px;
  font-family: var(--font-display);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}
.status-badge::before {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}

/* Canonical color map (palette verbatim from existing pages) */
.badge-active,
.badge-approved,
.badge-stored,
.badge-allow            { background: var(--status-ok-bg);  color: var(--status-ok-fg); }
.badge-submitted,
.badge-info,
.badge-intake           { background: var(--status-info-bg);  color: var(--status-info-fg); }
.badge-reviewed,
.badge-on_hold,
.badge-duplicate        { background: var(--status-warn-bg); color: var(--status-warn-fg); }
.badge-returned,
.badge-locked,
.badge-restricted,
.badge-deny,
.badge-entered_in_error { background: var(--status-error-bg);  color: var(--status-error-fg); }
.badge-draft,
.badge-closed,
.badge-pending,
.badge-archived         { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }
.badge-void             { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }

/* Client-visibility disclosure pills (reusable; replaces inline styles) */
.badge-client-visible   { background: rgba(125,207,138,0.12); color: var(--status-ok-fg); }
.badge-client-hidden    { background: rgba(170,170,170,0.12); color: var(--text-dim); }

/* Lock / immutability indicator. Markup carries the "Locked" text label, so
   the lock glyph degrades gracefully if the emoji font is unavailable. */
.lock-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-display);
  font-size: 0.58rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--status-warn-fg);
  white-space: nowrap;
}

/* Wraps a row of badges/tags so they stay tidy instead of overflowing.
   Opt-in helper — not auto-applied to existing rows in this batch. */
.badge-cluster {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  row-gap: 4px;
}

/* ── OWNERSHIP CHIP — "whose setting is this" ──────────────────────────────
   Answered on the card itself, never by a scrolled-away zone heading
   (owner-ruled 2026-08-20). Promoted here 2026-09-01 because the notification
   surfaces made this a THIRD copy, and three inline copies of an indicator
   this load-bearing is how the three drift apart.

   RECONCILED, because the two originals had already drifted: /settings carried
   the base + both modifiers; /profile carried a base equal to .hat-you and no
   modifiers. The settings form wins (a neutral base that MUST be qualified),
   and profile's `text-transform: none` is kept — its card headings are
   uppercase, so the chip has to opt out or it shouts. */
.hat-chip {
  float: right;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .06em;
  padding: 1px var(--sp-2);
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-dim);
  background: var(--surface-2);
  text-transform: none;
}
.hat-chip.hat-agency { border-color: var(--brand);         color: var(--brand); }
.hat-chip.hat-you    { border-color: var(--border-strong); color: var(--text-secondary); }

/* ── NOTIFICATION PREFERENCE ROWS (piece 1) ────────────────────────────────
   Rendered on three surfaces -- a person's profile, the agency defaults, and
   the admin's read-only view -- so they live here rather than in three pages.

   The row leads with its STATE and then says which LAYER decided it, because
   "off" and "off because the agency turned it off" are different facts and a
   bare checkbox cannot tell them apart.

   THREE STATES, and the third is a fault, not a choice:
     on   -> status-ok       a thing that will be sent
     off  -> status-neutral  a DELIBERATE choice; never red, nothing is wrong
     n/a  -> status-warn     NO ADDRESS: nothing can be sent at all. This must
                             never read as "off" -- off is something somebody
                             chose, this is something nobody chose. */
.notif-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--border-soft);
}
.notif-row:last-child { border-bottom: 0; }
.notif-row-label { flex: 1 1 auto; font-size: var(--fs-sm); color: var(--text-primary); }
.notif-state {
  flex: 0 0 auto;
  font-family: var(--font-display);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 4px;
  white-space: nowrap;
}
.notif-state.is-on        { background: var(--status-ok-bg);      color: var(--status-ok-fg); }
.notif-state.is-off       { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }
.notif-state.is-locked    { background: var(--status-info-bg);    color: var(--status-info-fg); }
.notif-state.is-undeliverable {
  background: var(--status-warn-bg);
  color: var(--status-warn-fg);
}
/* Which layer decided — the SAME ownership vocabulary as the card chips, so a
   reader does not have to learn a second indicator for the same idea. */
.notif-layer {
  flex: 0 0 auto;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .06em;
  padding: 1px var(--sp-2);
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-dim);
  background: var(--surface-2);
  white-space: nowrap;
}
.notif-layer.layer-you    { border-color: var(--border-strong); color: var(--text-secondary); }
.notif-layer.layer-agency { border-color: var(--brand);         color: var(--brand); }

/* The undeliverable banner + the always-sent block. */
.notif-fault {
  border: 1px solid var(--status-warn-border);
  background: var(--status-warn-bg);
  color: var(--text-primary);
  border-radius: var(--radius-surface, 6px);
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.notif-always {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border-soft);
  font-size: var(--fs-sm);
  color: var(--text-secondary);
  line-height: 1.5;
}

/* ── EMPTY STATES (canonical / shared) ── */
.empty-state      { padding: 48px 24px; text-align: center; }
.empty-state-icon { font-size: 2rem; margin-bottom: 10px; opacity: 0.3; }
.empty-state-text {
  font-size: 0.85rem;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--text-dim);
}
.empty-state-sub  { font-size: 0.8rem; color: var(--text-dim); margin-top: 6px; opacity: 0.7; }

/* Narrow-phone badge handling (separate from the 768px layout block).
   Keeps badge clusters wrapping cleanly and stops glyph+word splitting. */
@media (max-width: 500px) {
  .badge-cluster { gap: 4px; row-gap: 3px; }
  .status-badge,
  .lock-tag,
  .pub-badge { white-space: nowrap; }
}

/* ══ EVIDENCE DETAIL — CONTENT + PERSISTENT CUSTODY RAIL (§E-95) ══════════════
   Owner ruling: the evidence detail reads as a CONTENT COLUMN with the chain of
   custody standing beside it as a persistent rail.

   WHY THIS LIVES IN THE SHARED SHEET AND NOT IN EITHER PAGE. There are TWO
   renderers of this panel — the case workspace (cases/detail.html) and the
   evidence page (cases/evidence.html) — and they had already drifted into two
   different layouts of the same content. Defining the layout once, here, is what
   stops them drifting again: neither page owns it, so neither can quietly change
   it alone. Row-level styling (fact rows, custody rows) stays per-page; only the
   LAYOUT is shared.

   WHAT THE PREVIOUS LAYOUT GOT WRONG. It split the body 1fr/1fr by KIND — facts
   left, the two append-only records right. But the left is BOUNDED (a handful of
   facts) while the right is UNBOUNDED (it grows with every custody event and
   every metadata assertion), so the columns could never balance: whichever ran
   longer decided where the dead space landed, and on any item with real history
   that left the lower-left empty. A fixed-width rail does not have that failure
   mode — the content column takes the slack.

   The rail is STICKY, not merely tall: custody is the thing you scan while
   reading everything else, so it holds position while the content column scrolls
   past it, and scrolls internally only when it is longer than the window. */
.evd-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 0 var(--sp-6);
  align-items: start;
}
.evd-main { min-width: 0; }
.evd-rail {
  min-width: 0;
  border-left: 1px solid var(--border-soft);
  padding-left: var(--sp-6);
  position: sticky;
  top: 0;
  /* Bounded by the modal it sits in (.rzm-panel is max-height:min(88vh,900px));
     the subtraction is its chrome — header, footer and the body's own padding.
     Only bites when custody is longer than the window; short chains never scroll. */
  max-height: calc(min(88vh, 900px) - 190px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Stack below 900px — the same breakpoint the previous two-column body used, so
   phones and narrow laptops keep today's single-column reading order. */
@media (max-width: 900px) {
  .evd-layout { display: block; }
  .evd-rail {
    border-left: none; padding-left: 0; margin-top: var(--sp-5);
    position: static; max-height: none; overflow-y: visible;
    border-top: 1px solid var(--border-soft); padding-top: var(--sp-5);
  }
}

/* ── L-68 — A PHONE NUMBER THAT LOOKS LIKE A TYPO ────────────────────────────
   Owner-ruled 2026-08-31, from a real slip: 810-564-26422 saved silently.
   A number too long to be a US number is FLAGGED and still SAVES. It is
   deliberately a notice, not an error: a hard block on a phone field is how
   somebody abandons a form, and a private investigator does end up holding
   foreign numbers and odd formats. Warm amber, not red — this is "have another
   look", not "you have done something wrong". */
input.raiz-phone-flagged {
  border-color: var(--status-warn-border);
  background: var(--status-warn-bg);
}
.raiz-phone-note {
  margin-top: 4px;
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--status-warn-fg);
}

/* ── A DATE DECADES BACK: month / day / year, over the native field ──
   RaizDate.ymd (owner-ruled 2026-10-09). The native <input type="date"> is still
   there and still holds the value — it is simply not the thing being looked at.
   Shared, because a date of birth is asked for in more than one place. */
/* ▶ IT WRAPS (owner-ruled 2026-10-10, walked RED). Three dropdowns and a
   calendar button have a hard floor of ~330px. Without `flex-wrap` the ones
   that cannot shrink simply overflow the field's box and are drawn on top of
   whatever sits beside it — which is exactly what the owner read: a Year box
   under "Phone number" and a calendar button hidden entirely. Wrapping keeps
   every control inside its own field, however narrow the column gets. The
   field also asks for a full-width row where it is built; this is the floor
   under that, for any caller who forgets. */
/* ▶ THE MONTH BOX USED TO EAT THE ROW (owner-ruled 2026-10-10, second walk).
   `flex: 1 1 auto` on the month made it absorb every spare pixel, so in the
   full-width row this field asks for, Month stretched across the whole screen
   and Day, Year and the calendar wrapped onto a SECOND LINE underneath it —
   which is what he saw and is the opposite of the ruling: "compact month, day
   and year dropdowns plus the calendar, all on one line, the same size and
   style as every other field."

   Two changes, and both are needed. The month no longer GROWS (`0 1 auto`), and
   the group is CAPPED so a wide row cannot stretch it. The caps are not
   arbitrary: full month names need about 104px, and 104 + 76 + 92 + 44 plus
   three 8px gaps is 340px — so 23rem leaves a little slack and still fits
   inside a 390px phone. The min-widths stay, so it can never be squeezed to
   unreadable either. */
.rzymd {
  display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: stretch;
  max-width: 23rem;
}
.rzymd select {
  min-height: 44px; font-size: var(--fs-base); padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--radius-control);
  background: var(--surface-inset); color: var(--text-primary);
  /* ▶ width:auto IS LOAD-BEARING. A global rule in this stylesheet sets EVERY
     select to width:100%, so `flex-basis: auto` resolved to the full row and the
     month box grew to 276px of a 276px group — measured in a browser, not
     guessed, after the first fix did nothing. Day and Year escaped it only
     because their fixed flex-basis beat the width. */
  width: auto;
}
/* Sized for the THREE-LETTER months the dropdown now uses, so all four controls
   fit one line at 390px: 66 + 56 + 76 + 44 plus three 8px gaps is 266, inside
   the ~276 a phone leaves. The mins keep them readable if a column is narrower. */
.rzymd .rzymd-m { flex: 0 1 auto; min-width: 66px; }
.rzymd .rzymd-d { flex: 0 0 56px; }
.rzymd .rzymd-y { flex: 0 0 76px; }
.rzymd .rzymd-cal {
  flex: 0 0 auto; min-width: 44px; min-height: 44px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius-control);
  background: var(--surface-raised); color: var(--text-secondary); font-size: var(--fs-lg);
}
.rzymd .rzymd-cal:hover { color: var(--text-primary); border-color: var(--border-strong); }
/* Kept in the DOM, kept submitting, not shown. `display:none` would stop some
   browsers reporting it to assistive technology at all, so it is collapsed
   instead and remains reachable. */
.rzymd .rzymd-native {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (max-width: 768px) {
  .rzymd select, .rzymd .rzymd-cal { min-height: 48px; font-size: 16px; }
}
