/* Katch PI design tokens — ported from the approved Phase 0 design-review artifact (D-001). */
:root {
  --bg0: #DDE5F0; --bg1: #EEF2F8;
  --panel: rgba(255, 255, 255, .66); --panel-solid: #F7FAFD; --panel2: rgba(255, 255, 255, .5);
  --line: rgba(13, 25, 48, .13); --line-strong: rgba(13, 25, 48, .26);
  --row-alt: rgba(13, 25, 48, .035);
  --ink: #0D1930; --muted: #46587A; --faint: #5B667B;
  /* The *-ink and status hues are what a chip, a badge and the active rail item write on
     their own wash. A wash is only a 10–18% tint, so the effective background is nearly
     --bg0 and these have to clear 4.5:1 against it (WCAG 1.4.3) — measured over every
     surface a chip can land on, worst case included, by ColourContrastTests. */
  --accent: #0284C7; --accent-ink: #025A85; --accent-wash: rgba(2, 132, 199, .10);
  --scrollbar-thumb: rgba(13, 25, 48, .30); --scrollbar-thumb-hover: rgba(13, 25, 48, .5);
  --amber: #7A5207; --amber-wash: rgba(251, 191, 36, .18); --amber-dot: #D97706;
  --ok: #106029; --ok-wash: rgba(21, 128, 61, .11);
  --danger: #A32828; --danger-wash: rgba(220, 60, 60, .10);
  /* Solid fills that carry a label or a glyph. A fill under text owes 4.5:1 (1.4.3); a
     hairline, a focus ring or a progress bar owes 3:1 against what is beside it (1.4.11),
     which is what --accent/--ok/--danger/--amber are tuned for. Conflating the two is how
     every primary button in the light theme came to be a --bg0 label on a bright accent at
     2.8–4.5:1. --accent-fill follows --accent-ink so all eight selectable accents get a
     compliant fill without eight more declarations; give an accent block its own
     --accent-fill if the two ever need to diverge. */
  --accent-fill: var(--accent-ink); --on-accent-fill: #FFFFFF;
  --ok-fill: var(--ok); --on-ok-fill: #FFFFFF;
  --warn-fill: var(--amber); --on-warn-fill: #FFFFFF;
  --danger-fill: var(--danger); --on-danger-fill: #FFFFFF;
  /* Categorical chart series (indigo, teal, slate): hues no selectable accent uses
     exactly, so secondary series never collide with the accent-colored primary. */
  --chart-1: #4F46E5; --chart-2: #0D9488; --chart-3: #64748B;
  /* Elevation scale (§9.1). Depth is information, so the surfaces do not all sit at one depth:
     --elev-1 is a flyout anchored to the control that opened it (a lookup list) and barely leaves
     the page; --elev-2 is something transient floating over content (a toast); --elev-3 is a surface
     that has taken the viewport over a dimmed backdrop. Drawer and modal share level 3 deliberately
     — both are entered through a backdrop, and reading one as shallower would misdescribe how they
     behave. Blur and alpha grow together, because a shadow that spreads without softening reads as
     a border rather than as height. */
  --elev-1: 0 4px 12px rgba(13, 25, 48, .16);
  --elev-2: 0 10px 28px rgba(13, 25, 48, .21);
  --elev-3: 0 18px 50px rgba(13, 25, 48, .25);
  --blur: 18px;
  --r-lg: 14px; --r-md: 10px;
  --font: "Space Grotesk", "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
  --mono: "IBM Plex Mono", "Cascadia Mono", Consolas, ui-monospace, monospace;
  --pad: 16px; --gap: 12px; --fs: 13.5px; --fs-sm: 12px;
  /* Motion scale (§9.1). These are tokens so that reduced motion is a change of TOKEN — see the
     reduce-motion rules below — rather than only a blanket override, and so a designer can retune
     the whole product's timing in one place instead of hunting 20 literals.
     --dur-fast   state change on a control (hover, checked, focus)
     --dur-base   something entering or leaving (a toast, a drawer, the rail widening)
     --dur-slow   layout resolving (a fold opening, a bar growing to its value)
     --dur-instant is what reduced motion collapses the others to, named once here instead of
     repeated as .001s at four sites.
     Looping ambient animation — the reconnect spinner, the skeleton shimmer — keeps its own literal
     duration and `linear` timing on purpose: a rotation that eases is a rotation that stutters, and
     its period is a property of that one animation rather than of the system. */
  --dur-instant: .001s; --dur-fast: .15s; --dur-base: .18s; --dur-slow: .28s;
  --ease-standard: ease; --ease-out: ease-out;
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg0: #060D1D; --bg1: #0B1830;
    --panel: rgba(13, 26, 48, .60); --panel-solid: #0D1930; --panel2: rgba(10, 20, 40, .5);
    --line: rgba(148, 190, 255, .13); --line-strong: rgba(148, 190, 255, .28);
    --row-alt: rgba(148, 190, 255, .045);
    --ink: #E2ECF9; --muted: #93A7C8; --faint: #71839E;
    --accent: #22D3EE; --accent-ink: #67E8F9; --accent-wash: rgba(34, 211, 238, .10);
  --scrollbar-thumb: rgba(148, 190, 255, .32); --scrollbar-thumb-hover: rgba(148, 190, 255, .55);
    --amber: #FBBF24; --amber-wash: rgba(251, 191, 36, .13); --amber-dot: #FBBF24;
    --ok: #4ADE80; --ok-wash: rgba(74, 222, 128, .10);
    --danger: #F87171; --danger-wash: rgba(248, 113, 113, .12);
    /* Inverted from the light theme: here the bright hue is the fill and the near-black
       background colour is the label, which is what already made the dark theme pass. */
    --accent-fill: var(--accent); --on-accent-fill: var(--bg0);
    --ok-fill: var(--ok); --on-ok-fill: var(--bg0);
    --warn-fill: var(--amber); --on-warn-fill: var(--bg0);
    --danger-fill: var(--danger); --on-danger-fill: var(--bg0);
    --chart-1: #818CF8; --chart-2: #2DD4BF; --chart-3: #94A3B8;
    --elev-1: 0 4px 14px rgba(0, 0, 0, .45);
    --elev-2: 0 12px 34px rgba(0, 0, 0, .5);
    --elev-3: 0 24px 70px rgba(0, 0, 0, .55);
    color-scheme: dark;
  }
}

/* Explicit theme override (user toggle) wins over the OS preference in both directions. */
:root[data-theme="dark"] {
  --bg0: #060D1D; --bg1: #0B1830;
  --panel: rgba(13, 26, 48, .60); --panel-solid: #0D1930; --panel2: rgba(10, 20, 40, .5);
  --line: rgba(148, 190, 255, .13); --line-strong: rgba(148, 190, 255, .28);
  --row-alt: rgba(148, 190, 255, .045);
  --ink: #E2ECF9; --muted: #93A7C8; --faint: #71839E;
  --accent: #22D3EE; --accent-ink: #67E8F9; --accent-wash: rgba(34, 211, 238, .10);
  --scrollbar-thumb: rgba(148, 190, 255, .32); --scrollbar-thumb-hover: rgba(148, 190, 255, .55);
  --amber: #FBBF24; --amber-wash: rgba(251, 191, 36, .13); --amber-dot: #FBBF24;
  --ok: #4ADE80; --ok-wash: rgba(74, 222, 128, .10);
  --danger: #F87171; --danger-wash: rgba(248, 113, 113, .12);
  /* Inverted from the light theme: here the bright hue is the fill and the near-black
     background colour is the label, which is what already made the dark theme pass. */
  --accent-fill: var(--accent); --on-accent-fill: var(--bg0);
  --ok-fill: var(--ok); --on-ok-fill: var(--bg0);
  --warn-fill: var(--amber); --on-warn-fill: var(--bg0);
  --danger-fill: var(--danger); --on-danger-fill: var(--bg0);
  --chart-1: #818CF8; --chart-2: #2DD4BF; --chart-3: #94A3B8;
  --elev-1: 0 4px 14px rgba(0, 0, 0, .45);
  --elev-2: 0 12px 34px rgba(0, 0, 0, .5);
  --elev-3: 0 24px 70px rgba(0, 0, 0, .55);
  color-scheme: dark;
}

/* Configurable accent (§9.13): preset hues with light/dark variants chosen inside the
   approved contrast constraints. No data-accent = the default cyan from the theme. */
:root[data-accent="blue"]:not([data-theme="dark"]) { --accent: #2563EB; --accent-ink: #1A45BE; --accent-wash: rgba(37, 99, 235, .10); }
@media (prefers-color-scheme: dark) {
  :root[data-accent="blue"]:not([data-theme="light"]) { --accent: #60A5FA; --accent-ink: #93C5FD; --accent-wash: rgba(96, 165, 250, .12); }
}
:root[data-theme="dark"][data-accent="blue"] { --accent: #60A5FA; --accent-ink: #93C5FD; --accent-wash: rgba(96, 165, 250, .12); }

:root[data-accent="violet"]:not([data-theme="dark"]) { --accent: #7C3AED; --accent-ink: #6423C6; --accent-wash: rgba(124, 58, 237, .10); }
@media (prefers-color-scheme: dark) {
  :root[data-accent="violet"]:not([data-theme="light"]) { --accent: #A78BFA; --accent-ink: #C4B5FD; --accent-wash: rgba(167, 139, 250, .12); }
}
:root[data-theme="dark"][data-accent="violet"] { --accent: #A78BFA; --accent-ink: #C4B5FD; --accent-wash: rgba(167, 139, 250, .12); }

:root[data-accent="emerald"]:not([data-theme="dark"]) { --accent: #059669; --accent-ink: #046045; --accent-wash: rgba(5, 150, 105, .10); }
@media (prefers-color-scheme: dark) {
  :root[data-accent="emerald"]:not([data-theme="light"]) { --accent: #34D399; --accent-ink: #6EE7B7; --accent-wash: rgba(52, 211, 153, .12); }
}
:root[data-theme="dark"][data-accent="emerald"] { --accent: #34D399; --accent-ink: #6EE7B7; --accent-wash: rgba(52, 211, 153, .12); }

/* Light amber leans golden (hue ~41°) rather than amber-700's burnt orange (~26°);
   still ≥4.5:1 on white so primary-button text keeps its contrast. */
:root[data-accent="amber"]:not([data-theme="dark"]) { --accent: #9A6C06; --accent-ink: #755408; --accent-wash: rgba(251, 191, 36, .16); }
@media (prefers-color-scheme: dark) {
  :root[data-accent="amber"]:not([data-theme="light"]) { --accent: #FBBF24; --accent-ink: #FCD34D; --accent-wash: rgba(251, 191, 36, .13); }
}
:root[data-theme="dark"][data-accent="amber"] { --accent: #FBBF24; --accent-ink: #FCD34D; --accent-wash: rgba(251, 191, 36, .13); }

:root[data-accent="orange"]:not([data-theme="dark"]) { --accent: #EA580C; --accent-ink: #9C340A; --accent-wash: rgba(234, 88, 12, .10); }
@media (prefers-color-scheme: dark) {
  :root[data-accent="orange"]:not([data-theme="light"]) { --accent: #FB923C; --accent-ink: #FDBA74; --accent-wash: rgba(251, 146, 60, .12); }
}
:root[data-theme="dark"][data-accent="orange"] { --accent: #FB923C; --accent-ink: #FDBA74; --accent-wash: rgba(251, 146, 60, .12); }

:root[data-accent="rose"]:not([data-theme="dark"]) { --accent: #E11D48; --accent-ink: #A50F34; --accent-wash: rgba(225, 29, 72, .10); }
@media (prefers-color-scheme: dark) {
  :root[data-accent="rose"]:not([data-theme="light"]) { --accent: #FB7185; --accent-ink: #FDA4AF; --accent-wash: rgba(251, 113, 133, .12); }
}
:root[data-theme="dark"][data-accent="rose"] { --accent: #FB7185; --accent-ink: #FDA4AF; --accent-wash: rgba(251, 113, 133, .12); }

:root[data-accent="fuchsia"]:not([data-theme="dark"]) { --accent: #C026D3; --accent-ink: #8D1897; --accent-wash: rgba(192, 38, 211, .10); }
@media (prefers-color-scheme: dark) {
  :root[data-accent="fuchsia"]:not([data-theme="light"]) { --accent: #E879F9; --accent-ink: #F0ABFC; --accent-wash: rgba(232, 121, 249, .12); }
}
:root[data-theme="dark"][data-accent="fuchsia"] { --accent: #E879F9; --accent-ink: #F0ABFC; --accent-wash: rgba(232, 121, 249, .12); }

/* High-contrast theme (§10, §19.5): pure-black surfaces, white ink, solid strong lines,
   zero translucency and blur — glass never outranks readability. Declared after the
   accent variants (and repeated with [data-accent]) so a chosen accent can't pull the
   palette below the contrast floor. */
:root[data-theme="contrast"],
:root[data-theme="contrast"][data-accent] {
  --bg0: #000000; --bg1: #000000;
  --panel: #000000; --panel-solid: #000000; --panel2: #0A0A0A;
  --line: #8A8A8A; --line-strong: #FFFFFF;
  --ink: #FFFFFF; --muted: #E8E8E8; --faint: #C4C4C4;
  --accent: #00E5FF; --accent-ink: #7DF3FF; --accent-wash: rgba(0, 229, 255, .22);
  --scrollbar-thumb: #9A9A9A; --scrollbar-thumb-hover: #FFFFFF;
  --amber: #FFD34D; --amber-wash: rgba(255, 211, 77, .22); --amber-dot: #FFD34D;
  --ok: #4DFF88; --ok-wash: rgba(77, 255, 136, .18);
  --danger: #FF7373; --danger-wash: rgba(255, 115, 115, .2);
  /* Pure black under every fill — the whole point of this theme is that nothing is a
     near-miss, so the label does not inherit --bg0 by way of a variable. */
  --accent-fill: var(--accent); --on-accent-fill: #000000;
  --ok-fill: var(--ok); --on-ok-fill: #000000;
  --warn-fill: var(--amber); --on-warn-fill: #000000;
  --danger-fill: var(--danger); --on-danger-fill: #000000;
  --chart-1: #9DA9FF; --chart-2: #4DFFDF; --chart-3: #D6DEE9;
  /* The scale collapses here rather than shrinking: a soft shadow on pure black is invisible, and a
     hard one would compete with the strong lines that carry structure in this theme. */
  --elev-1: none; --elev-2: none; --elev-3: none;
  --blur: 0px;
  color-scheme: dark;
}
:root[data-theme="contrast"] * { backdrop-filter: none !important; }
:root[data-theme="contrast"] :focus-visible { outline: 3px solid #FFFFFF !important; outline-offset: 2px; }

/* OS forced-colors (Windows High Contrast): keep boundaries and focus visible with
   system palette keywords; everything else defers to the forced palette. */
@media (forced-colors: active) {
  .btn, .chip, .panel, .tile, .stat, input, select, textarea { border: 1px solid CanvasText; }
  :focus-visible { outline: 3px solid Highlight; outline-offset: 2px; }
}

/* Corner style (cosmetic, §9.13 spirit): the default is the rounded language. "Sweep"
   gives containers one large swooping corner pair; "cut" bevels the corners via
   corner-shape (Chromium 139+ — older engines gracefully keep rounded corners).
   Containers only; buttons, inputs, chips, and rows keep their normal radii. */
:root[data-shape="sweep"] .panel,
:root[data-shape="sweep"] .tile,
:root[data-shape="sweep"] .stat,
:root[data-shape="sweep"] .case-head,
:root[data-shape="sweep"] .xp-drawer,
:root[data-shape="sweep"] .reconnect-card,
:root[data-shape="sweep"] .toast { border-radius: 1.5rem 9px 1.5rem 9px; }
:root[data-shape="cut"] .panel,
:root[data-shape="cut"] .tile,
:root[data-shape="cut"] .stat,
:root[data-shape="cut"] .case-head,
:root[data-shape="cut"] .xp-drawer,
:root[data-shape="cut"] .reconnect-card,
:root[data-shape="cut"] .toast { corner-shape: bevel; border-radius: 13px; }
/* Cut sweep: only the top-left/bottom-right pair is beveled (a zero radius keeps the
   opposite corners sharp) — the angular cousin of the sweep treatment. */
:root[data-shape="cut-sweep"] .panel,
:root[data-shape="cut-sweep"] .tile,
:root[data-shape="cut-sweep"] .stat,
:root[data-shape="cut-sweep"] .case-head,
:root[data-shape="cut-sweep"] .xp-drawer,
:root[data-shape="cut-sweep"] .reconnect-card,
:root[data-shape="cut-sweep"] .toast { corner-shape: bevel; border-radius: 15px 0 15px 0; }

/* "None": the corner language switched off entirely — unlike the treatments above it reaches
   every surface AND every control, so the whole app reads as hard rectangles, and panels drop
   their hairline border to sit as pure planes of colour. !important so radii carried in inline
   styles go too. Three kinds of roundness survive on purpose: the appearance picker's own
   swatches (role="radio"), so the other options still preview what they would do; elements that
   are circles by identity rather than by styling (avatars, status dots, the spinner), restored
   below; and pseudo-element circles (toggle knobs, timeline nodes), which the element selector
   never touched. The companion's picker previews its treatments through .m-swatch spans rather
   than role="radio" buttons, so that class rides the same exemption; its identity circles are
   restored in its own stylesheet. */
/* :where() keeps the exemptions from adding specificity — a bare :not() lifts this rule to
   (0,4,0) and it silently beats every circle-restore below, which is exactly how the avatars
   first shipped square. */
:root[data-shape="none"] *:where(:not([role="radio"]):not(.m-swatch)) { border-radius: 0 !important; }
:root[data-shape="none"] .avatar,
:root[data-shape="none"] .tb-avatar,
:root[data-shape="none"] .reconnect-spinner,
:root[data-shape="none"] .health i,
:root[data-shape="none"] .cdot,
:root[data-shape="none"] .notif-dot,
:root[data-shape="none"] .chat-dots span,
:root[data-shape="none"] .icon-ring,
:root[data-shape="none"] .toast-ico { border-radius: 50% !important; }
/* The switch stays a pill: its shape is what says "this slides", and its knob (a pseudo-element)
   kept its circle anyway — a round knob in a square track would read as broken, not styled. */
:root[data-shape="none"] input[type="checkbox"] { border-radius: 999px !important; }
:root[data-shape="none"] .panel,
:root[data-shape="none"] .tile,
:root[data-shape="none"] .stat,
:root[data-shape="none"] .case-head,
:root[data-shape="none"] .xp-drawer,
:root[data-shape="none"] .reconnect-card,
:root[data-shape="none"] .toast { border-color: transparent; }
/* A deep link's target must stay findable — the accent border is that tile's whole highlight. */
:root[data-shape="none"] .tile.linked-target { border-color: var(--accent); }

:root[data-theme="light"] {
  --bg0: #DDE5F0; --bg1: #EEF2F8;
  --panel: rgba(255, 255, 255, .66); --panel-solid: #F7FAFD; --panel2: rgba(255, 255, 255, .5);
  --line: rgba(13, 25, 48, .13); --line-strong: rgba(13, 25, 48, .26);
  --row-alt: rgba(13, 25, 48, .035);
  --ink: #0D1930; --muted: #46587A; --faint: #5B667B;
  /* The *-ink and status hues are what a chip, a badge and the active rail item write on
     their own wash. A wash is only a 10–18% tint, so the effective background is nearly
     --bg0 and these have to clear 4.5:1 against it (WCAG 1.4.3) — measured over every
     surface a chip can land on, worst case included, by ColourContrastTests. */
  --accent: #0284C7; --accent-ink: #025A85; --accent-wash: rgba(2, 132, 199, .10);
  --scrollbar-thumb: rgba(13, 25, 48, .30); --scrollbar-thumb-hover: rgba(13, 25, 48, .5);
  --amber: #7A5207; --amber-wash: rgba(251, 191, 36, .18); --amber-dot: #D97706;
  --ok: #106029; --ok-wash: rgba(21, 128, 61, .11);
  --danger: #A32828; --danger-wash: rgba(220, 60, 60, .10);
  /* Solid fills that carry a label or a glyph. A fill under text owes 4.5:1 (1.4.3); a
     hairline, a focus ring or a progress bar owes 3:1 against what is beside it (1.4.11),
     which is what --accent/--ok/--danger/--amber are tuned for. Conflating the two is how
     every primary button in the light theme came to be a --bg0 label on a bright accent at
     2.8–4.5:1. --accent-fill follows --accent-ink so all eight selectable accents get a
     compliant fill without eight more declarations; give an accent block its own
     --accent-fill if the two ever need to diverge. */
  --accent-fill: var(--accent-ink); --on-accent-fill: #FFFFFF;
  --ok-fill: var(--ok); --on-ok-fill: #FFFFFF;
  --warn-fill: var(--amber); --on-warn-fill: #FFFFFF;
  --danger-fill: var(--danger); --on-danger-fill: #FFFFFF;
  --chart-1: #4F46E5; --chart-2: #0D9488; --chart-3: #64748B;
  /* Elevation scale (§9.1). Depth is information, so the surfaces do not all sit at one depth:
     --elev-1 is a flyout anchored to the control that opened it (a lookup list) and barely leaves
     the page; --elev-2 is something transient floating over content (a toast); --elev-3 is a surface
     that has taken the viewport over a dimmed backdrop. Drawer and modal share level 3 deliberately
     — both are entered through a backdrop, and reading one as shallower would misdescribe how they
     behave. Blur and alpha grow together, because a shadow that spreads without softening reads as
     a border rather than as height. */
  --elev-1: 0 4px 12px rgba(13, 25, 48, .16);
  --elev-2: 0 10px 28px rgba(13, 25, 48, .21);
  --elev-3: 0 18px 50px rgba(13, 25, 48, .25);
  color-scheme: light;
}

* { box-sizing: border-box; }

/* Themed, 3px scrollbars that reveal only while the container is hovered.
   The thumb is transparent at rest and picks up the theme colour on hover;
   WebView2/Chromium honour the exact 3px, Firefox falls back to "thin". */
* { scrollbar-width: thin; scrollbar-color: transparent transparent; }
*:hover { scrollbar-color: var(--scrollbar-thumb) transparent; }
*::-webkit-scrollbar { width: 3px; height: 3px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-corner { background: transparent; }
*::-webkit-scrollbar-thumb { background: transparent; border-radius: 3px; }
*:hover::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); }
*:hover::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }

html, body { margin: 0; padding: 0; height: 100%; }
body {
  font-family: var(--font); font-size: var(--fs); color: var(--ink);
  background: linear-gradient(155deg, var(--bg1), var(--bg0) 70%);
  overflow: hidden;
}

/* Selectable backdrop patterns (data-bg): each is one monochrome SVG used as a mask
   over --bg-pattern-ink, so a single asset adapts to both themes and always sits at
   ~5% opacity — far below text and accent contrast. The glass panels blur it. */
:root {
  --bg-pattern-ink: rgba(43, 72, 128, .07);
  --bgpat-circuit: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cg fill='none' stroke='white' stroke-width='1.3'%3E%3Cpath d='M18 34h84l26 26v52'/%3E%3Cpath d='M128 236v-60l30-30h58'/%3E%3Cpath d='M234 44v40l-24 24h-36'/%3E%3Cpath d='M36 210h48l22-22v-34'/%3E%3Cpath d='M18 120h44'/%3E%3Cpath d='M196 210h40'/%3E%3Ccircle cx='18' cy='34' r='4'/%3E%3Ccircle cx='128' cy='112' r='4'/%3E%3Ccircle cx='128' cy='236' r='4'/%3E%3Ccircle cx='216' cy='146' r='4'/%3E%3Ccircle cx='234' cy='44' r='4'/%3E%3Ccircle cx='174' cy='108' r='4'/%3E%3Ccircle cx='36' cy='210' r='4'/%3E%3Ccircle cx='106' cy='154' r='4'/%3E%3Ccircle cx='18' cy='120' r='4'/%3E%3Ccircle cx='62' cy='120' r='4'/%3E%3Ccircle cx='196' cy='210' r='4'/%3E%3C/g%3E%3Cg fill='white'%3E%3Ccircle cx='18' cy='34' r='1.4'/%3E%3Ccircle cx='128' cy='236' r='1.4'/%3E%3Ccircle cx='234' cy='44' r='1.4'/%3E%3Ccircle cx='36' cy='210' r='1.4'/%3E%3Ccircle cx='62' cy='120' r='1.4'/%3E%3Ccircle cx='196' cy='210' r='1.4'/%3E%3C/g%3E%3C/svg%3E");
  --bgpat-grid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96'%3E%3Cpath d='M0 .5h96M.5 0v96' stroke='white' stroke-width='1'/%3E%3Cpath d='M0 24.5h96M0 48.5h96M0 72.5h96M24.5 0v96M48.5 0v96M72.5 0v96' stroke='white' stroke-width='1' opacity='.35'/%3E%3C/svg%3E");
  --bgpat-hex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='48'%3E%3Cpath d='M14 48V32L0 24V8L14 0l14 8v16L14 32' fill='none' stroke='white' stroke-width='1' stroke-linejoin='round' stroke-linecap='round'/%3E%3C/svg%3E");
  --bgpat-radar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='360'%3E%3Cg fill='none' stroke='white' stroke-width='1.1'%3E%3Ccircle cx='180' cy='180' r='30'/%3E%3Ccircle cx='180' cy='180' r='60'/%3E%3Ccircle cx='180' cy='180' r='90'/%3E%3Cpath d='M180 64v232M64 180h232' opacity='.55'/%3E%3C/g%3E%3C/svg%3E");
}
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg-pattern-ink: rgba(56, 96, 168, .09); } }
:root[data-theme="dark"] { --bg-pattern-ink: rgba(56, 96, 168, .09); }
:root[data-bg] body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: var(--bg-pattern-ink);
  -webkit-mask-repeat: repeat; mask-repeat: repeat;
}
:root[data-bg="circuit"] body::before { -webkit-mask-image: var(--bgpat-circuit); mask-image: var(--bgpat-circuit); -webkit-mask-size: 260px; mask-size: 260px; }
:root[data-bg="grid"] body::before { -webkit-mask-image: var(--bgpat-grid); mask-image: var(--bgpat-grid); -webkit-mask-size: 96px; mask-size: 96px; }
:root[data-bg="hex"] body::before { -webkit-mask-image: var(--bgpat-hex); mask-image: var(--bgpat-hex); -webkit-mask-size: 72px; mask-size: 72px; }
:root[data-bg="radar"] body::before { -webkit-mask-image: var(--bgpat-radar); mask-image: var(--bgpat-radar); -webkit-mask-size: 360px; mask-size: 360px; }
/* "None" is flatter than the default, not a synonym for it: no pattern AND no gradient — one
   plane of the theme's base colour. After the [data-bg] rules above so it wins the ::before. */
:root[data-bg="none"] body { background: var(--bg0); }
:root[data-bg="none"] body::before { content: none; }
/* The content pane's own dot grid goes too: it is a background decoration like the rest, and
   an option called None that still draws dots is an option that lies. */
:root[data-bg="none"] .content { background-image: none; }
:root[data-theme="contrast"] body::before { content: none; }

/* Background picker swatches: the same masks at swatch scale, tinted with --muted
   so the preview is legible at 26px tall. */
.bgsw { position: relative; overflow: hidden; }
.bgsw::after { content: ""; position: absolute; inset: 0; background: var(--muted); -webkit-mask-repeat: repeat; mask-repeat: repeat; -webkit-mask-position: center; mask-position: center; }
.bgsw.circuit::after { -webkit-mask-image: var(--bgpat-circuit); mask-image: var(--bgpat-circuit); -webkit-mask-size: 72px; mask-size: 72px; }
.bgsw.grid::after { -webkit-mask-image: var(--bgpat-grid); mask-image: var(--bgpat-grid); -webkit-mask-size: 26px; mask-size: 26px; }
.bgsw.hex::after { -webkit-mask-image: var(--bgpat-hex); mask-image: var(--bgpat-hex); -webkit-mask-size: 18px; mask-size: 18px; }
.bgsw.radar::after { -webkit-mask-image: var(--bgpat-radar); mask-image: var(--bgpat-radar); -webkit-mask-size: 46px; mask-size: 46px; }
/* "None"'s swatch drops the tint layer entirely, so it reads flatter than the default's. */
.bgsw.none { background: var(--bg0) !important; }
.bgsw.none::after { content: none; }
/* The reset carries SC 2.5.8's floor so a button is a 24px target before anyone styles it. Several
   call sites write a bare <button> with `padding:0 0 0 4px` at 10px — about 12px tall — and those
   are compliant here rather than at each site. Every classed control is already taller, so this only
   ever lifts the ones nobody sized. */
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; padding: 0;
  min-height: 24px; min-width: 24px; }
/* Every text control is on-system by default — no .field wrapper or inline
   style required. Class-scoped controls (.field, .palette-input) override. */
input, select, textarea {
  font: inherit; color: var(--ink);
  height: 32px; border-radius: 8px; background: var(--panel2);
  border: 1px solid var(--line-strong); padding: 0 10px; font-size: var(--fs-sm);
}
textarea { height: auto; min-height: 70px; padding: 8px 10px; }
/* Accent the border on focus, but only drop the outline for pointer focus: a keyboard user must
   keep a real focus ring (WCAG 2.2 AA — 2.4.7 focus visible, 1.4.1 not by colour alone). The
   :focus-visible rule below then supplies the ring. */
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
input:focus:not(:focus-visible), select:focus:not(:focus-visible), textarea:focus:not(:focus-visible) { outline: none; }
/* The native dropdown body Chromium paints for <select>: keep it on-theme rather than
   system-default. color-scheme handles the chrome; these handle the option rows. */
select option, select optgroup {
  background: var(--panel-solid); color: var(--ink);
  font-family: var(--font); font-size: var(--fs-sm);
}
select option:disabled { color: var(--faint); }
select { accent-color: var(--accent); }
input[type="radio"] {
  height: auto; width: auto; padding: 0; border: none; background: none; accent-color: var(--accent);
}
input[type="file"] { height: auto; padding: 6px 10px; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* Reduced motion, from the OS preference. Two layers, and both are deliberate: re-pointing the
   duration scale is the MECHANISM — one place to retune, and it reads as "motion is instant here"
   rather than as an override fighting the rules — while the blanket sweep stays as the GUARANTEE,
   because an inline style or a future rule that animates without going through a token would
   otherwise slip past. The tokens make the intent legible; the sweep makes it true.
   An explicit in-app "false" wins, matching reduced transparency below: someone who has asked their
   system for less motion but turned it back on inside the app has made a deliberate choice, and
   until now the app had no way to honour it. */
@media (prefers-reduced-motion: reduce) {
  :root:not([data-reduce-motion="false"]) {
    --dur-fast: var(--dur-instant); --dur-base: var(--dur-instant); --dur-slow: var(--dur-instant);
  }
  :root:not([data-reduce-motion="false"]) *,
  :root:not([data-reduce-motion="false"]) *::before,
  :root:not([data-reduce-motion="false"]) *::after {
    animation-duration: var(--dur-instant) !important;
    transition-duration: var(--dur-instant) !important;
  }
}

/* ===== Display & accessibility options (PRD §9.1/§15) =====
   Explicit user toggles; each attribute is set by the shell and persisted per user.
   Reduced transparency swaps every glass surface for its solid equivalent — glass must
   never cost readability. */
:root[data-reduce-transparency="true"] {
  --blur: 0px;
  --panel: var(--panel-solid);
  --panel2: var(--panel-solid);
}
/* The OS preference is honoured too, so someone who has already asked their system for
   solid surfaces gets them on first paint rather than after finding the in-app toggle —
   the same courtesy prefers-reduced-motion gets above. An explicit "false" still wins:
   turning transparency back on inside the app is a deliberate choice. */
@media (prefers-reduced-transparency: reduce) {
  :root:not([data-reduce-transparency="false"]) {
    --blur: 0px;
    --panel: var(--panel-solid);
    --panel2: var(--panel-solid);
  }
}
/* The in-app toggle, same two layers as the OS preference above. */
:root[data-reduce-motion="true"] {
  --dur-fast: var(--dur-instant); --dur-base: var(--dur-instant); --dur-slow: var(--dur-instant);
}
:root[data-reduce-motion="true"] *,
:root[data-reduce-motion="true"] *::before,
:root[data-reduce-motion="true"] *::after {
  animation-duration: var(--dur-instant) !important;
  transition-duration: var(--dur-instant) !important;
}
:root[data-density="compact"] {
  --pad: 10px; --gap: 8px; --fs: 12.5px; --fs-sm: 11px; --r-lg: 10px; --r-md: 8px;
}


/* ===== Case workspace chrome (Phase 0 concept) =====
   The case stays the focus: every case-child view renders inside this header + tab row. */
.case-head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 14px; margin-bottom: 16px;
  background: var(--panel); backdrop-filter: blur(var(--blur)); border: 1px solid var(--line); border-radius: var(--r-lg);
}
/* The back affordance is the same wherever it appears — the case header, an evidence,
   party, location, or board detail. One rule, not a per-page reinvention. */
.ch-back {
  width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; color: var(--muted);
  border: 1px solid var(--line); text-decoration: none; background: none; cursor: pointer; flex: 0 0 auto;
}
.ch-back:hover { background: var(--accent-wash); color: var(--ink); }
.case-head .ch-title { font-family: var(--font); font-weight: 650; font-size: 16px; }
.case-head .ch-num { font-family: var(--mono); font-size: 11px; color: var(--faint); }
/* Tab strips — one style everywhere: underline tabs (.case-tabs pulls up under the case head) */
.case-tabs, .tabs { display: flex; gap: 2px; margin: 0 0 16px; border-bottom: 1px solid var(--line); overflow-x: auto; }
.case-tabs { margin-top: -6px; }
.case-tabs button, .tabs button {
  padding: 8px 13px; font-size: var(--fs-sm); color: var(--muted); border-bottom: 2px solid transparent;
  white-space: nowrap; background: none; border-top: none; border-left: none; border-right: none; cursor: pointer;
}
.case-tabs button:hover, .tabs button:hover { color: var(--ink); }
/* Keyed on aria-selected, which is what a tablist actually sets — the case strip moved to it
   with the ARIA work and this selector was left on aria-current, so the case workspace was the
   one place in the app that never showed which tab you were on. */
.case-tabs button[aria-selected="true"], .tabs button[aria-selected="true"] { color: var(--accent-ink); border-bottom-color: var(--accent); font-weight: 600; }
/* Tab arrangement (§8.4 "rearranged"): the ⋮ pins to the strip's right edge rather than
   sitting in the tablist, which may contain only tabs. The strip keeps room for it so a
   long tab row scrolls UNDER the control instead of beneath it. */
.tabs-bar { position: relative; }
.tabs-bar > .case-tabs, .tabs-bar > .tabs { padding-right: 30px; }
.tabs-cust {
  position: absolute; right: 0; top: 3px; width: 24px; height: 24px; border-radius: 6px;
  display: grid; place-items: center; color: var(--faint); cursor: pointer; background: var(--bg0);
  border: none; font-size: 14px; line-height: 1;
}
.tabs-cust:hover, .tabs-cust[aria-expanded="true"] { background: var(--accent-wash); color: var(--ink); }
.tabs-arrange { margin: -8px 0 14px; }

/* concept column layouts */
.cols { display: grid; gap: var(--gap); align-items: start; }
.cols.c2 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
.cols.c3 { grid-template-columns: 275px minmax(0, 1fr) 275px; }
/* Two equal columns of independent panels (e.g. Setup → Case configs). */
.cols.c2e { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Three equal columns of independent panels (e.g. System settings), room to grow. */
.cols.c3e { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cols.c4 { grid-template-columns: 260px minmax(0, 1fr) 260px 260px; }
/* Main content plus two side rails (e.g. intake Request tab). */
.cols.c2r { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); }
/* Grid and flex items refuse to shrink below their content by default
   (min-width: auto) — without these, one long row in a side column slides
   the whole column under its neighbor instead of wrapping or truncating. */
.cols > *, .grid > * { min-width: 0; }
.rowline > * { min-width: 0; }
.rowline > .chip, .rowline > .tb-avatar, .rowline > svg { flex-shrink: 0; }
@media (max-width: 1100px) { .cols.c2, .cols.c2e, .cols.c3, .cols.c3e, .cols.c2r { grid-template-columns: 1fr; } }
/* Four columns need more room: drop the 4th (Access summary) under the activity feed first,
   then stack fully on narrow screens. */
@media (max-width: 1400px) { .cols.c4 { grid-template-columns: 260px minmax(0, 1fr) 260px; } }
@media (max-width: 1100px) { .cols.c4 { grid-template-columns: 1fr; } }

/* Text truncation: when a label runs out of room it ends in an ellipsis instead of
   wrapping into a tall stack. `.trunc` is the one-line utility; `.clamp-2` allows two.
   The growing text of a list row (the flex:1 child of a .rowline) truncates by default —
   that is the pattern that otherwise collapses into a one-word-per-line column. */
.trunc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clamp-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
.rowline > span[style*="flex:1"] {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* avatars */
.avatar {
  width: 26px; height: 26px; border-radius: 50%; display: inline-grid; place-items: center; font-size: 10px; font-weight: 700;
  border: 1px solid var(--line-strong); background: var(--accent-wash); color: var(--accent-ink); flex-shrink: 0;
}
.avatar.amb { background: var(--amber-wash); color: var(--amber); }

/* masked / restricted note block */
.mask {
  position: relative; border: 1px dashed var(--line-strong); border-radius: var(--r-md); padding: 14px;
  background: repeating-linear-gradient(45deg, transparent, transparent 6px, var(--accent-wash) 6px, var(--accent-wash) 7px);
}
.mask .m-line { display: flex; gap: 8px; align-items: center; color: var(--muted); font-size: var(--fs-sm); }

/* dashed review-note card */
.note-card {
  border: 1px dashed var(--line-strong); border-radius: var(--r-md); padding: 12px 14px;
  font-size: var(--fs-sm); color: var(--muted); line-height: 1.55; background: transparent;
}
.note-card b { color: var(--ink); }

/* ===== Circuit reconnect / resume overlay =====
   Styles the custom #components-reconnect-modal element from the web host. Blazor drives
   the state by swapping components-reconnect-{show,hide,failed,rejected} classes. */
.reconnect-overlay {
  display: none; position: fixed; inset: 0; z-index: 2000;
  background: color-mix(in srgb, var(--bg0) 62%, transparent); backdrop-filter: blur(calc(var(--blur) / 3));
  justify-content: center; align-items: center; padding: 24px;
}
.reconnect-overlay.components-reconnect-show,
.reconnect-overlay.components-reconnect-failed,
.reconnect-overlay.components-reconnect-rejected { display: flex; }
.reconnect-card {
  background: var(--panel-solid); border: 1px solid var(--line-strong); border-radius: var(--r-lg);
  box-shadow: var(--elev-3); padding: 26px 30px; max-width: 380px; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.reconnect-card h2 { margin: 0; font-size: 16px; color: var(--ink); }
.reconnect-card p { margin: 0; font-size: var(--fs-sm); color: var(--muted); }
.reconnect-eyebrow {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--accent-ink);
}
.reconnect-spinner {
  width: 26px; height: 26px; border-radius: 50%;
  border: 2.5px solid var(--line-strong); border-top-color: var(--accent);
  animation: reconnect-spin 0.9s linear infinite;
}
@keyframes reconnect-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .reconnect-spinner { animation: none; } }
:root[data-reduce-motion="true"] .reconnect-spinner { animation: none; }
/* State-dependent content: spinner + progress text while retrying; message + resume
   button once the circuit is gone. */
.reconnect-overlay .reconnect-when-failed { display: none; }
.reconnect-overlay.components-reconnect-failed .reconnect-when-failed,
.reconnect-overlay.components-reconnect-rejected .reconnect-when-failed { display: inline-flex; }
.reconnect-overlay.components-reconnect-failed p.reconnect-when-failed,
.reconnect-overlay.components-reconnect-rejected p.reconnect-when-failed { display: block; }
.reconnect-overlay.components-reconnect-failed .reconnect-when-reconnecting,
.reconnect-overlay.components-reconnect-rejected .reconnect-when-reconnecting,
.reconnect-overlay.components-reconnect-failed .reconnect-spinner,
.reconnect-overlay.components-reconnect-rejected .reconnect-spinner { display: none; }

/* ===== Shell ===== */
.app-shell { display: flex; flex-direction: column; height: 100vh; }
.titlebar {
  display: flex; align-items: center; gap: 10px; height: 46px; padding-left: 14px; flex-shrink: 0;
  background: var(--panel); backdrop-filter: blur(var(--blur)); border-bottom: 1px solid var(--line);
  user-select: none; -webkit-app-region: drag; app-region: drag;
  /* Above the content's stacking context so titlebar dropdowns (e.g. the service-health
     popover) paint over the page instead of underneath it. */
  position: relative; z-index: 60;
}
.titlebar button, .titlebar input { -webkit-app-region: no-drag; app-region: no-drag; }
.tb-brand { display: flex; align-items: center; gap: 9px; font-weight: 700; letter-spacing: .12em; }
.tb-brand .pi { color: var(--accent-ink); }
/* no-drag so the double-click (About) reaches the DOM in the desktop WebView2, where
   the titlebar's app-region:drag would otherwise hand all mouse input to the OS. */
.tb-workspace { font-size: var(--fs-sm); color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; white-space: nowrap; -webkit-app-region: no-drag; app-region: no-drag; }
.tb-nav { display: flex; align-items: center; gap: 4px; margin-left: auto; -webkit-app-region: no-drag; app-region: no-drag; }
.tb-search {
  flex: 1; max-width: 430px; min-width: 0; margin: 0 auto; display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; color: var(--faint);
  background: var(--panel2); font-size: var(--fs-sm); -webkit-app-region: no-drag; app-region: no-drag;
  /* One line, always (D-339): a narrow window shrinks this bar, and a bar that wrapped onto
     two lines grew the title bar and pushed the page under it down. The label clips with an
     ellipsis, the shortcut hint goes first, and below a phone's width only the glass stays. */
  white-space: nowrap; overflow: hidden;
}
.tb-search-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: left; }
.tb-search-key { flex: none; }
@media (max-width: 980px) { .tb-search-key { display: none; } }
@media (max-width: 720px) { .tb-search-label { display: none; } .tb-search { flex: none; max-width: none; } }
/* Back/refresh hug the search bar; the Online pill sits 1rem to its right and the
   remaining space flexes before the right-hand button cluster. */
.tb-nav + .tb-search { margin: 0 0 0 6px; }
.tb-sep { width: 1px; height: 18px; background: var(--line); margin: 0 5px; flex: none; }
.tb-right { display: flex; align-items: center; gap: 4px; margin-left: auto; -webkit-app-region: no-drag; app-region: no-drag; }
.tb-icon { width: 34px; height: 34px; border-radius: 8px; display: grid; place-items: center; color: var(--muted); }
.tb-icon:hover { background: var(--accent-wash); color: var(--ink); }
/* The language trigger carries a flag plus a code, so it earns its width — everything else
   about it (height, radius, hover) stays a tb-icon. */
.tb-icon.tb-lang { width: auto; padding: 0 8px; display: inline-flex; align-items: center; gap: 6px; }
/* The privacy lock stays accent-colored even on hover — its whole point is to not be
   mistaken for the amber bell beside it. */
.tb-icon.lock, .tb-icon.lock:hover { color: var(--accent-ink); }
.tb-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--accent-wash); border: 1px solid var(--line-strong); display: grid; place-items: center; font-size: 11px; font-weight: 700; color: var(--accent-ink); }
.health { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); padding: 0 8px; }
.health i { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); display: inline-block; }
.health.down i { background: var(--danger); }
.win-controls { display: flex; height: 46px; }
.win-controls button { width: 46px; display: grid; place-items: center; color: var(--muted); }
.win-controls button:hover { background: var(--accent-wash); color: var(--ink); }
.win-controls button.close:hover { background: #C42B1C; color: #fff; }

.app-body { display: flex; flex: 1; min-height: 0; }
.rail {
  width: 218px; flex-shrink: 0; display: flex; flex-direction: column; padding: 10px 8px; gap: 2px;
  background: var(--panel2); backdrop-filter: blur(var(--blur)); border-right: 1px solid var(--line);
  overflow-y: auto; overflow-x: hidden; transition: width var(--dur-base) var(--ease-standard);
}
.rail.collapsed { width: 58px; }
.rail-label { font-size: 10px; font-family: var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--faint); padding: 22px 12px 4px; white-space: nowrap; }
.rail.collapsed .rail-label { visibility: hidden; height: 10px; padding: 4px; }
/* Group headers are buttons (D-339): each group folds, and the folded set follows the person.
   In the icon-only rail the headers are hidden, so every group stays open there. */
button.rail-label { display: flex; align-items: center; gap: 6px; width: 100%; background: none; border: none; cursor: pointer; text-align: left; }
button.rail-label:hover { color: var(--muted); }
.rail-chev { display: inline-flex; margin-left: auto; transition: transform var(--dur-base) var(--ease-standard); }
.rail-label.closed .rail-chev { transform: rotate(-90deg); }
.rail-group-links { display: contents; }
.rail-group-links.closed { display: none; }
.rail.collapsed .rail-chev { display: none; }
.rail a.nav, .rail button.nav {
  display: flex; align-items: center; gap: 11px; padding: 8px 11px; border-radius: 8px; color: var(--muted);
  font-size: var(--fs); text-decoration: none; white-space: nowrap; width: 100%; text-align: left; cursor: pointer;
}
.rail a.nav svg, .rail button.nav svg { flex-shrink: 0; }
.rail a.nav:hover, .rail button.nav:hover { background: var(--accent-wash); color: var(--ink); }
.rail a.nav.active { background: var(--accent-wash); color: var(--accent-ink); font-weight: 600; box-shadow: inset 2px 0 0 var(--accent); }
/* Collapse the label visually but keep it in the accessibility tree: `display:none` would strip
   the link's only text (the icons are aria-hidden), leaving a screen reader with a row of unnamed
   links (§10). Clipping keeps the name while occupying no space. */
.rail.collapsed .nav span {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* Standard list pager (docs/UI-Design.md): controls sized and drawn exactly like the
   case-header back button — 30px squares with 16px stroked SVG chevrons. */
.pager { display: flex; align-items: center; gap: 6px; justify-content: flex-end; margin-top: 10px; flex-wrap: wrap; }
.pager select { font-size: 11.5px; height: 30px; padding: 0 6px; }
.pager button.pg {
  min-width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--line);
  display: grid; place-items: center; padding: 0; color: var(--muted);
  font-size: 12.5px; line-height: 1; cursor: pointer;
}
.pager button.pg.num { padding: 0 7px; }
/* Page numbers fuse into one segmented control: shared outer border, hairline dividers. */
.pager .pg-group { display: inline-flex; height: 30px; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.pager .pg-group button.pg { border: none; border-radius: 0; height: 100%; }
.pager .pg-group button.pg + button.pg { border-left: 1px solid var(--line); }
.pager button.pg:hover:not(:disabled) { background: var(--accent-wash); color: var(--ink); }
.pager button.pg:disabled { opacity: .35; cursor: default; }
.pager button.pg[aria-current="page"] { background: var(--accent-wash); color: var(--accent-ink); font-weight: 700; border-color: var(--line-strong); }

/* Icon-only collapse toggle pinned to the rail's top-right; centered when collapsed. */
.rail-collapse {
  align-self: flex-end; width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0;
  display: grid; place-items: center; color: var(--faint); cursor: pointer;
}
.rail.collapsed .rail-collapse { align-self: center; }
.rail-collapse:hover { background: var(--accent-wash); color: var(--ink); }

.content {
  flex: 1; min-width: 0; overflow: auto; padding: var(--pad) clamp(12px, 1.6vw, 24px) 28px;

  /* The dot grid is COUNTER-SCALED against the text-size zoom below, so it renders at the same
     24px pitch and 1px dot whatever size the type is set to. `zoom` scales an element's own
     painting, backgrounds included, so without this the decoration grew with the words — at the
     largest step the dots were nearly twice the size and pitch, which reads as the page itself
     being magnified rather than the text being made readable. Dividing both the tile and the dot
     radius by the same factor lands them back on their real pixel sizes. */
  background-image: radial-gradient(
    var(--line) calc(1px / var(--content-zoom, 1)),
    transparent calc(1px / var(--content-zoom, 1)));
  background-size: calc(24px / var(--content-zoom, 1)) calc(24px / var(--content-zoom, 1));
}
.content-inner { max-width: 1500px; margin: 0 auto; }
/* Full-width layout option (§16): the content fills the pane minus the rail and padding. */
:root[data-wide="true"] .content-inner { max-width: none; }
/* Text size (accessibility): five steps above the default, scaling everything inside the
   main pane proportionally (the app's type ramp is pixel-based, so a font-size boost alone
   would skip most text). The rail and top bar are outside .content and keep their size. */
:root[data-text-scale="1"] .content { zoom: 1.15; --content-zoom: 1.15; }
:root[data-text-scale="2"] .content { zoom: 1.3; --content-zoom: 1.3; }
:root[data-text-scale="3"] .content { zoom: 1.45; --content-zoom: 1.45; }
:root[data-text-scale="4"] .content { zoom: 1.6; --content-zoom: 1.6; }
:root[data-text-scale="5"] .content { zoom: 1.75; --content-zoom: 1.75; }
/* With the extra room of full-width, let the three-column side rails grow too (§16).
   Scoped above the collapse breakpoint so narrow panes still stack to one column. */
@media (min-width: 1101px) {
    /* Full width earns the side rails real share: at least 18% of the row each (owner ask,
       2026-08-29 — 15% proved indistinguishable from the old fixed width on real monitors),
       never narrower than their fixed default; minmax picks whichever is larger, so ordinary
       widths look exactly as before and big monitors stop starving the rails. */
    :root[data-wide="true"] .cols.c3 { grid-template-columns: minmax(330px, 18%) minmax(0, 1fr) minmax(330px, 18%); }
    :root[data-wide="true"] .cols.c4 { grid-template-columns: minmax(300px, 18%) minmax(0, 1fr) minmax(300px, 18%) minmax(300px, 18%); }
    /* Prose tiles get a third column only when the shell is full-width; two is the
       deliberate default, because three narrow columns of paragraphs read worse than two. */
    :root[data-wide="true"] .tiles.pair { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ===== Type & structure ===== */
.eyebrow { font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--accent-ink); }
h1.page { font-size: 21px; font-weight: 650; margin: 2px 0; }
h2.sec { font-size: 15px; font-weight: 650; margin: 0 0 10px; }
.page-head { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 18px; flex-wrap: wrap; }
.page-head .sub { color: var(--muted); font-size: var(--fs-sm); margin-top: 3px; }
.page-head .actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.mono { font-family: var(--mono); font-size: .92em; }
.num { font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); } .faint { color: var(--faint); }
mark { background: var(--accent-wash); color: var(--accent-ink); border-radius: 2px; padding: 0 1px; }

/* ===== Components ===== */
/* .btn renders on <button> and <a> alike — never underlined, cursor always a pointer. */
/* min-height/min-width rather than height: SC 2.5.8 wants a 24px floor, and eight call sites trim
   this button's padding inline to fit a dense row — `padding:1px 6px` at 10px lands at 16px, which
   is where the target-size failures came from. A floor survives those overrides, and unlike a fixed
   height it still lets the control grow with the reader's text-size setting instead of clipping its
   own label. No justify-content here: thirty call sites stretch a .btn to width:100% and expect its
   label to stay put. */
.btn {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 14px; border-radius: 8px;
  min-height: 24px; min-width: 24px;
  font-size: var(--fs-sm); font-weight: 600; border: 1px solid var(--line-strong); color: var(--ink); background: var(--panel2);
  text-decoration: none; cursor: pointer;
}
.btn:hover { background: var(--accent-wash); }
.btn.primary { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent-fill); font-weight: 700; }
.btn.primary:hover { filter: brightness(1.08); }
.btn.quiet { border-color: transparent; color: var(--muted); }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
/* A button that is working (D-339): the ring says so where a changed label alone did not reach
   a slow connection's screen. Disabled while busy, but drawn as busy rather than as unavailable. */
.btn-spinner {
  width: 13px; height: 13px; border-radius: 50%; flex: none;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: reconnect-spin .8s linear infinite;
}
.btn.busy[disabled] { opacity: .85; cursor: progress; }
@media (prefers-reduced-motion: reduce) { .btn-spinner { animation: none; } }
:root[data-reduce-motion="true"] .btn-spinner { animation: none; }

.panel { background: var(--panel); backdrop-filter: blur(var(--blur)); border: 1px solid var(--line); border-radius: var(--r-lg); padding: var(--pad); }
.panel.tight { padding: 12px; }
/* A panel that houses a destructive, firm-wide action (e.g. the clearance lockdown):
   two stacked danger washes read clearly red over the glass without losing readability,
   a danger-tinted border, and a red section title. Presentation only — enforcement stays
   server-side. */
.panel.danger {
  background: linear-gradient(var(--danger-wash), var(--danger-wash)), linear-gradient(var(--danger-wash), var(--danger-wash)), var(--panel);
  border-color: color-mix(in srgb, var(--danger) 55%, var(--line));
}
.panel.danger .xp-head .sec { color: var(--danger); }

/* Case-log upload previews (LogCaptureModal): a responsive gallery where image files show a live
   thumbnail and everything else shows a family-coloured file-type badge. Colours are derived from
   a per-card --fam via color-mix so the badge reads on both light and dark themes. */
.filethumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 10px; margin-top: 2px; }
.filecard { min-width: 0; }
.filethumb {
  --fam: var(--faint);
  height: 78px; border-radius: 8px; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  background: color-mix(in srgb, var(--fam) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--fam) 34%, transparent);
}
.filethumb.img { background: var(--panel-solid); border-color: var(--line); }
.filethumb.img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.filethumb .fg { font-size: 26px; line-height: 1; }
/* The extension label is small text, so it must clear WCAG contrast on light, dark, AND
   high-contrast themes — the raw family hex fails several combinations (e.g. slate on white,
   purple on navy). Blend the family hue toward the theme foreground: keeps a hint of family
   colour while guaranteeing contrast on any surface. The tint/border/emoji stay full --fam. */
.filethumb .ext { font-size: 9px; font-weight: 700; letter-spacing: .06em; color: color-mix(in srgb, var(--fam) 45%, var(--ink)); }
.filecard .fc-name { font-size: 11px; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filecard .fc-size { font-size: 10px; }
/* High-contrast theme strips subtle tints: its badge fill (14% over pure black) is near-invisible,
   so give the badge a solid theme edge to keep it readable as a bordered tile. */
:root[data-theme="contrast"] .filethumb:not(.img) { border-color: var(--line-strong); }

.grid { display: grid; gap: var(--gap); }
.cols-2 { display: grid; grid-template-columns: 2fr 1fr; gap: var(--gap); }
@media (max-width: 1100px) { .cols-2 { grid-template-columns: 1fr; } }

.chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; border-radius: 999px; padding: 2.5px 9px; border: 1px solid var(--line); color: var(--muted); white-space: nowrap; }
/* A chip is a status pill in most places and a filter button in the palette. Only the button form
   is a pointer target, so only it owes SC 2.5.8's 24px — the label form stays the compact pill it
   was, and the element type is the whole distinction. */
button.chip { min-height: 24px; min-width: 24px; justify-content: center; }
.chip .cdot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.chip.ok { background: var(--ok-wash); color: var(--ok); border-color: transparent; }
.chip.warn { background: var(--amber-wash); color: var(--amber); border-color: transparent; }
.chip.danger { background: var(--danger-wash); color: var(--danger); border-color: transparent; }
.chip.acc { background: var(--accent-wash); color: var(--accent-ink); border-color: transparent; }

.badge { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 10px; letter-spacing: .08em; border-radius: 6px; padding: 3px 8px; border: 1px solid var(--line-strong); color: var(--muted); white-space: nowrap; text-transform: uppercase; }
.badge.l0, .badge.l1 { background: var(--accent-wash); color: var(--accent-ink); border-color: transparent; }
.badge.l2, .badge.l3 { background: var(--amber-wash); color: var(--amber); border-color: transparent; }
.badge.l4, .badge.l5 { background: var(--danger-wash); color: var(--danger); border-color: transparent; }

/* Expandable side panel — maximize into a centered drawer, capped at 70% viewport */
.xp-head { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 10px; }
.xp-head .sec { margin: 0; }
.xp-max {
  width: 24px; height: 24px; border-radius: 6px; display: grid; place-items: center;
  color: var(--faint); border: 1px solid transparent; flex-shrink: 0;
}
.xp-max:hover { background: var(--accent-wash); color: var(--accent-ink); border-color: var(--line); }

/* The print control that follows the pointer from panel to panel (D-293). Fixed to the viewport
   and positioned by print.js, so it never has to be written into markup Blazor owns and may
   replace underneath it. Quiet until it is wanted — the clean view is the point — so it appears
   only once a panel is under the pointer, or focused, which is what keeps it reachable from the
   keyboard rather than making printing a mouse-only feature. z-index clears the maximize drawer's
   backdrop so a maximized panel can be printed too.

   Styled as .xp-max, because on a panel that has a header strip this sits IN that strip, one place
   left of the maximize control — it is one of the panel's buttons and should not announce itself
   as a floating chip on top of them. */
.panel-print {
  position: fixed; z-index: 70; width: 24px; height: 24px; border-radius: 6px;
  display: grid; place-items: center; cursor: pointer;
  color: var(--faint); background: none; border: 1px solid transparent;
}
.panel-print:hover { background: var(--accent-wash); color: var(--accent-ink); border-color: var(--line); }
.panel-print[hidden] { display: none; }
/* Sitting on the panel's own edge rather than inside its header (see print.js): it needs to
   occlude what is behind it, or a transparent glyph over a border reads as damage rather than
   as a control. */
.panel-print.on-edge { background: var(--panel-solid); border-color: var(--line); }

.xp-backdrop {
  position: fixed; inset: 0; z-index: 60; display: grid; place-items: center;
  background: rgba(4, 10, 22, .55); backdrop-filter: blur(2px);
}

/* The image viewer's stage (D-324): when the host wires a set to walk, prev/next ride the
   picture's own edges and stay invisible until the pointer arrives — the modern gallery
   idiom. Keyboard focus reveals them too; a control you can Tab to must not be a secret. */
.lb-stage { position: relative; }
.lb-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 68px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: 10px;
  background: color-mix(in srgb, var(--panel) 82%, transparent);
  color: var(--ink); font-size: 26px; line-height: 1; cursor: pointer;
  /* The duration token, not a literal: the token is what reduced-motion collapses to
   * instant, so a literal here was a fade that ignored the preference (D-330 ride-along —
   * caught by the a11y suite the gallery's own proof run did not include). */
  opacity: 0; transition: opacity var(--dur-fast) ease;
}
.lb-prev { left: 10px; }
.lb-next { right: 10px; }
.lb-stage:hover .lb-nav, .lb-nav:focus-visible { opacity: 1; }
.lb-nav:hover { background: var(--accent-wash); }
.xp-drawer {
  /* vh divided by the text-scale zoom (§19.5): the drawer renders inside the zoomed
     .content, where an uncompensated 70vh overflows the screen at the top text sizes. */
  width: min(70vw, 980px); max-height: calc(70vh / var(--content-zoom, 1));
  display: flex; flex-direction: column;
  box-shadow: var(--elev-3); outline: none;
}
.xp-drawer .xp-body { overflow: auto; min-height: 0; padding-right: 4px; }
/* Collapse/expand slide: the grid-rows 1fr↔0fr trick animates to content height
   without measuring it. .anim arms the transition on the first user toggle only. */
.xp-fold { display: grid; grid-template-rows: 1fr; }
.xp-fold.anim { transition: grid-template-rows var(--dur-slow) var(--ease-standard); }
.xp-fold.closed { grid-template-rows: 0fr; }
.xp-fold-inner { overflow: hidden; min-height: 0; }
/* A SmartLookup's results float absolutely below their input; inside a collapsible panel the
   fold's overflow:hidden clips them out of view (they vanish, so the lookup looks empty). Relax
   the clip on just that panel, and only while a dropdown is actually open — so the collapse
   animation is unaffected everywhere else. */
.xp-fold-inner:has(.lookup-results) { overflow: visible; }
/* Each glass .panel is its own stacking context (backdrop-filter), so the dropdown's z-index
   can't rise above a *sibling* panel that comes later in the column — the results would paint
   behind it. While a dropdown is open, lift its whole panel above its siblings (kept below the
   drawer/backdrop layer at z-index 50+, which never coexists with an open dropdown). */
.panel:has(.lookup-results) { position: relative; z-index: 40; }
/* The same escape for every other scroll container that can host a lookup. An absolutely
   positioned dropdown inside an overflow:auto ancestor doesn't float — it just extends the
   scrollable area, so the user has to scroll the container to reach the results. Each clip
   relaxes only while a dropdown is actually open, exactly like the fold rule above. */
.xp-drawer .xp-body:has(.lookup-results) { overflow: visible; }
.drawer:has(.lookup-results) { overflow-y: visible; }
.modal-backdrop > .panel:has(.lookup-results) { overflow-y: visible; }
.modal-body { overflow-y: auto; min-height: 0; }
.modal-body:has(.lookup-results) { overflow-y: visible; }
/* A shrunk panel un-shrinks while its dropdown is open — a 220px window can't show a
   240px result list — and folds back the moment the dropdown closes. */
.pb-wrap.pb-compact:has(.lookup-results) { max-height: none; overflow: visible; }
@media (prefers-reduced-motion: reduce) { .xp-fold.anim { transition: none; } }
:root[data-reduce-motion="true"] .xp-fold.anim { transition: none; }

/* Chip-shaped filter select — the concept's filter chips row, made interactive */
select.chip-select { height: 26px; width: auto; border-radius: 999px; font-size: 11px; font-weight: 600; padding: 0 8px; background: var(--panel2); border: 1px solid var(--line); color: var(--muted); }
select.chip-select:hover { border-color: var(--line-strong); color: var(--ink); }

/* Segmented control — the concept's view-format switcher (Tiles/List/Table etc.) */
.seg { display: inline-flex; border: 1px solid var(--line-strong); border-radius: 999px; overflow: hidden; background: var(--panel2); }
.seg button { padding: 5px 12px; font-size: var(--fs-sm); color: var(--muted); }
.seg button:hover { background: var(--accent-wash); color: var(--ink); }
.seg button[aria-pressed="true"] { background: var(--accent-wash); color: var(--accent-ink); font-weight: 600; }
.seg button + button { border-left: 1px solid var(--line); }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--gap); }
/* One-row strip (dashboard top): N tiles always share the row equally — 10 tiles means 10%
   each — instead of wrapping to a second line. Implicit equal columns, so hiding a tile in
   Customize just widens the rest. */
.stats.oneline { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); grid-template-columns: none; }
.stats.oneline .stat { min-width: 0; padding: 11px 12px; }
.stats.oneline .stat .lbl { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.stat { background: var(--panel); backdrop-filter: blur(var(--blur)); border: 1px solid var(--line); border-radius: var(--r-md); padding: 12px 14px; }
.stat .k { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat .lbl { font-size: 11px; color: var(--muted); margin-top: 2px; }
.stat.attn { box-shadow: inset 0 2px 0 var(--amber-dot); }

.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(285px, 1fr)); gap: 14px; }
/* Data-heavy tiles (intake): ~30% wider columns to fit creator, client, and contact info. */
.tiles.wide { grid-template-columns: repeat(auto-fill, minmax(370px, 1fr)); }
/* Laid out by an explicit column count rather than a minimum width, for tiles whose body is
   prose rather than a line or two: two abreast normally, three when the shell is running
   full-width, and one when there is not room for two honest columns. */
.tiles.pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 1100px) { .tiles.pair { grid-template-columns: 1fr; } }

/* Two panels that belong together, side by side (D-209): sign-in codes and change password are
   the same errand — how a person proves who they are — and somebody reading one usually wants the
   other. align-items:start so the shorter panel keeps its own height rather than being stretched
   to match its neighbour, which reads as an unfinished box rather than a short one. */
.profile-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; align-items: stretch; margin-bottom: 14px; }
@media (max-width: 980px) { .profile-pair { grid-template-columns: 1fr; } }

/* The profile page's two columns (D-282): account errands left, the card's text right. */
.profile-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 340px); gap: 14px; align-items: start; margin-bottom: 14px; }
@media (max-width: 1080px) { .profile-cols { grid-template-columns: 1fr; } }

.tile { background: var(--panel); backdrop-filter: blur(var(--blur)); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 14px; display: flex; flex-direction: column; gap: 9px; cursor: pointer; position: relative; text-decoration: none; color: var(--ink); }
.tile::before, .tile::after { content: ""; position: absolute; width: 13px; height: 13px; border: 2px solid transparent; border-radius: 2px; transition: border-color var(--dur-fast) var(--ease-standard); pointer-events: none; }
.tile::before { top: -4px; left: -4px; border-width: 2px 0 0 2px; }
.tile::after { bottom: -4px; right: -4px; border-width: 0 2px 2px 0; }
.tile:hover::before, .tile:hover::after { border-color: var(--accent); }
/* The record a deep link arrived about (a bell click, a shared URL): the corner brackets the
   hover state borrows are held on permanently, so the reader can tell which tile the link meant
   without the page inventing a second highlight language. */
.tile.linked-target { border-color: var(--accent); }
.tile.linked-target::before, .tile.linked-target::after { border-color: var(--accent); }
.tile .t-num { font-family: var(--mono); font-size: 10.5px; color: var(--faint); letter-spacing: .05em; }
.t-title { font-weight: 650; line-height: 1.3; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
.tile .t-title { font-size: 14px; }
.tile .t-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.tile .t-foot { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); }
.t-team { display: flex; align-items: center; }
.t-team .tb-avatar { width: 22px; height: 22px; font-size: 9px; }
.t-team .tb-avatar + .tb-avatar { margin-left: -7px; }

.rowline { display: flex; align-items: center; gap: 10px; padding: 9px 8px; border-bottom: 1px solid var(--line); border-radius: 8px; font-size: var(--fs-sm); }
/* A row lights up on hover only when it is actionable: you can click it (a link/button/label, or a
   div made clickable with role=button, tabindex, or cursor:pointer) OR it carries an action control
   (a button or link, e.g. a list row with an Edit/Manage button). Read-only layout rows — key-value
   detail rows, static labels with no control — stay flat. Headers, toolbars, and banners opt out
   from their own markup (margin-bottom, border:0, their own background, or role=alert), and .nohover
   is the explicit escape hatch. Tiles use .tile, so they never take the row wash; an actionable
   .rowline inside a tile still does. (:has() degrades gracefully — where unsupported, only the
   directly-interactive rows light up.) */
.rowline:not(.nohover):not([role="alert"]):not([style*="background"]):not([style*="margin-bottom"]):not([style*="border:0"]):is(a, button, label, [role="button"], [tabindex], [style*="cursor:pointer"], :has(button), :has(a)):hover { background: var(--accent-wash); }
.rowline:last-child { border-bottom: 0; }
/* .lrow keeps a list row as a block (its inner content stacks) while still taking .rowline's
   separator, rounding, and hover — for rows whose body isn't a single horizontal line. */
.rowline.lrow { display: block; }
/* A message is a block of someone's writing, not a table row. It gets real vertical
   room, no hairline chopping one post from the next, and a body that wraps and breathes
   instead of being squeezed onto one line. */
.msgline { display: flex; align-items: flex-start; gap: 11px; padding: 11px 4px 13px; font-size: var(--fs-sm); border-radius: 8px; }
.msgline + .msgline { margin-top: 4px; }
.msgline:hover { background: var(--accent-wash); }
.msgline .msg-body { font-size: 12.5px; line-height: 1.55; margin-top: 3px; white-space: pre-wrap; overflow-wrap: anywhere; }

.kv { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 5px 12px; font-size: var(--fs-sm); }
.kv dt { color: var(--faint); } .kv dd { margin: 0; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }

/* border-collapse:separate (with zero spacing) keeps the hairline separators identical but lets
   the hovered row's outer cells carry a border-radius, so the hover fill has rounded corners. */
table.data { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-sm); }
table.data th { font-family: var(--mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line-strong); }
table.data td { padding: 9px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
/* Alternating row fill (D-276): a quiet zebra so the eye tracks a row across wide tables. The
   hover rule below outranks it (higher specificity), so an actionable row still lights up. */
table.data tbody tr:nth-child(even) td { background: var(--row-alt); }
/* Same rule as list rows: only an actionable row (clickable, or carrying a button/link such as a
   Manage/Edit action) fills on hover; a purely read-only table row stays flat. The radius sits on
   plain :hover — harmless with no fill, and it rounds the corners once an actionable row fills. */
table.data tr:is([role="button"], [tabindex], [style*="cursor:pointer"], :has(button), :has(a)):hover td { background: var(--accent-wash); }
table.data tr:hover td:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
table.data tr:hover td:last-child { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }

.tabs { display: flex; gap: 2px; margin: -6px 0 16px; border-bottom: 1px solid var(--line); overflow-x: auto; }
.tabs button { padding: 8px 13px; font-size: var(--fs-sm); color: var(--muted); border-bottom: 2px solid transparent; white-space: nowrap; }
.tabs button.active { color: var(--accent-ink); border-bottom-color: var(--accent); font-weight: 600; }

/* Timeline: who on the left of the node line, when + what on the right. The actor repeats
   on every node — a reviewer scanning down the line always sees who did each step. */
.tl { display: flex; flex-direction: column; }
.tl-row { display: grid; grid-template-columns: minmax(90px, 150px) 20px 1fr; column-gap: 8px; }
.tl-actor { display: flex; align-items: flex-start; justify-content: flex-end; gap: 7px; padding-top: 1px; min-width: 0; }
.tl-actor-id { display: flex; flex-direction: column; align-items: flex-end; min-width: 0; padding-top: 1px; }
.tl-actor-name { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.tl-actor-code { font-family: var(--mono); font-size: 9px; color: var(--faint); }
.tl-node { position: relative; }
.tl-node::before { content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--line-strong); }
.tl-row:first-child .tl-node::before { top: 5px; }
.tl-row:last-child .tl-node::before { bottom: auto; height: 5px; }
.tl-row:only-child .tl-node::before { display: none; }
.tl-node::after { content: ""; position: absolute; left: 50%; top: 5px; width: 11px; height: 11px; margin-left: -5.5px; border-radius: 50%; background: var(--panel-solid); border: 2.5px solid var(--accent); box-sizing: border-box; }
.tl-row.amb .tl-node::after { border-color: var(--amber-dot); }
/* Investigator note (§3): a distinct square accent marker, no background fill — reads as a note, not an event. */
.tl-row.note .tl-node::after { border-radius: 2px; border-color: var(--accent-ink); background: var(--accent-wash); }
.tl-content { padding: 0 0 26px; min-width: 0; }
.tl-row:last-child .tl-content { padding-bottom: 2px; }
.tl-when { font-family: var(--mono); font-size: 10px; color: var(--faint); }
.tl-title { font-weight: 600; font-size: 13px; margin: 2px 0; }
.tl-body { font-size: var(--fs-sm); color: var(--muted); line-height: 1.45; }

/* ===== States ===== */
.sk { border-radius: 8px; background: linear-gradient(100deg, var(--panel2) 40%, var(--accent-wash) 50%, var(--panel2) 60%); background-size: 200% 100%; animation: shimmer 1.4s infinite linear; }
@keyframes shimmer { to { background-position: -200% 0; } }
.state-center { min-height: 320px; display: grid; place-items: center; text-align: center; }
.state-center .icon-ring { width: 64px; height: 64px; border-radius: 50%; border: 1px solid var(--line-strong); display: grid; place-items: center; margin: 0 auto 16px; color: var(--faint); background: var(--panel2); font-size: 24px; }
.state-center h3 { font-size: 17px; margin: 0 0 6px; }
.state-center p { color: var(--muted); font-size: var(--fs-sm); max-width: 400px; margin: 0 auto 18px; line-height: 1.5; }

/* ===== Forms ===== */
.field label { display: block; font-family: var(--mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--accent-ink); margin: 12px 0 5px; }
.field input, .field select, .field textarea {
  width: 100%; height: 38px; border-radius: 10px; background: var(--panel2);
  border: 1px solid var(--line-strong); padding: 0 12px; font-size: var(--fs-sm); color: var(--ink);
}
.field textarea { height: auto; min-height: 70px; padding: 9px 12px; font-family: var(--font); }
/* A note explaining the control above it. A .field carries no bottom margin of its own —
   the gap between stacked fields comes from the NEXT label's top margin — so a hint has to
   supply its own space, and a negative one lands the text inside the control it explains. */
.field-hint { font-size: 10px; line-height: 1.45; color: var(--faint); margin: 6px 0 0; }
/* Marks a control the form will not submit without. Sits on the label so it is announced
   with the field's name rather than floating as decoration beside it. */
.field label .req { color: var(--danger); margin-left: 3px; }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--accent); }
.field input:focus:not(:focus-visible), .field select:focus:not(:focus-visible), .field textarea:focus:not(:focus-visible) { outline: none; }
/* Radios are NEVER full-width 38px controls, even inside a .field (which stretches its
   inputs). The higher-specificity ".field input[type=radio]" (0,2,1) beats ".field input"
   (0,1,1); flex:none stops a flex row from stretching them. */
input[type="radio"], .field input[type="radio"] {
  width: auto; height: auto; min-height: 0; padding: 0;
  border: none; background: none; accent-color: var(--accent); flex: none;
}

/* Every checkbox in the app renders as a toggle switch rather than a native check — one rule
   covers every existing and future checkbox with no per-component markup. The native <input>
   IS the track; ::before is the sliding knob, so screen readers still see a checkbox and the
   value binds exactly as before. ".field input[type=checkbox]" (0,2,1) beats ".field input"
   so an in-field toggle is never stretched to 38px. The .switch component keeps its own
   compact hidden-input styling (it wins on source order and is opacity:0 anyway). */
input[type="checkbox"], .field input[type="checkbox"] {
  appearance: none; -webkit-appearance: none; box-sizing: border-box;
  flex: none; margin: 0; padding: 0; position: relative; cursor: pointer;
  width: 34px; height: 20px; min-height: 0; border-radius: 999px; vertical-align: middle;
  background: var(--panel2); border: 1px solid var(--line);
  transition: background var(--dur-fast), border-color var(--dur-fast);
}
input[type="checkbox"]::before, .field input[type="checkbox"]::before {
  content: ""; position: absolute; top: 50%; left: 2px;
  width: 14px; height: 14px; border-radius: 50%; background: var(--faint);
  transform: translateY(-50%); transition: transform var(--dur-fast), background var(--dur-fast);
}
input[type="checkbox"]:checked, .field input[type="checkbox"]:checked {
  background: var(--accent-wash); border-color: var(--accent);
}
input[type="checkbox"]:checked::before, .field input[type="checkbox"]:checked::before {
  transform: translate(14px, -50%); background: var(--accent);
}
input[type="checkbox"]:disabled, .field input[type="checkbox"]:disabled {
  opacity: .5; cursor: not-allowed;
}
/* A button that shares a container with .field controls falls in line with them:
   the same 38px control height (a bare .btn is ~6px shorter, so it reads as sunken
   beside a .field input in a filter/form row), and standing room above so a button
   stacked below a field block never touches the control it follows. Inline margins
   at call sites still win where a tighter fit was deliberate. */
.field ~ .btn { min-height: 38px; margin-top: 10px; }

/* Available to assistive technology, invisible on screen. The rail's collapsed labels had
   their own copy of this trick scoped to one selector; this is the shared utility, used by the
   shell's loading live region and the canvas's text alternative. clip-path plus a 1px box is
   the form that survives a screen reader's virtual cursor — display:none and visibility:hidden
   remove the element from the accessibility tree entirely, which is the opposite of the point. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Skip link — visible on keyboard focus only */
.skip-link {
  position: absolute; left: 8px; top: -48px; z-index: 100; padding: 8px 14px;
  background: var(--accent-fill); color: var(--on-accent-fill); border-radius: 8px; font-weight: 700;
  font-size: 12px; text-decoration: none; transition: top var(--dur-fast) var(--ease-standard);
}
.skip-link:focus { top: 8px; }

/* Notification indicator */
.notif-dot { position: absolute; top: 6px; right: 6px; width: 8px; height: 8px; border-radius: 50%; background: var(--amber-dot); border: 2px solid var(--panel-solid); }
.tb-icon { position: relative; }
.rowline.unread { font-weight: 600; }

/* Command palette */
.palette-backdrop {
  position: fixed; inset: 0; z-index: 50; background: rgba(4, 10, 25, .45);
  backdrop-filter: blur(3px); display: flex; justify-content: center; align-items: flex-start; padding-top: 12vh;
}
.palette { width: min(560px, 92vw); max-height: 60vh; overflow-y: auto; box-shadow: var(--elev-3); }
.palette-input {
  width: 100%; height: 42px; border-radius: 10px; background: var(--panel2);
  border: 1px solid var(--accent); padding: 0 14px; font-size: 14px; color: var(--ink); font-family: var(--font);
}
.palette-input:focus:not(:focus-visible) { outline: none; }
.palette-hit { width: 100%; border-radius: 8px; }
.palette-hit:hover { background: var(--accent-wash); }

/* Right-side drawer (intake / event forms), openable from anywhere in the shell */
.drawer-backdrop {
  position: fixed; inset: 0; z-index: 60; background: rgba(4, 10, 25, .45);
  backdrop-filter: blur(3px); display: flex; justify-content: flex-end;
}
.drawer {
  width: min(440px, 96vw); height: 100%; overflow-y: auto; border-radius: 0;
  border-top: none; border-bottom: none; border-right: none;
  display: flex; flex-direction: column; gap: 2px;
  box-shadow: var(--elev-3);
  animation: drawer-in var(--dur-base) var(--ease-out);
}
@keyframes drawer-in { from { transform: translateX(18px); opacity: .4; } to { transform: none; opacity: 1; } }
.drawer-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }

/* The assistant as a companion (D-229): a flex sibling of the content inside .app-body,
   never a curtain over it. The pages narrow to make room and stay fully interactive;
   only a left border and elevation separate the two. Slides in like a drawer, but there
   is no backdrop and no modality to escape from.

   The width is the reader's (D-278): --companion-w is set inline by the grip below, in REM
   so the choice scales with the text-size setting, clamped 25–60rem by the script. The
   45vw cap stays — on a narrow window even the chosen width must not swallow the page. */
.companion {
  width: min(var(--companion-w, 430px), 45vw); flex-shrink: 0; min-height: 0;
  position: relative;
  display: flex; flex-direction: column; gap: 2px;
  border-radius: 0; border-top: none; border-bottom: none; border-right: none;
  box-shadow: var(--elev-3);
  animation: drawer-in var(--dur-base) var(--ease-out);
}
:root[data-reduce-motion="true"] .companion { animation: none; }

/* The companion's width grip: a slim strip on the panel's content edge. The whole strip is
   the pointer target; the ::after bar is the visible affordance, shown on hover and focus. */
.companion-grip {
  position: absolute; left: -2px; top: 0; bottom: 0; width: 10px;
  cursor: ew-resize; touch-action: none; z-index: 1; background: transparent;
}
.companion-grip::after {
  content: ""; position: absolute; left: 3px; top: 50%; transform: translateY(-50%);
  width: 3px; height: 44px; border-radius: 2px;
  background: var(--line-strong); opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}
.companion-grip:hover::after { opacity: 1; }
.companion-grip:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.companion-grip:focus-visible::after { opacity: 1; background: var(--accent); }
:root[data-reduce-motion="true"] .companion-grip::after { transition: none; }

/* The assistant's floating button (D-228). Rendered only when the firm has switched the
   assistant on; sits below the drawer/backdrop layer (60) so an open drawer covers it, and
   far below the privacy lock (2000). The brand tile IS the artwork (AssistantIcon draws
   its own ground, straight from the vector project), so the button stays transparent and
   contributes only placement, elevation, and the focus ring. */
.assistant-fab {
  position: fixed; right: 22px; bottom: 22px; z-index: 55;
  width: 52px; height: 52px; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: none;
  box-shadow: var(--elev-3); cursor: pointer; padding: 0;
  transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.assistant-fab:hover { transform: translateY(-2px); }
.assistant-fab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
:root[data-reduce-motion="true"] .assistant-fab { transition: none; }
:root[data-reduce-motion="true"] .assistant-fab:hover { transform: none; }

/* Dashboard chart marks (D-280): hovering a slice, column, or point brightens it and dims
   its siblings; clicking holds a selection that pulses once so the eye finds it, then keeps
   a steady emphasis. All state is classes toggled by the files.js delegate — the SVG itself
   stays whatever the server rendered. */
svg .mark { cursor: pointer; transition: opacity var(--dur-base) var(--ease-out), filter var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out); }
svg.has-hot .mark:not(.mark-hot):not(.mark-sel) { opacity: .4; }
svg .mark-hot { filter: brightness(1.18); }
/* Doughnut slices thicken; line points grow. Kept as two DISTINCT classes because a slice
   is positioned by its rotate(-90 60 60) ATTRIBUTE, whose center is in viewBox units —
   fill-box transform math on a slice reinterprets that center against the slice's own
   bounds and throws the whole ring outside the clip. Only the dots, which carry no
   attribute transform, may use fill-box scaling. */
svg circle.slice.mark-hot, svg circle.slice.mark-sel { stroke-width: 20; }
svg circle.dot { transform-box: fill-box; transform-origin: center; }
svg circle.dot.mark-hot, svg circle.dot.mark-sel { transform: scale(1.6); }
svg rect.mark { transform-box: fill-box; transform-origin: center bottom; }
svg rect.mark.mark-hot, svg rect.mark.mark-sel { transform: scaleY(1.04); }
svg .mark-sel { filter: brightness(1.22) drop-shadow(0 0 3px var(--accent)); animation: mark-pulse var(--dur-slow) var(--ease-out) 3; }
@keyframes mark-pulse { 50% { filter: brightness(1.5) drop-shadow(0 0 6px var(--accent)); } }
:root[data-reduce-motion="true"] svg .mark { transition: none; }
:root[data-reduce-motion="true"] svg .mark-sel { animation: none; }

/* The calendar's side column (D-285): saved calendars and the event inspector RIDE the
   scroll. In Agenda view a selection far down the list used to land in an inspector pinned
   to the page top — invisible until you scrolled all the way back. Sticky against the
   content scroller keeps it in view; a column taller than the viewport scrolls inside
   itself instead of clipping (vh divided by the text-scale zoom, §19.5). The lookup
   exception mirrors .drawer's: results must overflow the column, not clip against it. */
.cal-side { position: sticky; top: 0; align-self: start; max-height: calc(100vh / var(--content-zoom, 1) - 70px); overflow-y: auto; }
.cal-side:has(.lookup-results) { overflow-y: visible; }

/* The colleague profile card (D-282): a small fixed panel near the hovered avatar. Sits
   above page content but below modal/drawer surfaces, and closes before they open. */
.user-card {
  position: fixed; z-index: 65; width: 290px;
  background: var(--panel-solid); border: 1px solid var(--line-strong); border-radius: 12px;
  padding: 12px; box-shadow: var(--elev-3); font-size: 12px; color: var(--ink);
  animation: drawer-in var(--dur-base) var(--ease-out);
}
:root[data-reduce-motion="true"] .user-card { animation: none; }

/* The floating readout beside the pointer, and the held selection's line under the chart. */
.chart-tip {
  position: fixed; z-index: 1400; display: none; pointer-events: none;
  background: var(--panel-solid); border: 1px solid var(--line-strong); border-radius: 8px;
  padding: 5px 9px; font-size: 11px; font-weight: 600; color: var(--ink);
  box-shadow: var(--elev-2); white-space: nowrap;
}
[data-chart-info] { font-size: 11.5px; color: var(--ink); margin-top: 6px; min-height: 0; }
[data-chart-info]:empty { display: none; }
[data-chart-info] a { color: var(--accent-ink); font-weight: 600; text-decoration: none; }
[data-chart-info] a:hover { text-decoration: underline; }

/* Chat layout inside the assistant drawer: the log scrolls, the composer stays put. */
.chat-log { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 4px 4px 1px; display: flex; flex-direction: column; gap: 10px; }
.chat-row { display: flex; flex-direction: column; }
.chat-row.me { align-items: flex-end; }
.chat-meta { display: flex; gap: 6px; align-items: center; margin: 0 2px 3px; }
.bubble { border-radius: 12px; padding: 8px 11px; font-size: 11.5px; white-space: pre-wrap; overflow-wrap: break-word; }
.bubble.me { max-width: 88%; background: var(--accent-wash); border-bottom-right-radius: 4px; }
.bubble.ai { background: var(--panel2); border: 1px solid var(--line); border-bottom-left-radius: 4px; }
.chat-dots { display: inline-flex; gap: 4px; align-items: center; }
.chat-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); opacity: .3; animation: chat-dot 1.2s infinite; }
.chat-dots span:nth-child(2) { animation-delay: .2s; }
.chat-dots span:nth-child(3) { animation-delay: .4s; }
@keyframes chat-dot { 0%, 60%, 100% { opacity: .25; transform: none; } 30% { opacity: 1; transform: translateY(-2px); } }
:root[data-reduce-motion="true"] .chat-dots span { animation: none; opacity: .7; }
.drawer-head h2 { margin: 0; }
.drawer-head .x { margin-left: auto; }
:root[data-reduce-motion="true"] .drawer { animation: none; }

/* Team-workload bars (dashboard) */
.wl-track { height: 7px; border-radius: 4px; background: var(--accent-wash); overflow: hidden; margin-top: 4px; }
.wl-fill { height: 100%; border-radius: 4px; background: var(--accent); transition: width var(--dur-slow) var(--ease-standard); min-width: 3px; }
/* Busiest bar: a fixed non-accent hue so it reads even when the accent is amber. */
.wl-fill.hot { background: var(--chart-1); }

/* Read notifications: flatten the emoji icon to the text colour (transparent glyph +
   zero-blur shadow renders a solid ink silhouette) so only unread items carry colour. */
.notif-ico-read { color: transparent; text-shadow: 0 0 0 var(--ink); }
/* Unread: the same silhouette trick in the accent colour, so new items pop consistently
   regardless of how the platform renders each emoji. */
.notif-ico-unread { color: transparent; text-shadow: 0 0 0 var(--accent); }

/* Floating status toasts: top-center, above modals, never blocking the content. */
.toast-stack {
  position: fixed; top: 76px; left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 8px; z-index: 320; max-width: min(520px, 90vw);
}
.toast {
  display: flex; align-items: flex-start; gap: 12px; padding: 14px 18px;
  border-radius: 10px; border: 1px solid var(--line-strong); background: var(--panel-solid);
  box-shadow: var(--elev-2); font-size: 12.5px; color: var(--ink);
  animation: toast-in var(--dur-base) var(--ease-out);
}
/* Status-tinted fill: the kind's wash layered over the solid panel, so the tint reads
   in both themes without costing text contrast; the border picks up the same hue. */
.toast.ok { background: linear-gradient(var(--ok-wash), var(--ok-wash)), var(--panel-solid); border-color: var(--ok); }
.toast.warn { background: linear-gradient(var(--amber-wash), var(--amber-wash)), var(--panel-solid); border-color: var(--amber-dot); }
.toast.err { background: linear-gradient(var(--danger-wash), var(--danger-wash)), var(--panel-solid); border-color: var(--danger); }
.toast.info { background: linear-gradient(var(--accent-wash), var(--accent-wash)), var(--panel-solid); border-color: var(--accent); }
@keyframes toast-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
:root[data-reduce-motion="true"] .toast { animation: none; }
.toast-ico {
  flex: none; width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center;
  font-size: 11px; font-weight: 700;
}
/* Each variant carries both halves. A blanket `color: #fff` on the shared rule put white on
   --amber-dot at 3.19:1, and split across two rules like that neither half states a contrast
   claim on its own — so nothing could measure it. */
.toast.ok .toast-ico { background: var(--ok-fill); color: var(--on-ok-fill); }
.toast.warn .toast-ico { background: var(--warn-fill); color: var(--on-warn-fill); }
.toast.err .toast-ico { background: var(--danger-fill); color: var(--on-danger-fill); }
.toast.info .toast-ico { background: var(--accent-fill); color: var(--on-accent-fill); }
.toast-msg { flex: 1; padding-top: 1px; }
.toast-x { flex: none; color: var(--faint); font-size: 11px; padding: 2px 4px; border-radius: 6px;
  min-height: 24px; min-width: 24px; display: inline-flex; align-items: center; justify-content: center; }
.toast-x:hover { color: var(--ink); background: var(--panel2); }

/* Modal list pattern: rows scroll inside a capped container while the pager or add
   control stays put below it like a fixed footer — long lists can't grow the modal.
   The 40rem cap yields to short viewports so the modal itself never overflows. */
.modal-list { max-height: min(40rem, calc(55vh / var(--content-zoom, 1))); overflow-y: auto; }

/* The user editor's Roles tab stacks two of these, and the lower one — the effective-permission
   read-out, a row per action plus a line per granting role — grew tall enough to push the held
   roles and the add control out of sight. Capped separately rather than tightening the shared
   rule: the held roles keep the more generous share because they are what the tab is for. Both
   still yield to short viewports, as the base rule does. */
.modal-list.roles-held { max-height: min(30rem, calc(55vh / var(--content-zoom, 1))); }
.modal-list.roles-effective { max-height: min(20rem, calc(40vh / var(--content-zoom, 1))); }

/* Inline status notices (kept where a message belongs to a form): padded, with a kind icon. */
.notice { display: flex; align-items: flex-start; gap: 9px; padding: 9px 14px; border-radius: 8px; font-size: 12px; }
.notice.err { background: var(--danger-wash); }
.notice.ok { background: var(--ok-wash); }
.notice.warn { background: var(--amber-wash); }
.notice .n-ico { flex: none; font-weight: 700; }
.notice.err .n-ico { color: var(--danger); }
.notice.ok .n-ico { color: var(--ok); }
.notice.warn .n-ico { color: var(--amber); }
:root[data-reduce-motion="true"] .wl-fill { transition: none; }

/* Centered modal (About) */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 60; background: rgba(4, 10, 25, .5);
  backdrop-filter: blur(3px); display: flex; justify-content: center; padding: 20px;
  /* Safe centering: a modal taller than the viewport aligns to the top (header first)
     instead of spilling above it unreachably; the backdrop scrolls as a last resort. */
  align-items: center; align-items: safe center; overflow-y: auto;
}
/* No modal may outgrow 90% of the REAL viewport; taller content scrolls inside the
   panel. vh units inside a zoomed .content are inflated by the text-size zoom, so the
   cap divides it back out — without this, a large text size pushes the modal (and its
   header) past the screen edges. */
.modal-backdrop > .panel { max-height: calc(90vh / var(--content-zoom, 1)); overflow-y: auto; }
/* When the panel itself scrolls, the header stays pinned to its top, opaque over the
   passing content. (The wide shell scrolls its body instead, so this never engages there.) */
.modal-backdrop > .panel > .modal-head {
  position: sticky; top: calc(-1 * var(--pad)); z-index: 3;
  background: var(--panel-solid);
  margin: calc(-1 * var(--pad)) calc(-1 * var(--pad)) 8px;
  padding: var(--pad) var(--pad) 8px;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
/* A modal had no shadow at all before the scale existed — it read as flat against its own backdrop,
   which is the one place depth is doing real work. */
.modal { width: min(460px, 94vw); box-shadow: var(--elev-3); }
.modal-wide { width: min(640px, 96vw); min-height: 15rem; display: flex; flex-direction: column; box-shadow: var(--elev-3); }
.modal-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.modal-head h2 { margin: 0; }
.modal-head .x { margin-left: auto; }
.perm-row { display: flex; gap: 10px; align-items: flex-start; padding: 6px 4px; border-radius: 8px; cursor: pointer; }
.perm-row:hover { background: var(--accent-wash); }
.perm-row input { margin-top: 2px; }

/* Typeahead results: in flow, right under the input, bounded with their own scroll. */
/* Typeahead results float over the content below, like every other dropdown surface.
   position:absolute (not fixed) so backdrop-filter ancestors can't re-anchor it. */
.lookup { position: relative; }
.lookup-results {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 80;
  max-height: 240px; overflow-y: auto;
  background: var(--panel-solid); border-color: var(--line-strong); box-shadow: var(--elev-1);
}
/* Result rows are buttons: keep them transparent but light up on hover / keyboard focus like any
   active row. (The global .rowline:hover rule skips rows whose inline style carries a background,
   so the row must not set one inline — the base transparency lives here instead.) */
.lookup-results button { background: transparent; }
.lookup-results button:hover, .lookup-results button:focus-visible { background: var(--accent-wash); }

/* Login card */
.login-wrap { min-height: 70vh; display: grid; place-items: center; }
.login-card { width: 360px; padding: 30px 28px; border-radius: 18px; }
.login-brand { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }

/* Compact inline toggle switch (notes item 1) — pairs with a text label. */
.switch{position:relative;display:inline-block;width:28px;height:16px;flex:none;vertical-align:middle}
.switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer;z-index:1}
.switch i{position:absolute;inset:0;border-radius:999px;background:var(--panel2);border:1px solid var(--line);transition:background var(--dur-fast),border-color var(--dur-fast);font-style:normal}
.switch i::after{content:"";position:absolute;top:2px;left:2px;width:10px;height:10px;border-radius:50%;background:var(--faint);transition:transform var(--dur-fast),background var(--dur-fast)}
.switch input:checked + i{background:var(--accent-wash);border-color:var(--accent)}
.switch input:checked + i::after{transform:translateX(12px);background:var(--accent)}
.switch input:focus-visible + i{outline:2px solid var(--accent);outline-offset:1px}

/* Rearrangeable panels (PanelBoard): a Customize toggle + pointer-drag reordering. */
.pb-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-bottom:8px}
.pb-hint{flex:1;font-size:11px;color:var(--faint)}
.pb-customize{font-size:11px;display:inline-flex;align-items:center;gap:5px}
.pb-tray{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:8px}
.pb-chip{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:2px 8px;border-radius:999px;
  min-height:24px;min-width:24px;justify-content:center;
  background:var(--panel2);border:1px solid var(--line);color:var(--ink);cursor:pointer}
.pb-chip:hover{border-color:var(--accent);background:var(--accent-wash)}
.pb-wrap{position:relative}
/* A panel-board slot may hold SEVERAL stacked panels (the intake Conflicts tab's Decision /
   Reviewers / Conflicts report share one slot). The board's grid gap only separates slots, and
   .panel carries no margin of its own — so siblings inside a slot rubbed edge to edge. The
   general-sibling combinator keeps the gap even when a panel's overlay drawer renders between. */
.pb-wrap > .panel ~ .panel{margin-top:var(--gap)}
.panel-board.pb-editing .pb-wrap{outline:1px dashed var(--line);outline-offset:3px;border-radius:var(--r-lg);touch-action:none}
/* While rearranging, every column must be reachable by the pointer — including one holding
   nothing, and the space below the last panel of a short column. Boards set align-items:start,
   so a column is otherwise only as tall as its content and there is nothing under the cursor
   for elementFromPoint to land on: the drop reported column -1 and the panel sprang back.
   align-content keeps the panels packed to the top while the column itself stretches. */
.panel-board.pb-editing .pb-col{align-self:stretch;align-content:start;min-height:120px}
.pb-empty{display:flex;align-items:center;justify-content:center;min-height:110px;padding:10px;
  border:1px dashed var(--line-strong);border-radius:var(--r-lg);color:var(--muted);
  font-size:11px;text-align:center;background:var(--panel2)}
.pb-handle{display:flex;align-items:center;gap:6px;padding:3px 6px;margin-bottom:5px;
  background:var(--panel2);border:1px solid var(--line);border-radius:6px}
/* A drag handle is dragged with a pointer, so SC 2.5.8 applies to it as much as to a button — and
   at 13×19 this was the smallest target in the product. It is a span, so the floor has to be here
   rather than inherited from the button reset. */
.pb-grip{cursor:grab;font-size:14px;line-height:1;color:var(--faint);user-select:none;touch-action:none;padding:0 2px;
  min-height:24px;min-width:24px;display:inline-flex;align-items:center;justify-content:center}
.pb-grip:active{cursor:grabbing}
.pb-handle-title{flex:1;font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text-muted,#9aa0aa)}
.pb-eye{border:none;background:none;cursor:pointer;color:var(--faint);font-size:11px;line-height:1;padding:2px 4px;border-radius:4px;
  min-height:24px;min-width:24px;display:inline-flex;align-items:center;justify-content:center}
.pb-eye:hover{color:var(--accent-ink);background:var(--accent-wash)}
.pb-dragging{opacity:.45}
.pb-drop-before::after,.pb-drop-after::after{content:"";position:absolute;left:0;right:0;height:3px;
  background:var(--accent);border-radius:2px;z-index:2}
.pb-drop-before::after{top:-5px}
.pb-drop-after::after{bottom:-5px}

/* A shrunk panel (PanelBoard ⤡). Capped height with its own scrollbar — never clipped, because
   a panel that quietly ends early reads as a complete list. */
.pb-wrap.pb-compact{max-height:220px;overflow:auto}
.pb-wrap.pb-compact::-webkit-scrollbar{width:8px}

/* ---------------------------------------------------------------------------
   Help centre (D-236). Two columns: a sticky topic rail and the document. The
   prose styles are deliberately generous — this is the one surface in the
   product meant to be READ rather than scanned, so line length is capped and
   the vertical rhythm is looser than the working screens.
   --------------------------------------------------------------------------- */
/* The reading column is capped and centred as a UNIT. Uncapped, a 78ch article strands
   itself against the left edge of a very wide card with half the panel empty beside it. */
.help-layout{display:flex;gap:16px;align-items:flex-start;max-width:1180px;margin-inline:auto}
.help-nav{flex:0 0 258px;position:sticky;top:12px;overflow:auto;padding-right:4px;
  max-height:calc((100vh - 96px) / var(--content-zoom, 1))}
/* The CARD carries the reading measure, so the title, the breadcrumbs, the contents and the
   prose all share one left and right edge. Capping the prose instead left a full-width title
   sitting over a half-width column of text inside a much wider card. */
.help-body{flex:1;min-width:0;max-width:672px}
/* The topic index is a directory: it needs room for its tiles, not a reading measure. */
.help-layout.wide{max-width:1560px}
.help-body.wide{max-width:none}
/* Breadcrumbs are a header, not a list row: no rule under them, no row padding. */
.help-crumbs{display:flex;align-items:center;gap:8px;margin:0 0 10px}
.help-title{margin:0 0 12px}
.help-link{display:block;padding:6px 8px;border-radius:var(--r-md);border:1px solid transparent;
  text-decoration:none;color:var(--ink)}
.help-link:hover{background:var(--accent-wash);border-color:var(--line)}
.help-link.on{background:var(--accent-wash);border-color:var(--accent)}
.help-link-title{display:block;font-size:12px;font-weight:600}
.help-link-sub{display:block;font-size:10.5px;color:var(--faint);margin-top:2px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.help-tile{text-decoration:none;color:var(--ink)}
.help-tile-title{font-size:12.5px;font-weight:650;margin-bottom:2px}
.help-contents{margin:0 0 12px;font-size:11.5px}
.help-contents summary{cursor:pointer;color:var(--accent-ink);font-family:var(--mono);
  font-size:10.5px;letter-spacing:.12em;text-transform:uppercase}
.help-contents ul{margin:6px 0 0;padding-left:18px;line-height:1.9}

.help-prose{font-size:13px;line-height:1.72;color:var(--ink)}
.help-prose h1{font-size:21px;font-weight:650;margin:0 0 10px;letter-spacing:-.01em}
.help-prose h2{font-size:16px;font-weight:650;margin:26px 0 8px;padding-bottom:6px;
  border-bottom:1px solid var(--line)}
.help-prose h3{font-size:13.5px;font-weight:650;margin:18px 0 6px}
.help-prose h4{font-size:12.5px;font-weight:650;margin:14px 0 4px;color:var(--muted)}
.help-prose p{margin:0 0 11px}
.help-prose ul,.help-prose ol{margin:0 0 12px;padding-left:22px}
.help-prose li{margin:0 0 5px}
.help-prose a{color:var(--accent-ink)}
.help-prose code{font-family:var(--mono);font-size:11.5px;background:var(--accent-wash);
  border:1px solid var(--line);border-radius:4px;padding:1px 5px}
.help-prose pre.help-code{background:var(--panel2);border:1px solid var(--line);
  border-radius:var(--r-md);padding:10px 12px;overflow-x:auto;margin:0 0 12px}
.help-prose pre.help-code code{background:none;border:none;padding:0;font-size:11.5px;
  line-height:1.6;white-space:pre}
.help-prose blockquote{margin:0 0 12px;padding:8px 12px;border-left:3px solid var(--accent);
  background:var(--accent-wash);border-radius:0 var(--r-md) var(--r-md) 0;color:var(--muted)}
.help-prose hr{border:none;border-top:1px solid var(--line);margin:22px 0}
.help-tablewrap{overflow-x:auto;margin:0 0 14px}
.help-prose table{border-collapse:collapse;width:100%;font-size:11.5px}
.help-prose th,.help-prose td{border:1px solid var(--line);padding:6px 9px;text-align:left;
  vertical-align:top}
.help-prose th{background:var(--accent-wash);font-weight:650}

@media (max-width:900px){
  .help-layout{flex-direction:column}
  /* Stacked, the full topic list would push the document a screen and a half down, so it
     keeps its own scroll and the article starts where the reader is already looking. */
  .help-nav{position:static;flex:1 1 auto;width:100%;max-height:300px;overflow:auto}
  /* And the card takes the width it is given — a 672px cap would strand it. */
  .help-body{max-width:none;width:100%}
}

/* The note a reader gets when their language has no copy of this guide yet (D-239). Quiet:
   it explains the language change without competing with the title above it. */
.help-untranslated{font-size:11px;margin:0 0 12px;padding:6px 10px;border-radius:var(--r-md);
  border:1px solid var(--line);background:var(--panel2)}

/* ---- Support portal (D-290) ------------------------------------------------------------
   Four pages share one strip of section links and one reading column. Links rather than a
   tablist, because these ARE navigations — each section is its own address — so the selected
   one is aria-current, which is what a link states, and no role="tab" is claimed for something
   the keyboard does not drive like a tablist. */
.pnav{display:flex;gap:2px;margin:0 0 16px;border-bottom:1px solid var(--line);overflow-x:auto}
.pnav a{padding:8px 13px;font-size:var(--fs-sm);color:var(--muted);border-bottom:2px solid transparent;
  white-space:nowrap;text-decoration:none}
.pnav a:hover{color:var(--ink)}
.pnav a[aria-current="page"]{color:var(--accent-ink);border-bottom-color:var(--accent);font-weight:600}

/* The one-line reminder that this portal is not a case. Quiet by design — it is a standing
   fact about the room, not a warning about what the reader just did. */
.pnote{font-size:11px;color:var(--muted);margin:0 0 14px;padding:7px 11px;border-radius:var(--r-md);
  border:1px solid var(--line);background:var(--panel2)}

/* A message in a ticket or a topic. The author sits above their words rather than beside them:
   a support answer can be long, and a portrait column of avatars pushes the text into a gutter. */
.pmsg{padding:11px 0;border-top:1px solid var(--line)}
.pmsg:first-child{border-top:0}
.pmsg-head{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--muted);margin-bottom:5px;
  flex-wrap:wrap}
.pmsg-body{font-size:var(--fs-sm);line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}
/* An internal note is tinted and labelled, never merely indented: it is the one thing on the
   page whose audience differs from everything around it. */
.pmsg.note{background:var(--amber-wash);border-radius:var(--r-md);padding:11px;border-top:0;margin:8px 0}
.pmsg.gone .pmsg-body{color:var(--faint);font-style:italic}
/* The accepted answer earns a left edge rather than a wash, so a long thread still reads as
   one column of text with one of them marked. */
.pmsg.accepted{box-shadow:inset 3px 0 0 var(--ok);padding-left:11px}

/* A portal list row: title on top, the facts about it underneath. */
.prow{display:block;padding:11px 8px;border-bottom:1px solid var(--line);text-decoration:none;color:var(--ink)}
.prow:last-child{border-bottom:0}
.prow:hover{background:var(--accent-wash)}
.prow-title{font-weight:650;font-size:var(--fs-sm);line-height:1.35}
.prow-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--muted);
  margin-top:4px}
.prow-excerpt{font-size:11.5px;color:var(--muted);margin-top:4px;display:-webkit-box;
  -webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}

/* A pair of list panels that has to read as a pair. `.cols` aligns to start by default, which
   is right for panels of unrelated content and wrong for two side-by-side lists: the shorter one
   becomes a stub beside its neighbour and the row of two stops looking like a row. `.matched`
   stretches them to a common height; `.capped` bounds that height and hands the overflow to the
   list's own scroller, so a busy queue scrolls inside its panel instead of pushing the page down.
   The heading stays put — a scroll region whose title scrolls away is a scroll region nobody can
   label. */
.cols.matched { align-items: stretch; }
.panel.capped { display: flex; flex-direction: column; max-height: 40rem; }
.panel.capped > .xp-head { flex-shrink: 0; }
.pscroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

/* The filter bar every portal list carries: wraps rather than scrolls, so a narrow window
   never hides a filter behind an edge nobody thinks to drag. */
.pbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:12px}
.pbar input[type=search],.pbar input[type=text]{min-width:200px;flex:1 1 200px}

/* The reading column for a post or a topic. Same measure as a help document, and for the same
   reason: prose wider than this is prose nobody finishes. */
.pread{max-width:672px}

/* ===== Printing (PRD §8.17, D-293) =====
   Two jobs in one block: print whatever is on screen, and give a staged report its formal
   presentation instead.

   <b>Why this was rewritten.</b> It used to be `.app-shell { display: none }` plus
   `.report-print { display: block }` — on the theory that the report is the only thing worth
   printing. But the report renders inside @Body, which is inside .app-shell, and a descendant
   of a display:none ancestor cannot be brought back by any rule. So the shell was hidden, the
   report went with it, and EVERY page in the app — the evidence label that tells you to print
   it, a case, a list, a report — printed a blank sheet.

   Isolation is done with visibility rather than display, because visibility:hidden IS
   overridable by a descendant and display:none is not.

   <b>THIS BLOCK MUST STAY LAST IN THE FILE. Add nothing after it.</b> A media query adds no
   specificity of its own, so these rules beat the screen layout only by coming after it. Sitting
   where this block used to — above `.app-shell { height: 100vh }` and `.content { overflow:
   auto }` — every unwinding rule below lost the cascade to the screen rule of the same
   specificity further down, the content pane kept its viewport height, and printing produced one
   page with the rest truncated. The chrome still vanished, because those rules carry
   !important — which is what made the failure look like a layout problem rather than an ordering
   one. */
.report-print { display: none; }
@media print {
  /* Dark ink on white paper, whichever theme the person works in.
     A browser drops backgrounds when it prints — that is the default and the right one, since
     nobody wants a page of solid navy — so the dark theme's near-white text lands on white paper
     at 1.19:1 and is simply not there. Measured on the dashboard, not guessed at. Both selectors
     are listed because [data-theme="dark"] outranks a bare :root and would otherwise win.
     Accent and status hues drop to their light-theme values rather than to black: they carry
     meaning on a chip, and those values are already tuned to clear 4.5:1 on a near-white ground. */
  :root, :root[data-theme="dark"] {
    --bg0: #FFFFFF; --bg1: #FFFFFF;
    --panel: #FFFFFF; --panel-solid: #FFFFFF; --panel2: #FFFFFF;
    --ink: #000000; --muted: #333333; --faint: #555555;
    --line: rgba(0, 0, 0, .28); --line-strong: rgba(0, 0, 0, .5);
    --row-alt: transparent;
    --accent: #0284C7; --accent-ink: #025A85; --accent-wash: transparent;
    --amber: #7A5207; --amber-wash: transparent;
    --ok: #106029; --ok-wash: transparent;
    --danger: #A32828; --danger-wash: transparent;
    --accent-fill: #025A85; --ok-fill: #106029; --warn-fill: #7A5207; --danger-fill: #A32828;
    --chart-1: #4F46E5; --chart-2: #0D9488; --chart-3: #64748B;
    color-scheme: light;
  }

  /* Shadows are a screen affordance and a grey smudge on paper — the dark theme's are heavy
     enough to read as a border. Everything else that carries one (the palette, modals, toasts,
     tooltips, the print control itself) is already hidden below, so the maximize drawer is the
     only one that can reach a sheet. Done by selector rather than by blanking --elev-*: those
     tokens belong to the palettes, and DesignTokenScaleTests counts their declarations to catch
     a theme that forgets one. */
  .xp-drawer { box-shadow: none; }

  /* Margins belong to the paper, not to the app's screen padding. Size stays auto, so whatever
     is in the tray is what it fits. */
  @page { margin: 12mm; }

  /* Some elements ARE their background. The bar charts here are painted divs (.wl-track and
     .wl-fill), not SVG shapes — so a printer's default "drop the backgrounds" erased the bars and
     printed their labels beside nothing, while the SVG charts in the next panel came out fine.
     Forcing exact colour is safe precisely BECAUSE the palette above is already white surfaces
     and black ink: what survives is the fills that carry meaning, not a page of navy. */
  :root { print-color-adjust: exact; -webkit-print-color-adjust: exact; }

  /* And the groove needs a colour of its own, having just lost its wash to that palette. */
  .wl-track { background: rgba(0, 0, 0, .10); }

  /* The app's own breakpoints DO fire on paper — they are written bare rather than as
     `screen and (max-width: …)`, and a page box is narrower than 1100px — so the multi-column
     bands collapse to a single column by themselves. The stat strip is the exception:
     `.stats.oneline` pins every card into one row with grid-auto-flow, which is the point on a
     screen and squeezes eight cards into 88px each on a sheet. On paper it wraps like an
     ordinary stat grid, and the two-line clamp on its labels comes off with it. */
  .stats.oneline { grid-auto-flow: row; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
  .stats.oneline .stat .lbl { -webkit-line-clamp: none; overflow: visible; }

  /* Frosted glass costs a printer nothing and gains it nothing. */
  .panel, .titlebar, .case-head { backdrop-filter: none; }

  /* The two page textures, both off. The chosen backdrop pattern (data-bg: grid, circuit, hex,
     radar) is a fixed full-viewport pseudo-element on body, and the content pane carries a dot
     grid of its own. Both sit at around 5% ink on a screen, which is the point of them and
     unreadable noise on paper — a grid ruled across every sheet behind the actual content.
     They only began to show once print-color-adjust: exact told the printer to honour
     backgrounds; that switch is what the bar charts need, so these are turned off by name. */
  body::before, body::after { content: none !important; display: none !important; }

  /* The shell is laid out for a screen: a fixed-height flex column whose content pane does its
     own scrolling. A printer has neither a viewport nor a scrollbar, so each of those has to be
     unwound or the output is clipped at one screenful — or empty. */
  html, body { height: auto; overflow: visible; background: #fff; }
  .app-shell { display: block; height: auto; }
  .app-body { display: block; height: auto; overflow: visible; }
  .content { flex: none; height: auto; overflow: visible; padding: 0; background-image: none; }
  .content-inner { max-width: none; margin: 0; }

  /* Chrome nobody wants on paper. The rail and titlebar are navigation, and navigation does not
     survive the journey to a sheet of paper. The assistant's doorway is the same kind of thing:
     a button fixed to the bottom-right corner of the VIEWPORT, so on paper it neither scrolls
     away nor belongs anywhere — it lands as a stray icon stamped over the first sheet, and over
     whichever panel happens to be printing. Its companion is deliberately NOT here: that one is
     an in-flow .panel holding a conversation somebody may well have printed on purpose. */
  .titlebar, .rail, .skip-link, .reconnect-overlay, .assistant-fab,
  .toast-stack, .palette-backdrop, .modal-backdrop { display: none !important; }

  /* For chrome the stylesheet cannot recognise by class, because it is a control that happens to
     sit inside content being printed: the viewer overlays' own Print and Close buttons. Printing
     an image from the lightbox should yield the picture and its file name, not the picture with
     two buttons stamped underneath the caption. Opt-in by name, so nothing is hidden by accident. */
  .no-print { display: none !important; }

  /* Let long content flow onto as many sheets as it needs, and keep a row whole. */
  tr, .rowline, .panel-item { break-inside: avoid; }

  /* One panel on its own sheet (D-294).
     Everything off the panel's ancestor line is marked by print.js and taken out of the layout
     here. DISPLAY, not visibility: an invisible box is still a box, so hiding the shell that way
     left the document as tall as the whole page — the panel printed on sheet one and the rest of
     the application followed it as blank sheets. Collapsing the siblings instead ends the
     document with the panel. The panel itself is neither moved nor repositioned, so a Blazor
     re-render arriving mid-print cannot lose the thing being printed. */
  body[data-printing="panel"] [data-print-hidden] { display: none !important; }

  /* Every ancestor becomes a plain block for the duration, which is what makes the panel as wide
     as the sheet. Hiding the neighbours is not enough on a grid: an empty column track still
     holds its width, so a dashboard panel printed at a third of the page while the full-width
     panels on intake and case detail already filled it. */
  body[data-printing="panel"] [data-print-ancestor] {
    display: block !important;
    grid-template-columns: none !important;
    width: auto !important; max-width: none !important;
    margin: 0 !important; padding: 0 !important;
  }

  body[data-printing="panel"] [data-print-me] {
    width: auto; max-width: none;
    border: none; box-shadow: none; max-height: none; overflow: visible;
  }

  /* Printing a MAXIMIZED panel. The drawer's backdrop is an ancestor of the panel, so the sibling
     walk rightly leaves it alone — but it is `position: fixed; inset: 0`, a box the size of the
     viewport, and it went on claiming that height below the drawer and cost a second sheet. On
     paper it stops being an overlay and simply wraps its drawer. */
  body[data-printing="panel"] .xp-backdrop {
    position: static; display: block; background: none; z-index: auto; padding: 0;
  }
  body[data-printing="panel"] .xp-drawer { width: auto; max-width: none; max-height: none; }

  /* The control itself is never part of what it prints. */
  .panel-print { display: none !important; }

  /* A staged report replaces the page rather than joining it. */
  body:has(.report-print) .app-shell { visibility: hidden; }
  body:has(.report-print) .report-print,
  body:has(.report-print) .report-print * { visibility: visible; }
  body:has(.report-print) .report-print { position: absolute; left: 0; top: 0; width: 100%; }

  .report-print { display: block; color: #000; font-family: Georgia, "Times New Roman", serif; padding: 24px; }
  .report-print .classification-banner {
    text-align: center; font-family: var(--mono); font-weight: 700; letter-spacing: .2em;
    border: 2px solid #000; padding: 6px; margin: 0 0 18px; text-transform: uppercase;
  }
  .report-print .classification-banner.bottom { margin: 24px 0 0; }
  .report-print h1 { font-size: 20px; margin: 0 0 4px; }
  .report-print .report-meta { font-family: var(--mono); font-size: 11px; margin-bottom: 18px; }
  .report-print .report-body { font-size: 12.5px; line-height: 1.55; white-space: pre-wrap; }
  .report-print .watermark {
    position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
    font-size: 110px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
    color: rgba(0, 0, 0, .08); transform: rotate(-30deg); pointer-events: none; z-index: 10;
  }
}
