/* ===========================================================================
   GRI Voice QC Data Capture — light enterprise theme, adopted from the
   team's UI mockup (GRI_Voice_QC_Capture_Team_Mockup_1.html) as the visual
   and navigation reference. Same mobile-first discipline as before: base
   rules target 320-430px phones, a 431px breakpoint adds tablet
   refinements, a 1025px breakpoint builds the full desktop layout
   (including pinning the sidebar). No horizontal scrolling at any
   supported width.

   Class/id names already used by gri-app.js and qc-grid.js are preserved
   exactly (state-pill, notice*, stage-*, context-*, orb-*, icon-btn*,
   error-banner, caption-*, qc-*) — only their colors and, where noted,
   their container changed in this pass. Everything under "shell & nav"
   and "cards / empty states" below is new, for the sidebar/topbar layout
   and the five screens still pending real-data wiring.
=========================================================================== */

:root {
  /* Styling and Color Theme Replacement pass: ink/surface/border tokens
     retoned to the uploaded reference's neutral palette. --ink-900/--ink-500
     use the reference's exact "Primary text #343A40" / "Secondary text
     #777777" swatches, with --ink-500 nudged two steps darker (#6B6B6B) so
     it still clears 4.5:1 against both --panel (white) and the new --surface
     (#F2F3F5) — the literal #777777 lands at ~4.0:1 against #F2F3F5, just
     under AA for normal text; the brief explicitly allows "adjusting shades
     only where necessary for readability." --ink-700/--ink-300/--ink-200/
     --ink-100 are derived mid-tones (not given swatches) kept in the same
     neutral-gray family instead of the old blue-gray (slate) family. */
  --ink-900: #343A40;
  --ink-700: #4A4F54;
  --ink-500: #6B6B6B;
  --ink-300: #AEB1B5;
  --ink-200: #D6D8DB;
  --ink-100: #EEEFF1;
  --surface: #F2F3F5;   /* reference: "Main application background" */
  --panel: #ffffff;     /* reference: "Card background" */
  /* Industrial Premium (Task D): a genuinely distinct elevated-surface tier
     (modals, hovered/focused tiles) — identical to --panel in light mode
     (elevation here reads through --shadow-lg + border, not a color shift,
     to keep the light theme coherent/unchanged per the brief) but a real
     step lighter than --panel in dark mode, see below. */
  --panel-elevated: #ffffff;
  --panel-inverse: #0B1220; /* Industrial Premium: richer, deeper sidebar/toast rail (was #0f172a) — stays a fixed dark rail in both color schemes, intentionally NOT touched by the dark-mode override block below. Left unchanged by the Styling/Color Theme Replacement pass: this is a floating toast/tooltip surface, not the sidebar itself (the sidebar's own background is now the gradient defined on .sidebar below), and the brief does not ask for toasts/tooltips to change. */
  --border: #E5E7EB;    /* reference: "Border" */
  --border-strong: #C7CACF;  /* derived: a stronger step of the new neutral border for floor-lighting cell separators, same role as before */

  /* Styling and Color Theme Replacement pass: --accent adopts the
     reference's "Primary teal #08A99C" hue, darkened slightly to #087F74
     (same hue family — only R stays 08, G/B pulled down) because the raw
     #08A99C tests at just 2.93:1 against white, well under the 4.5:1 AA
     minimum for the white button/nav text it sits under everywhere
     (.qc-btn-primary, the sidebar's active nav state, focus rings) — again,
     the brief's own "adjusting shades only where necessary for readability"
     allowance. #087F74 tests at 4.89:1 against white. */
  --accent: #087F74;
  --accent-dark: #076F65;       /* hover: 6.05:1 on white */
  --accent-soft: #E6F7F5;
  --accent-border: #9FDDD4;
  /* Industrial Premium: the text/icon color to use ON TOP of a solid
     --accent-filled surface (buttons, active tabs, the sidebar brand mark).
     White in light mode (unchanged visual behavior); dark mode needs a
     different value here (see override block) because its brighter accent
     fails contrast with white text — see that block's comment. */
  --accent-contrast: #FFFFFF;

  /* Styling and Color Theme Replacement pass: the sidebar is no longer a
     fixed dark rail (see .sidebar below, now the reference's teal-to-blue
     gradient) — --sidebar-active-bg (previously defined but unused) is
     repurposed as the translucent-white highlight for .nav-item.active
     sitting on that gradient; --sidebar-accent is untouched, since its only
     consumer (.notice::before, the toast dot) still sits on the unchanged
     dark --panel-inverse surface and is out of scope for this pass. */
  --sidebar-active-bg: rgba(255, 255, 255, 0.18);
  --sidebar-accent: #2DD4BF; /* 10.06:1 against the #0B1220 toast/tooltip surface — unrelated to the sidebar's own new gradient */

  /* Styling and Color Theme Replacement pass: admin-app.js's CHART_COLORS/
     STAGE_COLOR_CYCLE only ever reference these as var()s (never a literal
     hex), so the reference's four chart hues ("teal, blue, orange, and
     green") are applied purely by retoning the existing tokens below —
     --chart-purple is repurposed to the reference's ORANGE (#F6A936,
     darkened to #A77224 for the same contrast reason as --accent above;
     raw #F6A936 is only 1.97:1 on white) rather than renamed, to keep this
     a variable-value change, not a variable-rename, per the brief's
     "prefer updating existing CSS variables" instruction. --chart-blue
     becomes the reference's "Secondary blue" exactly as given (#2176BD
     already clears 4.78:1 on white, no adjustment needed). */
  --chart-purple: #A77224; /* was violet #7c3aed; now the reference's orange, used for the 4th multi-stage trend line and reused below for KPI tile 7 */
  --chart-blue: #2176BD;   /* reference: "Secondary blue" */

  /* Styling and Color Theme Replacement pass: the three KPI-category-only
     hues are retoned to the reference's orange/indigo/blue treatments per
     the brief ("use the reference design's orange, indigo, and blue color
     treatments where appropriate"), while leaving the KPI grid's content,
     arrangement, and the semantic --warn/--danger/--accent/--ok categories
     (pending/anomalies/total/completed) exactly as they were — only the two
     purely decorative categories ("Active sessions", "Confirmed readings")
     and the cyan one ("Average temperature") change hue. Values are the
     same contrast-adjusted hexes used elsewhere in this pass (blue #2176BD,
     indigo #303D91 as given — 9.54:1 on white, no adjustment needed —
     orange #A77224), so "blue"/"indigo"/"orange" never drift into three
     more one-off hexes. */
  --chart-bright-teal: #2176BD;  /* was teal-600; now "blue" — distinct from --accent (teal) for "Active sessions" */
  --chart-emerald: #303D91;      /* was emerald; now "indigo" — distinct from --ok's green for "Confirmed readings" */
  --chart-cyan: #A77224;         /* was cyan; now "orange" — distinct from --info's blue for "Average temperature" */
  /* StageShow visual-polish pass (soft/border companions), retoned to match
     the new blue/indigo/orange hues above, same Tailwind-100/300 lightness
     step convention as every other soft/border pair in this file. */
  --chart-bright-teal-soft: #E7F1FA;
  --chart-bright-teal-border: #B9D7EC;
  --chart-emerald-soft: #E7E8F6;
  --chart-emerald-border: #BCC2E8;
  --chart-cyan-soft: #F7EEE0;
  --chart-cyan-border: #E3C9A0;

  /* Styling and Color Theme Replacement pass: --ok adopts the reference's
     "Green #36B77D" hue, darkened to #157A4C for the same reason as
     --accent/--chart-purple above (raw #36B77D is 2.55:1 on white — used
     here as real status text/icon color on .state-pill, .tag-implemented,
     etc., not just a decorative dot, so it needs the full 4.5:1). */
  --ok: #157A4C;
  --ok-soft: #E1F5EA;
  --ok-border: #AEE0C5;

  /* --warn/--danger are semantic "pending"/"anomaly" status colors, not
     part of the reference's decorative palette — left exactly as they were
     so a real caution/error state stays visually distinct from the new
     decorative orange (--chart-cyan above) and from each other, per the
     brief's "preserve dashboard sections or KPI definitions". */
  --warn: #b45309;
  --warn-soft: #fef9c3;        /* Phase 1: more distinct amber wash */
  --warn-border: #fde68a;

  --danger: #b91c1c;
  --danger-soft: #fee2e2;       /* Phase 1: slightly more saturated */
  --danger-border: #fecaca;

  /* --info realigned to the reference's secondary blue (was an older,
     slightly different blue) so informational banners/pills match the new
     --chart-blue instead of carrying a third, near-identical blue hue. */
  --info: #2176BD;
  --info-soft: #E7F1FA;
  --info-border: #B9D7EC;

  --neutral-soft: #f1f5f9;
  --neutral-border: #e2e8f0;

  --corrected: #6d28d9;
  --corrected-soft: #f5f3ff;
  --corrected-border: #ddd6fe;

  /* Voice-session states — each gets its own hue so Ready / Connecting /
     Listening / Processing / Speaking are never confusable at a glance. */
  --state-ready: #64748b;
  --state-connecting: #b45309;
  --state-listening: #087F74; /* Styling/Color Theme Replacement pass: matches the retoned --accent (was #0F766E) */
  --state-thinking: #6d28d9;
  --state-answering: #0e7490;

  /* Feeds web/voice-orb.js's canvas animation (adapted from Marea's own
     --voice-teal*/*-strong tokens) — the orb's drawing code never
     hardcodes a color, it always reads these two. Mapped to GRI's own
     accent so the animation stays in GRI's palette automatically if the
     accent ever changes, the same way Marea's engine follows its own
     --voice-teal* tokens. */
  --voice-teal: var(--accent);
  --voice-teal-strong: var(--accent-dark);

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-pill: 999px;

  --space-3xs: 4px;
  --space-2xs: 6px;
  --space-xs: 10px;
  --space-sm: 14px;
  --space-md: 18px;
  --space-lg: 24px;
  --space-xl: 32px;

  /* Styling and Color Theme Replacement pass: shadow colors switch from a
     tinted navy (rgba(15,23,42,...)) to the reference's neutral black
     ("Subtle shadow: rgba(0, 0, 0, 0.12)") — only the color channel changes,
     the same blur/spread/opacity-step relationship between sm/md/lg is
     kept. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.10);
  /* Industrial Clarity (Phase 2): a third, more pronounced, OMNIDIRECTIONAL
     elevation step for surfaces that sit above the normal card layer —
     modals, a hovered/focused clickable stat tile. (.recording-review-panel
     keeps its own explicit, directional upward shadow below — a bottom
     sheet needs the shadow cast upward from its top edge, not a generic
     all-around one, so it isn't switched to this token.) This was
     previously referenced nowhere except as a `var(--shadow-lg, <fallback>)`
     with no real token ever declared behind it; now it's a real, reusable
     token. */
  --shadow-lg: 0 10px 32px rgba(0, 0, 0, 0.12); /* reference: "Subtle shadow" rgba(0,0,0,0.12) used as the headline/most-visible shadow step */

  /* Correction (Industrial Clarity pass): Phase 1's comment here claimed
     Inter was "loaded via <link> in each HTML head" — checked all three
     HTML heads directly and no such <link> exists; only the local
     /style.css stylesheet is ever linked, same as before Phase 1. That
     comment described work that was never actually done. Inter is, and
     has only ever been, a PREFERRED entry in this font stack: on any
     machine that happens to already have it installed it's used, and on
     the fish-processing-floor devices this app actually targets — which
     won't have it — the browser silently falls through to the next
     available name (-apple-system/Segoe UI/Roboto/system sans), so the
     offline/air-gapped contract (tests/test_frontend_contract.py, no
     external CDN/font requests anywhere) is intact and was never at risk.
     Nothing here loads anything over a network. */
  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  --focus-ring: 0 0 0 3px rgba(8, 127, 116, 0.35); /* Styling/Color Theme Replacement pass: matches the retoned --accent */

  /* 44x44 is the minimum recommended touch target (WCAG 2.5.5 / mobile HIG). */
  --touch-min: 44px;
}

/* Light-Colored, Glove-Friendly Interface pass: this dark palette used to
   apply unconditionally to every page (index.html, admin.html, worker.html
   alike) whenever the DEVICE/BROWSER had OS-level dark mode on — there was
   no way to opt out. For Worker Capture specifically that produced exactly
   the reported "dark dashboard" on any phone/tablet with system dark mode
   enabled, which is unsuitable for a bright fish-processing floor where
   high contrast against a light background matters more than matching the
   device's OS theme. `:root:not([data-theme="light"])` below means this
   block is now skipped entirely wherever `data-theme="light"` is set on the
   document root — see web/worker.html's `<html>` tag, which sets exactly
   that (plus `<meta name="color-scheme" content="light">`, so native form
   controls/scrollbars don't dark-style themselves independently of these
   custom properties either). admin.html and index.html set neither, so
   their dark-mode behavior is completely unchanged by this. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Industrial Premium (Task D): adopts the brief's suggested dark
       palette for surfaces — main bg #101827, card #1E293B, a genuinely
       distinct elevated tier #263449 (new — previously there was no
       elevated tier at all, which was part of why dark mode read as flat:
       --surface/--panel were two near-identical navy tones). ink-900
       tightened from #f1f5f9 to the brief's exact #F8FAFC (negligible
       visual difference, matches literally). --border/--border-strong
       nudged to stay proportionally visible against the new, slightly
       deeper backgrounds. */
    --ink-900: #F8FAFC;
    --ink-700: #cbd5e1;
    --ink-500: #94a3b8;
    --ink-300: #475569;
    --ink-200: #334155;
    --ink-100: #1e293b;
    --surface: #101827;
    --panel: #1E293B;
    --panel-elevated: #263449;
    --border: #2A3B54;
    --border-strong: #3A4D6B;

    /* Industrial Premium: a refined, slightly deeper teal than the previous
       dark-mode accent (#5eead4 minty teal-300 -> #2DD4BF teal-400) — still
       tests at 9.54:1 against the new #101827 bg, comfortably AA for text/
       icons/borders, but reads less neon/pastel and more "industrial."
       --accent-dark becomes the brief's own suggested accent (#0F766E is
       actually a near-perfect *hover/darker* tone here: 4.75:1, still
       legible as text, and clearly darker than the base accent). */
    --accent: #2DD4BF;
    --accent-dark: #0D9488;
    --accent-soft: #0d3b36;
    --accent-border: #155e56;
    /* A bright accent like #2DD4BF fails contrast with WHITE text used as a
       solid button/tab fill (checked: ~1.5:1) — pre-existing issue in this
       app's dark theme (.qc-btn-primary always hardcoded color:#fff). Using
       a near-black teal here instead for anything painted ON TOP of a
       solid --accent fill fixes that: 7.88:1 against #2DD4BF. */
    --accent-contrast: #052E2B;

    --chart-purple: #a78bfa; /* 5.38:1 on the new #1E293B card (the light-mode #7c3aed value drops to 2.57:1 on dark cards — needed its own dark-tuned value) */
    --chart-blue: #60a5fa;   /* 5.75:1 on #1E293B (the light-mode #2563eb value drops to 2.83:1 on dark cards) */

    /* Admin Overview Premium pass: dark-tuned counterparts of the three new
       KPI category tokens above (the light-mode hexes drop below AA on the
       #263449 elevated card) — same role, own per-theme value, same
       convention as --chart-purple/--chart-blue just above. */
    --chart-bright-teal: #2DD4BF; /* 8.4:1 on #263449 — "Active sessions" */
    --chart-emerald: #34D399;     /* 8.9:1 on #263449 — "Confirmed readings" */
    --chart-cyan: #22D3EE;        /* 9.6:1 on #263449 — "Average temperature" */

    --ok: #4ade80;
    --ok-soft: #0f2e1d;
    --ok-border: #1f5136;

    --warn: #fbbf24;
    --warn-soft: #2e2306;
    --warn-border: #4d3b0d;

    --danger: #f87171;
    --danger-soft: #2e1313;
    --danger-border: #5a1f1f;

    --info: #38bdf8;
    --info-soft: #0b2436;
    --info-border: #155e75;

    --neutral-soft: #1a2436;
    --neutral-border: #2a3b54;

    --corrected: #c4b5fd;
    --corrected-soft: #241c42;
    --corrected-border: #3c2f6b;

    --state-ready: #94a3b8;
    --state-connecting: #fbbf24;
    --state-listening: #2DD4BF; /* Industrial Premium: matches new dark-mode accent (was the old accent's hex) */
    --state-thinking: #c4b5fd;
    --state-answering: #38bdf8;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
    --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.45);
    --shadow-lg: 0 10px 36px rgba(0, 0, 0, 0.55);

    --focus-ring: 0 0 0 3px rgba(45, 212, 191, 0.4);
  }
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--surface);
  color: var(--ink-900);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden; /* belt-and-braces: layout itself must never need this */
}

/* Admin Dashboard layout-width correction pass: "increase the text size",
   scoped to admin.html ONLY via the html[data-app="admin"] attribute added
   to that file's own <html> tag (worker.html and index.html don't carry
   it, so neither is affected — honors "DO NOT MODIFY WORKER CAPTURE IN ANY
   WAY"). Almost every size in this stylesheet is set in `rem`, which
   resolves against the ROOT (<html>) font-size, not the nearest ancestor —
   so, unlike .worker-shell's own earlier same-pattern comment further down
   this file (which only bumps direct/inherited text, not rem-sized
   descendants), the fix here has to raise the root's own font-size for
   every rem value in admin.html to scale up together. 16px (the default
   root size every rem in this file is already written against) -> 17px is
   a modest, clearly-noticeable step without re-tuning every component's
   individual rem value. */
html[data-app="admin"] { font-size: 17px; }
html[data-app="admin"] body { font-size: 1rem; }
h1, h2, h3 { margin: 0; font-weight: 600; color: var(--ink-900); }
p { margin: 0; }
button, input, select { font-family: inherit; }
code { font-size: 0.92em; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

/* ======================================================= shell &amp; nav === */
.app-shell {
  display: grid;
  grid-template-columns: 1fr;
  min-height: 100vh;
}

.nav-backdrop { display: none; }

/* shadcn/ui redesign pass (Admin Overview, stage 2): the rail used to lean on
   a heavy drop shadow, a gradient/glow brand mark, and a glowing inset ring
   behind the active item to read as "premium." shadcn's own sidebar pattern
   gets its structure from flat surfaces and a single quiet fill for the
   active row instead — a thin border defines the rail, a solid (not
   gradient) accent square is the mark, and the active nav item is a plain
   filled rounded rectangle with no left notch or glow. Same classes/ids,
   same JS wiring (admin-app.js's router toggles .active by class name only),
   purely a flatter, more restrained paint pass. */
/* Enterprise Admin Dashboard implementation (Artifact 1 selected design):
   the sidebar moves from a fixed dark-navy rail to shadcn/ui dashboard-01's
   inset light rounded card — background/color flip only; every id, the
   .open class, and the collapse mechanics below are unchanged so
   admin-app.js's router/toggle code needs no edits.
   Styling and Color Theme Replacement pass: per the uploaded reference,
   the rail becomes a teal-to-blue gradient again — but unlike the OLD fixed
   dark rail (--panel-inverse, #0B1220), every color here is a forced-white/
   translucent-white literal scoped under .sidebar, never a swap of the
   shared --ink-900 / --ink-500 / --panel tokens, so this cannot affect any
   other surface that happens to reuse those tokens (same sidebar-gets-its-
   own-fixed-palette approach --panel-inverse and --sidebar-accent already
   use above in :root). Structure, ids, the .open class and the collapse
   mechanics below are unchanged. */
.sidebar {
  background: linear-gradient(180deg, #087F74 0%, #2176BD 100%);
  color: rgba(255, 255, 255, 0.86);
  display: flex;
  flex-direction: column;
  padding: 0;
  position: fixed;
  left: 0; top: 0; bottom: 0;
  width: 272px;
  transform: translateX(-100%);
  transition: transform 0.18s ease;
  z-index: 50;
  box-shadow: var(--shadow-lg);
  border-right: none;
}
.sidebar.open { transform: translateX(0); }

.sidebar-brand { display: flex; align-items: center; gap: 10px; padding: 16px 16px 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.18); margin-bottom: 0; }
.brand-mark {
  width: 32px; height: 32px; border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.92);
  color: #087F74;
  font-weight: 700; font-size: 0.76rem; letter-spacing: 0.02em;
  display: flex; align-items: center; justify-content: center; flex: none;
  box-shadow: none;
}
.brand-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.brand-title { font-weight: 600; font-size: 0.86rem; color: #ffffff; letter-spacing: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brand-tag { font-size: 0.66rem; color: rgba(255, 255, 255, 0.72); text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Sidebar search (Artifact 1: sidebar-search + ⌘K badge). A real quick-jump
   control, not decorative: typing filters the visible nav items by label and
   Enter navigates to the first visible match (wired in admin-app.js), so it
   does something rather than sitting there as an unused input. */
.sidebar-search { margin: 12px 14px; position: relative; flex: none; }
.sidebar-search input {
  width: 100%; padding: 8px 44px 8px 32px; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.14); font-size: 0.8rem; color: #ffffff; font-family: inherit;
}
.sidebar-search input::placeholder { color: rgba(255, 255, 255, 0.65); }
.sidebar-search input:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.55); border-color: rgba(255, 255, 255, 0.6); }
.sidebar-search svg.icon-search { position: absolute; left: 9px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: rgba(255, 255, 255, 0.72); pointer-events: none; }
.sidebar-search .kbd {
  position: absolute; right: 7px; top: 50%; transform: translateY(-50%);
  font-size: 0.64rem; color: rgba(255, 255, 255, 0.75); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; padding: 1px 5px; background: rgba(255, 255, 255, 0.14);
  pointer-events: none;
}
.nav-item.is-search-hidden { display: none; }
.app-shell.sidebar-collapsed .sidebar-search { display: none; }

/* Collapsible icon-rail (shadcn Sidebar's hallmark interaction): a small
   ghost icon button next to the brand that toggles .app-shell.sidebar-collapsed.
   Desktop-only — hidden below 1025px, where the sidebar is already an
   off-canvas drawer the mobile toggle opens/closes instead. */
.sidebar-collapse-toggle {
  display: none; flex: none; width: 22px; height: 22px; align-items: center; justify-content: center;
  background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: rgba(255, 255, 255, 0.72); cursor: pointer; padding: 0; margin-left: 4px;
}
.sidebar-collapse-toggle svg { width: 15px; height: 15px; }
.sidebar-collapse-toggle:hover { background: rgba(255, 255, 255, 0.16); color: #ffffff; }
@media (min-width: 1025px) { .sidebar-collapse-toggle { display: inline-flex; } }

.sidebar-nav { display: flex; flex-direction: column; gap: 14px; flex: 1; overflow-y: auto; padding: 4px 10px 10px; }
.nav-group { display: flex; flex-direction: column; gap: 1px; }
.nav-group-label {
  font-size: 0.64rem; font-weight: 700; color: rgba(255, 255, 255, 0.6); text-transform: uppercase;
  letter-spacing: 0.08em; padding: 0 10px; margin-bottom: 5px; white-space: nowrap; overflow: hidden;
}
.nav-item {
  display: flex; align-items: center; gap: 10px;
  background: none; border: none; color: rgba(255, 255, 255, 0.86); text-align: left;
  padding: 9px 10px; border-radius: var(--radius-sm); cursor: pointer;
  font-size: 0.85rem; font-weight: 500; width: 100%; min-height: var(--touch-min); position: relative;
  transition: background 0.15s ease, color 0.15s ease;
}
.nav-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nav-item:hover { background: rgba(255, 255, 255, 0.12); color: #ffffff; }
.nav-item.active {
  /* Styling and Color Theme Replacement pass: "Rounded active navigation
     item" per the brief — a translucent-white fill (--sidebar-active-bg,
     repurposed above in :root) that reads clearly against the gradient at
     any point along it, plus the same thin left accent bar this nav has
     always used for wayfinding (kept, just turned white to match). */
  background: var(--sidebar-active-bg); color: #ffffff; font-weight: 600;
  box-shadow: none;
}
.nav-item.active::before {
  content: '';
  position: absolute; left: 0; top: 5px; bottom: 5px; width: 3px;
  border-radius: 0 3px 3px 0;
  background: #ffffff;
}
.nav-item.active .nav-icon { color: #ffffff; }
.nav-icon { width: 18px; height: 18px; flex: none; color: rgba(255, 255, 255, 0.72); transition: color 0.12s ease; }
.nav-item:hover .nav-icon { color: #ffffff; }
.nav-badge {
  margin-left: auto; background: var(--warn); color: #fff; font-size: 0.68rem;
  font-weight: 700; border-radius: 999px; padding: 1px 7px; line-height: 1.4;
}

/* Admin Overview Premium: the sidebar footer was a bare two-line text stack
   ("Not signed in" / a plain blue "Sign out" link with a stray hardcoded
   inline color). Adds a generic account glyph (a CSS mask icon, so no new
   DOM/JS is needed — #sidebarWhoami's text content, set entirely by
   admin-app.js, is untouched) and turns Sign out into a proper small pill
   button. The one HTML change this pass makes (web/admin.html's #logoutBtn)
   is removing that leftover inline `style="color:#93c5fd"` — a pre-teal-
   palette blue that an inline style can't be overridden from CSS without
   !important — in favor of the .sidebar-signout class defined below; the
   element's id, event wiring, and hidden/shown logic are untouched. */
.sidebar-footer {
  border-top: 1px solid rgba(255, 255, 255, 0.18); padding-top: 10px; margin-top: 4px; padding-left: 10px; padding-right: 10px; padding-bottom: 10px;
}

/* ---- Sidebar user menu: real dropdown-menu pattern -------------------------
   A trigger button (avatar + name + chevron) that opens a popover panel
   above it (menu items stacked, same shape as shadcn's DropdownMenuContent),
   closed on outside click/Escape/selecting an item (wired in admin-app.js).
   #logoutBtn keeps its id and hidden/shown logic exactly as before — only
   its container and visual treatment changed. */
.sidebar-user-menu { position: relative; }
.sidebar-user-trigger {
  width: 100%; display: flex; align-items: center; gap: 9px;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  padding: 7px 8px; cursor: pointer; min-height: var(--touch-min);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.sidebar-user-trigger:hover, .sidebar-user-trigger[aria-expanded="true"] {
  background: rgba(255, 255, 255, 0.14); border-color: rgba(255, 255, 255, 0.28);
}
.sidebar-user-avatar {
  /* Styling and Color Theme Replacement pass: was a teal-on-teal mask
     (readable against the old dark rail, low-contrast against the new
     teal/blue gradient) — swapped to a solid white disc with the same
     person-glyph knocked out in the gradient's teal, so it reads clearly
     at any point along the rail. */
  width: 28px; height: 28px; border-radius: 50%; flex: none; border: none;
  background: #ffffff;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='white' stroke-width='1.5'%3E%3Ccircle cx='10' cy='7' r='3'/%3E%3Cpath d='M3.5 16.5c1-3.3 3.8-5 6.5-5s5.5 1.7 6.5 5' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center / 14px, linear-gradient(135deg, var(--accent), var(--accent-dark));
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='white' stroke-width='1.5'%3E%3Ccircle cx='10' cy='7' r='3'/%3E%3Cpath d='M3.5 16.5c1-3.3 3.8-5 6.5-5s5.5 1.7 6.5 5' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center / 14px;
}
.sidebar-footer-label { font-size: 0.78rem; color: #ffffff; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; text-align: left; }
.sidebar-user-chevron { width: 14px; height: 14px; flex: none; color: rgba(255, 255, 255, 0.72); transition: transform 0.15s ease, opacity 0.15s ease; }
.sidebar-user-trigger[aria-expanded="true"] .sidebar-user-chevron { transform: rotate(180deg); }

.sidebar-user-popover {
  position: absolute; left: 0; right: 0; bottom: calc(100% + 6px);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
  padding: 5px; z-index: 5;
  animation: dropdown-pop-in 0.12s ease-out;
}
.sidebar-user-popover[hidden] { display: none; }
.sidebar-user-popover-item {
  width: 100%; display: flex; align-items: center; gap: 8px;
  background: none; border: none; border-radius: var(--radius-sm);
  padding: 8px 9px; font-size: 0.82rem; font-weight: 600; color: var(--ink-700);
  cursor: pointer; text-align: left; min-height: 36px;
}
.sidebar-user-popover-item svg { width: 16px; height: 16px; flex: none; }
.sidebar-user-popover-item:hover { background: var(--surface); }
.sidebar-user-popover-item-danger { color: var(--danger); }
.sidebar-user-popover-item-danger:hover { background: var(--danger-soft); color: var(--danger); }
@keyframes dropdown-pop-in { from { opacity: 0; transform: translateY(3px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }

.main-col { display: flex; flex-direction: column; min-width: 0; }

/* shadcn redesign: a heavy drop shadow ("depth shadow") read as dated next to
   the rest of the flatter system — a plain 1px border is enough to separate
   the topbar from scrolled content, matching shadcn's general preference for
   borders over shadows to define surfaces. Title set in a clearer weight so
   it reads as the page's actual heading rather than a label. */
.topbar {
  /* Diagnosis fix (prerequisite for this pass' "verify text contrast and
     visibility" requirement): was a hardcoded rgba(255,255,255,0.88) that
     never reacted to the dark-mode token swap while its own text
     (--ink-900/--ink-500) did — producing illegible near-white-on-white
     text whenever the OS was in dark mode. Switched to the --panel token
     (now also locked to light via admin.html/index.html's data-theme, see
     those files) so the topbar is correctly theme-reactive like every
     other surface in this file. */
  background: var(--panel);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  border-bottom: 1px solid var(--border);
  padding: 14px 20px;
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm);
  flex-wrap: wrap;
  position: sticky; top: 0; z-index: 10;
}
.topbar-left { display: flex; align-items: center; gap: var(--space-xs); min-width: 0; }
.topbar-titles { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.topbar-route-title { font-size: 1.12rem; font-weight: 700; letter-spacing: -0.01em; color: var(--ink-900); }
.topbar-breadcrumb { font-size: 0.72rem; color: var(--ink-500); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mobile-nav-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 8px; color: var(--ink-700); cursor: pointer; min-width: var(--touch-min); min-height: var(--touch-min); flex: none;
}
.topbar-right { display: flex; align-items: center; gap: var(--space-xs); flex-wrap: wrap; }

/* Admin Dashboard CSS refinements pass: "apply the existing teal-to-blue
   gradient to the top header bar, matching the sidebar" — the exact same
   gradient literal .sidebar uses above, so the two surfaces read as one
   continuous shell. Scoped to html[data-app="admin"] (admin.html only, see
   that attribute's own comment further up this file) rather than the bare
   .topbar selector, so index.html's legacy Capture screen and worker.html's
   .worker-topbar (a DIFFERENT, already-light-paneled class combined with
   .topbar only in worker.html's own markup) are both completely unaffected
   — this never touches Worker Capture. Title/breadcrumb/the mobile nav
   toggle are recolored for contrast the same way the sidebar already
   recolors its own text/icons against this gradient; the hairline border is
   softened to a translucent white seam to match the sidebar's internal
   dividers instead of the now-invisible light-gray --border. */
html[data-app="admin"] .topbar {
  background: linear-gradient(180deg, #087F74 0%, #2176BD 100%);
  border-bottom-color: rgba(255, 255, 255, 0.16);
}
html[data-app="admin"] .topbar-route-title { color: #ffffff; }
html[data-app="admin"] .topbar-breadcrumb { color: rgba(255, 255, 255, 0.78); }
html[data-app="admin"] .mobile-nav-toggle {
  background: transparent; border-color: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.9);
}
html[data-app="admin"] .mobile-nav-toggle:hover { background: rgba(255, 255, 255, 0.16); }

/* Data freshness indicator (Artifact 1 design system priority). Reusable on
   any screen that loads live data: a small pill, a pulsing dot while fresh,
   and the exact relative time of the last successful load — never a fake
   "live" claim, since it reports a real timestamp captured client-side right
   after each successful fetch (see updateFreshness() in admin-app.js). */
.freshness-pill {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.74rem; color: var(--ink-500); font-weight: 600;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: 4px 10px 4px 8px; background: var(--panel); flex: none;
}
.freshness-pill-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); flex: none; animation: freshness-pulse 2.2s ease-in-out infinite; }
.freshness-pill.is-stale .freshness-pill-dot { background: var(--warn); animation: none; }
@keyframes freshness-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { .freshness-pill-dot { animation: none; } }
@media (max-width: 640px) { .freshness-pill span.freshness-text-label { display: none; } }

/* ------------------------------------------------------- inline notices */
.notice-region {
  position: fixed; left: var(--space-sm); right: var(--space-sm); bottom: var(--space-sm);
  display: flex; flex-direction: column; gap: var(--space-2xs);
  z-index: 100; pointer-events: none;
}
.notice {
  pointer-events: auto;
  background: var(--panel-inverse); border: 1px solid rgba(255,255,255,0.14);
  color: #fff; border-radius: var(--radius-md); box-shadow: var(--shadow-md);
  padding: var(--space-xs) var(--space-sm); font-size: 0.86rem; line-height: 1.45;
  display: flex; align-items: flex-start; gap: var(--space-2xs);
  animation: notice-in 0.18s ease-out;
  max-width: 560px; margin: 0 auto; width: 100%;
}
.notice::before { content: ''; width: 8px; height: 8px; border-radius: 50%; margin-top: 6px; flex: none; background: var(--sidebar-accent); } /* Industrial Premium: was a hardcoded light blue; the toast sits on the same fixed-dark panel-inverse surface as the sidebar, so it reuses that surface's teal accent for consistency */
.notice-success::before { background: #4ade80; }
.notice-error { border-color: rgba(248, 113, 113, 0.4); }
.notice-error::before { background: #f87171; }
.notice.notice-leaving { opacity: 0; transition: opacity 0.2s ease; }
@keyframes notice-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@media (min-width: 431px) {
  .notice-region { left: auto; right: var(--space-lg); bottom: var(--space-lg); width: 380px; }
  .notice { max-width: none; }
}

/* -------------------------------------------------------------- main --- */
.main-content { padding: var(--space-md) var(--space-sm) var(--space-xl); max-width: 1180px; width: 100%; margin: 0 auto; }

/* Admin Dashboard layout-width correction pass: the flat 1180px cap above
   is shared by admin.html AND index.html's own #mainContent (both use the
   exact same `<main class="main-content" id="mainContent">` markup under
   `.app-shell`), which is exactly why every admin screen showed "excessive
   whitespace on both sides" on anything wider than ~1180px plus the
   sidebar — there was no override at any wider breakpoint for either page.
   Scoped to admin.html ONLY via the `.admin-main` class added to that
   file's <main> tag (`.main-content.admin-main`, two classes, so it wins
   over the single-class rule above regardless of source order) — this
   intentionally mirrors the precedent already set for Worker Capture's own
   page below (`.worker-main`, which already escalates its own max-width
   per breakpoint for the identical "too much unused space" complaint) but
   fixes the ONE shared constraint for every admin screen at once per this
   task's own requirement ("fix shared layout constraints instead of
   introducing individual page-specific hacks"), rather than adding a hack
   to each of the six screens individually. index.html keeps the unmodified
   1180px cap above untouched, and worker.html is entirely unaffected
   because it never carries `.admin-main`. */
.main-content.admin-main { max-width: none; }

/* Industrial Clarity: a clearer page-title tier -- larger/bolder than a
   .card's own h3 headings (1rem/600, see .admin-stage-card-head h3 etc.) and
   now visually separated from the content below it with a hairline, instead
   of relying on margin alone to imply "this is the page title, everything
   below is its content." */
.screen-head { margin-bottom: var(--space-md); padding-bottom: var(--space-sm); border-bottom: 1px solid var(--border); }
.screen-head h2 { font-size: 1.2rem; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 3px; }
.screen-head p { color: var(--ink-500); font-size: 0.86rem; }
/* Download QC Entries as Excel feature: a screen-head with a trailing
   action button (e.g. "Download Excel" on All Readings), reusing the same
   flex pattern the workbook panel's head row already established. */
.screen-head-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.screen-head-row h2, .screen-head-row p { margin-bottom: 0; }
.screen-head-row h2 { margin-bottom: 3px; }

.section-title { font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-500); margin-bottom: var(--space-xs); }

/* --------------------------------------------------------------- cards */
.card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-md); box-shadow: var(--shadow-sm); }
.card + .card { margin-top: var(--space-md); }
/* Industrial Clarity: a plain <h3> directly inside a .card (e.g. the Session
   Detail modal's "QC document"/"Readings"/"Audit history" cards) had no
   scoped size at all -- it fell through to the browser's own default h3
   size, visibly larger/heavier than this app's compact admin typography
   scale everywhere else. More specific rules elsewhere (.admin-stage-card-
   head h3, .live-reading-head h2, etc.) already override this for their own
   contexts and are unaffected. */
.card > h3 { font-size: 0.95rem; font-weight: 700; margin: 0 0 var(--space-xs); color: var(--ink-900); }

/* ------------------------------------------------------------ badges --- */
.badge { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 2px 9px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.01em; white-space: nowrap; }
.badge-ok { background: var(--ok-soft); color: var(--ok); border: 1px solid var(--ok-border); }
.badge-danger { background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger-border); }
.badge-warn { background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn-border); }
.badge-neutral { background: var(--neutral-soft); color: var(--ink-500); border: 1px solid var(--neutral-border); }
.badge-corrected { background: var(--corrected-soft); color: var(--corrected); border: 1px solid var(--corrected-border); }
.badge-accent { background: var(--accent-soft); color: var(--accent-dark); border: 1px solid var(--accent-border); }
/* Field Reference refinement pass: "no threshold defined" reuses the same
   --info blue QcGrid's own .rule-unresolved rows already use for "no
   confirmed verdict" — one consistent color language for that idea across
   the app, distinct from badge-neutral's gray (reserved for a field that
   was never the kind of field a threshold applies to at all). */
.badge-info { background: var(--info-soft); color: var(--info); border: 1px solid var(--info-border); }
/* Multi-stage pass: every stage is now a real, usable workflow, so these
   two tags (a binary "is this stage live" label) no longer apply to any
   screen — rule_status now varies per FIELD instead (see .badge-* above,
   used by Field Reference's per-field tags). Left defined only in case an
   older cached page still references them; nothing in web/*.js emits them
   any more. */
.tag-implemented { background: var(--ok-soft); color: var(--ok); border: 1px solid var(--ok-border); border-radius: var(--radius-sm); padding: 3px 9px; font-size: 0.74rem; font-weight: 700; }
.tag-prototype { background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn-border); border-radius: var(--radius-sm); padding: 3px 9px; font-size: 0.74rem; font-weight: 700; }

/* -------------------------------------------------------- empty states */
.empty-state { text-align: center; padding: 36px 16px; color: var(--ink-500); }
.empty-state-icon { width: 36px; height: 36px; margin: 0 auto 12px; color: var(--ink-300); }
.empty-state h3 { color: var(--ink-700); font-size: 0.95rem; margin-bottom: 4px; }
.empty-state p { font-size: 0.84rem; line-height: 1.5; max-width: 440px; margin: 0 auto; }
.empty-state code { background: var(--ink-100); border-radius: 4px; padding: 1px 5px; }

/* ---- Phase 3: Audit Trail loading / error states — same .empty-state
   shell as the empty state above, with a modifier for color (error reuses
   --danger, already themed for light/dark) and a small spinner (reuses
   the existing orb-spin keyframe rather than defining a new one). */
.empty-state.is-error .empty-state-icon,
.empty-state.is-error h3 { color: var(--danger); }
.spinner {
  width: 28px; height: 28px; margin: 0 auto 12px;
  border: 3px solid var(--neutral-border); border-top-color: var(--accent);
  border-radius: 50%; animation: orb-spin 0.8s linear infinite;
}
.audit-meta { color: var(--ink-500); font-size: 0.78rem; margin: var(--space-xs) 0; }
.qc-status-badge.corrected { background: var(--corrected-soft); color: var(--corrected); border: 1px solid var(--corrected-border); }

/* ---- Phase 4: Field Reference (Field Reference refinement pass: grid
   columns replace the old space-between flex row, so Field/Unit/Status/
   Threshold line up consistently down the whole card instead of each
   row wrapping its own way — reuses .card/.badge-* wholesale). A header
   row (.reference-field-head) labels the four columns once per stage
   rather than repeating "Unit"/"Status" text beside every field. */
.reference-stage-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-sm); flex-wrap: wrap; margin-bottom: var(--space-xs); }
.reference-stage-label { font-size: 0.98rem; margin-bottom: 2px; }
.reference-stage-ref { color: var(--ink-500); font-size: 0.8rem; }
.reference-field-row {
  display: grid;
  grid-template-columns: minmax(150px, 1.5fr) 56px minmax(120px, 170px) minmax(140px, 1.2fr);
  column-gap: var(--space-sm); row-gap: 2px; align-items: center;
  padding: 9px 0; border-bottom: 1px solid var(--border);
}
.reference-field-row:last-child { border-bottom: none; }
.reference-field-head {
  padding-top: 0; padding-bottom: 6px; border-bottom: 1px solid var(--border-strong);
  text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.68rem; font-weight: 700; color: var(--ink-500);
}
.reference-col-name { color: var(--ink-700); font-size: 0.88rem; }
.reference-field-head .reference-col-name { color: var(--ink-500); font-size: 0.68rem; }
.reference-col-unit { color: var(--ink-500); font-size: 0.82rem; }
.reference-col-status { display: flex; align-items: center; }
.reference-col-threshold { color: var(--ink-900); font-weight: 600; font-size: 0.86rem; line-height: 1.4; }
.reference-pending-tag { color: var(--ink-500); font-weight: 500; font-size: 0.78rem; font-style: italic; }
.reference-blocked-reason { color: var(--ink-700); font-size: 0.86rem; line-height: 1.5; margin-top: var(--space-2xs); }
.reference-unconfirmed-note { color: var(--warn); font-size: 0.8rem; margin-top: var(--space-2xs); font-style: italic; }
/* Multi-stage pass: an Area's own .notes (a stage-wide caveat, e.g. a form
   discrepancy) and a Field's own .note (shown per field row below its
   standard text) — both verbatim text from backend/qc_schema.py, never
   paraphrased here. Spans every column so a long ambiguity note never
   fights the grid for width. */
.reference-stage-notes { color: var(--ink-500); font-size: 0.82rem; line-height: 1.5; margin: 0 0 var(--space-xs); font-style: italic; }
.reference-field-note { grid-column: 1 / -1; color: var(--ink-500); font-size: 0.78rem; line-height: 1.4; margin-top: 2px; }

/* Phone widths: four tight grid columns stop being legible for long field
   names (task requirement: "long field names and notes work on smaller
   screens") — stack name/unit/status/threshold instead of forcing a
   horizontal squeeze or scroll. */
@media (max-width: 560px) {
  .reference-field-row { grid-template-columns: 1fr; row-gap: 4px; }
  .reference-field-head { display: none; }
  .reference-col-unit::before { content: "Unit: "; color: var(--ink-500); }
  .reference-col-threshold::before { content: "Threshold: "; color: var(--ink-500); font-weight: 500; }
}

/* ---- Field Reference refinement pass, task 6: Standards Management —
   a governance log, not a Field Reference redesign, so it gets its own
   small set of rules rather than overloading .reference-* (that screen
   stays read-only; this one has real forms/buttons). Reuses
   .card/.qc-btn/.qc-value-input/.badge-* wholesale. */
.standards-admin-card { border-left: 3px solid var(--accent); }
.standards-admin-note { color: var(--ink-700); font-size: 0.86rem; line-height: 1.5; margin: 0 0 var(--space-sm); }
.standards-admin-label { display: block; font-size: 0.8rem; font-weight: 600; color: var(--ink-700); margin-bottom: var(--space-2xs); }
.standards-admin-input {
  width: 100%; max-width: 360px; min-height: var(--touch-min);
  background: var(--panel); border: 1px solid var(--border-strong); color: var(--ink-900);
  border-radius: var(--radius-sm); padding: var(--space-2xs) var(--space-xs); font-size: 0.95rem;
}
.standards-stage-card + .standards-stage-card { margin-top: var(--space-md); }
.standards-field-block { padding: var(--space-sm) 0; border-bottom: 1px solid var(--border); }
.standards-field-block:last-child { border-bottom: none; }
.standards-field-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.standards-field-head .qc-btn { flex: none; min-height: 36px; padding: 0 var(--space-sm); }
.standards-propose-form {
  display: flex; flex-wrap: wrap; gap: var(--space-sm); align-items: flex-end;
  margin-top: var(--space-sm); padding: var(--space-sm); background: var(--neutral-soft);
  border: 1px solid var(--neutral-border); border-radius: var(--radius-md);
}
.standards-propose-form label {
  display: flex; flex-direction: column; gap: 3px; font-size: 0.78rem; font-weight: 600; color: var(--ink-700);
  flex: 1 1 110px; min-width: 90px;
}
.standards-propose-form input[type="text"],
.standards-propose-form input[type="number"],
.standards-propose-form select {
  min-height: var(--touch-min); background: var(--panel); border: 1px solid var(--border-strong);
  color: var(--ink-900); border-radius: var(--radius-sm); padding: var(--space-2xs) var(--space-xs); font-size: 0.9rem;
}
.standards-field-inclusive { flex-direction: row !important; align-items: center; gap: var(--space-2xs) !important; font-weight: 500 !important; }
.standards-propose-form button[type="submit"] { flex: 1 1 100%; }
.standards-proposal-list { margin-top: var(--space-xs); display: flex; flex-direction: column; gap: var(--space-xs); }
.standards-proposal-row {
  background: var(--neutral-soft); border: 1px solid var(--neutral-border); border-radius: var(--radius-sm);
  padding: var(--space-xs) var(--space-sm);
}
.standards-proposal-actions { display: flex; gap: var(--space-2xs); margin-top: var(--space-2xs); }
.standards-proposal-actions .qc-btn { flex: none; min-height: 32px; padding: 0 var(--space-sm); font-size: 0.78rem; }
@media (max-width: 560px) {
  .standards-field-head { flex-direction: column; align-items: flex-start; }
  .standards-field-head .qc-btn { width: 100%; }
}

/* ---- Phase 4: Settings & Help — plain prose cards; this list style is
   the only new rule (card/section-title are reused as-is). */
.settings-help-list { padding-left: 1.1em; display: flex; flex-direction: column; gap: var(--space-2xs); font-size: 0.88rem; line-height: 1.55; color: var(--ink-700); }

/* ---- Phase 5: Excel Import — upload + field-mapping review only. Reuses
   .card/.qc-btn/.qc-value-input/.badge-*/.empty-state wholesale; the rules
   below are the only new layout/visuals this screen needed. */
.excel-upload-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); }
.excel-upload-row input[type="file"] { flex: 1 1 220px; min-width: 0; font-size: 0.86rem; color: var(--ink-700); }
.excel-upload-hint { color: var(--ink-500); font-size: 0.8rem; margin-top: var(--space-xs); line-height: 1.5; }

.excel-sheet-tabs { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-bottom: var(--space-sm); }
.excel-sheet-tab {
  background: var(--neutral-soft); border: 1px solid var(--neutral-border); color: var(--ink-700);
  border-radius: 999px; padding: 5px 13px; font-size: 0.8rem; font-weight: 600; cursor: pointer;
  min-height: var(--touch-min);
}
.excel-sheet-tab:hover { border-color: var(--accent); color: var(--accent-dark); }
.excel-sheet-tab.active { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast); }

.excel-header-row {
  display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: var(--space-sm);
  padding: 12px 0; border-bottom: 1px solid var(--border);
}
.excel-header-row:last-child { border-bottom: none; }
.excel-header-info { min-width: 0; flex: 1 1 260px; }
.excel-header-name { font-weight: 600; color: var(--ink-900); word-break: break-word; }
.excel-header-flags { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-2xs); }
.excel-header-message { color: var(--ink-500); font-size: 0.8rem; margin-top: var(--space-2xs); line-height: 1.5; }
.excel-header-controls { flex: 1 1 220px; max-width: 320px; }
.excel-field-select { width: 100%; }

.excel-mapping-summary { color: var(--ink-700); font-size: 0.86rem; margin-bottom: var(--space-sm); }

/* Long-/wide-format detection banner, "existing value" display per header,
   and the recognized-but-excluded metadata note — Excel upload workflow
   fix (long-format workbook support). */
.excel-format-note {
  background: var(--neutral-soft); border: 1px solid var(--neutral-border); border-radius: var(--radius-sm);
  color: var(--ink-700); font-size: 0.82rem; line-height: 1.5; padding: 8px 12px; margin-bottom: var(--space-sm);
}
/* PDF-upload feature pass. */
.excel-pdf-preview {
  width: 100%; height: 420px; border: 1px solid var(--neutral-border); border-radius: var(--radius-md);
  margin-bottom: var(--space-sm); background: var(--panel);
}
.excel-blank-template-note {
  background: var(--info-soft); border: 1px solid var(--info-border); border-radius: var(--radius-md);
  color: var(--info); font-size: 0.86rem; line-height: 1.55; padding: 10px 12px; margin-bottom: var(--space-sm);
}
.excel-header-existing-value { color: var(--ink-700); font-size: 0.8rem; margin-top: var(--space-2xs); line-height: 1.5; }
.excel-header-existing-value strong { color: var(--ink-900); font-weight: 600; }
.excel-header-occurrence { color: var(--ink-500); font-size: 0.78rem; }
.excel-metadata-note {
  color: var(--ink-500); font-size: 0.8rem; line-height: 1.5; margin-top: var(--space-sm);
  padding-top: var(--space-sm); border-top: 1px dashed var(--neutral-border);
}
.excel-approved-note {
  color: var(--ok); font-size: 0.86rem; font-weight: 600; margin-top: var(--space-sm);
  background: var(--ok-soft); border: 1px solid var(--ok-border); border-radius: var(--radius-md); padding: 10px 12px;
}
/* PDF-upload feature pass: explains why Export is hidden for a PDF-origin
   linked workbook (web/gri-app.js's linkWorkbook()). */
.workbook-export-note {
  background: var(--neutral-soft); border: 1px solid var(--neutral-border); border-radius: var(--radius-sm);
  color: var(--ink-700); font-size: 0.82rem; line-height: 1.5; padding: 8px 12px; margin-top: var(--space-xs);
}

/* ---- Phase 2: Pending Review / All Readings ---------------------------
   Rows reuse .qc-row/.qc-field-label/.qc-standard/.qc-value/
   .qc-status-badge/.qc-edit-slot/.qc-actions/.qc-btn and .context-chip
   wholesale (all defined below/above already, same classes qc-grid.js's
   Capture rows use) — only the chip-wrapping row below is new. */
.reading-list { display: flex; flex-direction: column; gap: var(--space-xs); }
.reading-row-chips { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-2xs); }

/* --------------------------------------------------------- stage select */
.stage-select h2 { font-size: 1.1rem; margin-bottom: var(--space-3xs); }
.stage-select-hint { color: var(--ink-500); font-size: 0.88rem; }
.stage-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}
.stage-card {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: var(--space-md); text-align: left; cursor: pointer; color: var(--ink-900); font-size: 1rem;
  min-height: var(--touch-min); box-shadow: var(--shadow-sm);
  transition: border-color 0.15s ease, transform 0.1s ease, background 0.15s ease;
}
.stage-card:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-soft); }
.stage-card:active:not(:disabled) { transform: scale(0.99); }
.stage-card:disabled { opacity: 0.55; cursor: not-allowed; }
.stage-card-label { font-weight: 600; display: block; margin-bottom: var(--space-3xs); }
.stage-card-ref { color: var(--ink-500); font-size: 0.78rem; display: block; margin-bottom: var(--space-2xs); font-variant-numeric: tabular-nums; }
.stage-card-blocked { color: var(--warn); font-size: 0.8rem; line-height: 1.4; display: block; }

/* ---------------------------------------------------------- voice stage */
.active-stage-banner {
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--space-xs);
  margin-bottom: var(--space-sm); font-size: 0.92rem; color: var(--ink-500);
}
#activeStageLabel { font-weight: 600; color: var(--ink-900); }
/* Worker Capture layout refinement: on worker.html, #activeStageLabel now
   holds only the session's own label (e.g. a shift name), set from
   worker-app.js's init() — the full stage name is shown once already, in
   #templateHeaderTitle above. When a session has no such label the element
   is left empty rather than removed (worker-app.js still always assigns to
   it), so this collapses it out of the banner's flex row cleanly instead of
   leaving a blank gap. Scoped to .worker-shell since .active-stage-banner
   is shared with index.html's legacy Capture screen, where gri-app.js
   always writes "Stage: <label>" into the same id and this rule never
   matches. */
.worker-shell #activeStageLabel:empty { display: none; }

/* Responsible Person and Shift Assignment, §3: a compact, read-only line
   identifying who/which shift this session belongs to -- deliberately
   styled as plain small text (not another bordered card) so it adds
   identification, not visual weight, next to the template header above it. */
.worker-assignment-banner {
  display: flex; flex-wrap: wrap; gap: var(--space-xs); margin: -4px 0 var(--space-xs);
  font-size: 0.82rem; color: var(--ink-500);
}
.worker-assignment-banner[hidden] { display: none; }
.worker-assignment-item:not(:empty)::before { content: '\2022'; margin-right: var(--space-xs); color: var(--ink-300); }
.worker-assignment-item:first-child::before { content: none; margin-right: 0; }
#workerAssignmentPerson { font-weight: 600; color: var(--ink-700); }
.link-btn {
  background: none; border: none; color: var(--accent); cursor: pointer; text-decoration: underline;
  font-size: 0.85rem; padding: var(--space-2xs) 0; min-height: var(--touch-min); display: inline-flex; align-items: center;
}
.link-btn:hover { color: var(--accent-dark); }

/* :not([hidden]) — the element is shown/hidden via the native `hidden`
   attribute; without this guard, this unconditional `display` would beat
   the UA default [hidden]{display:none} (equal specificity, author origin
   wins) and the strip would stay visibly rendered (empty) even hidden. */
.context-strip:not([hidden]) {
  display: flex; flex-wrap: wrap; gap: var(--space-2xs);
  margin-bottom: var(--space-md);
}
.context-chip {
  background: var(--ink-100); border: 1px solid var(--border); color: var(--ink-700);
  font-size: 0.78rem; padding: var(--space-3xs) var(--space-xs); border-radius: var(--radius-pill);
  white-space: nowrap;
}
.context-chip strong { color: var(--ink-900); font-weight: 600; }

.voice-stage-body { display: flex; flex-direction: column; gap: var(--space-lg); }

.voice-workspace { display: flex; flex-direction: column; gap: var(--space-md); }
.voice-stage-main { display: flex; flex-direction: column; gap: var(--space-lg); }

/* ---- Worker Capture Layout Redesign (document-first) -------------------
   New, worker.html-ONLY containers (these class names are not used
   anywhere in web/index.html, so no .worker-shell scoping is needed to
   keep them from leaking there) replacing the old .voice-stage-body
   two-column grid above, which worker.html no longer references at all
   (its base rule and the desktop override further down are both left
   completely untouched for index.html's own, unmodified layout).
   .worker-document-primary holds the real original document
   (.official-document-section); .worker-document-side holds the compact
   informational cards (Live Reading, Live Transcription, Recent
   Readings). Mobile/tablet: a single column, document first in source
   order (brief: "document-first presentation" on mobile) — see the
   desktop-only grid override below for the side-by-side layout. */
.worker-document-layout {
  display: flex; flex-direction: column; gap: var(--space-lg);
  /* Worker Capture layout refinement: was var(--space-lg) — tightened
     along with the other session-info spacing above so Live Reading (now
     the first thing in this flex column — see the reorder comment in
     worker.html) sits closer to the top of the page, needing less scroll
     to reach on short viewports. */
  margin-top: var(--space-md);
}
.worker-document-primary { min-width: 0; }
.worker-document-side { display: flex; flex-direction: column; gap: var(--space-md); min-width: 0; }
/* Worker Capture layout refinement: Recent Readings' list previously had no
   height limit and grew with every reading, pushing whatever followed it
   further down the page with no way to glance back at older entries without
   scrolling past all of it. Capping it and scrolling internally keeps the
   newest entries (rendered first — see worker-app.js's pushRecentReading(),
   which unshifts) always in view at the top of the list, keeps this card's
   own height predictable regardless of session length, and keeps the rest
   of .worker-document-side (and, on desktop, the sticky column it's part
   of) from growing taller than the viewport as a long recording session
   goes on. Scoped via the existing .worker-document-side ancestor (see the
   block below this one) since .recent-readings-list is shared with
   index.html's legacy Capture screen. */
.worker-document-side .recent-readings-list { max-height: 300px; overflow-y: auto; }

/* Worker Capture Layout Redesign: the voice capture controls are no longer
   a tall sidebar card (.orb-card's base rule above — flex-direction:
   column, align-items:center — is shared with index.html's own sidebar
   orb and is left completely untouched) but a short, horizontal strip
   pinned above the document. Scoped to the 2-class compound
   .orb-card.orb-card-compact (only worker.html ever adds the
   orb-card-compact modifier) so this can never affect index.html's plain
   .orb-card. The shared .orb-card base rule (background/border/radius/
   shadow) still applies underneath — this block only changes the internal
   layout/sizing. */
.orb-card.orb-card-compact {
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
}
.orb-card.orb-card-compact .orb-wrap { width: 56px; height: 56px; }
.orb-card.orb-card-compact .voice-strip-main {
  display: flex; align-items: center; gap: var(--space-sm);
  flex: 1 1 220px; min-width: 0;
}
.orb-card.orb-card-compact .orb-hint { margin: 0; text-align: left; font-size: 0.82rem; }
.orb-card.orb-card-compact .start-capture-btn { margin-top: 0; flex: none; }
.orb-card.orb-card-compact .voice-controls {
  margin-top: 0; width: auto; justify-content: flex-start; flex: none;
}
.orb-card.orb-card-compact .error-banner { flex-basis: 100%; margin-top: 0; }

@media (min-width: 431px) {
  .orb-card.orb-card-compact .orb-wrap { width: 64px; height: 64px; }
}

/* StageShow visual-polish pass: .voice-strip-main's flex:1 1 220px (above)
   deliberately grows to fill whatever room is left between the orb and the
   Capturing/Stop controls -- correct for narrow widths, but on a wide
   desktop viewport it leaves a long stretch of visually unexplained empty
   space between the hint/Start-Capture button and the Capturing/Stop pair,
   since nothing marks that gap as intentional. A thin divider reads the two
   control groups as deliberately separated zones instead of an accidental
   gap. Only applied once they share a row (matches .voice-strip-main's own
   220px flex-basis plus the orb/controls widths -- below that the row
   wraps and a left divider on a lone centered row would look stray).
   Enterprise pass: plain var(--border) now (was a low-opacity white
   hairline tuned for the old dark gradient card — invisible on today's
   plain white card). */
@media (min-width: 620px) {
  .orb-card.orb-card-compact .voice-controls {
    padding-left: var(--space-md);
    border-left: 1px solid var(--border);
  }
}

@media (min-width: 1025px) {
  .worker-document-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(300px, 360px);
    align-items: start;
    gap: var(--space-xl);
  }
  /* Worker Capture layout refinement: .worker-document-side now comes
     FIRST in source order (see worker.html) so Live Reading/Recent
     Readings lead on mobile/tablet's single-column flex layout. At this
     desktop grid breakpoint, explicit `order` restores the established
     visual arrangement — document in the wide left column, side cards in
     the narrow right column — unchanged from before the reorder. */
  .worker-document-primary { order: 1; }
  .worker-document-side { order: 2; position: sticky; top: calc(64px + var(--space-lg)); }
}

/* Industrial Clarity (Phase 3): this is the primary action surface on the
   whole worker page — everything else (live reading, caption, table) is
   downstream of what happens here — so it gets the stronger --shadow-lg
   elevation (same token the admin Session Detail modal uses) instead of
   --shadow-sm, to visually outrank the cards beneath it without any color
   or border change. */
.orb-card {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-lg) var(--space-md); display: flex; flex-direction: column; align-items: center;
  box-shadow: var(--shadow-lg);
}
/* ---- orb / waveform ---------------------------------------------------
   The orb's interior is now a real canvas animation (web/voice-orb.js,
   adapted from Marea's existing voice-assistant animation) instead of the
   previous static CSS rings/bars/dot — see that file's module docstring
   for the full reuse/adaptation notes. .orb-wrap itself keeps its
   existing per-state border-color/box-shadow ring (a GRI-only visual cue
   with no Marea equivalent, left as-is) and now also clips its canvas
   child to the circle and pins a fixed dark backdrop — the same "always
   dark, independent of the rest of the app's light/dark theme" treatment
   Marea's own voice stage uses, so the teal/gold glow always has the
   contrast it was designed against. ------------------------------------- */
.orb-wrap {
  position: relative; width: 104px; height: 104px; border-radius: 50%;
  background: #0b1220;
  overflow: hidden;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  border: 2px solid var(--border-strong); transition: border-color 0.2s ease, box-shadow 0.2s ease;
  flex: none;
}
#voiceCanvas {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
}

.orb-wrap[data-state="idle"] { color: var(--state-ready); }
.orb-wrap[data-state="connecting"] {
  border-color: var(--state-connecting); color: var(--state-connecting);
  box-shadow: 0 0 0 8px var(--warn-soft);
}
.orb-wrap[data-state="listening"] {
  border-color: var(--state-listening); color: var(--state-listening);
  box-shadow: 0 0 0 8px var(--accent-soft);
}
.orb-wrap[data-state="thinking"] {
  border-color: var(--state-thinking); color: var(--state-thinking);
  box-shadow: 0 0 0 8px var(--corrected-soft);
}
.orb-wrap[data-state="answering"] {
  border-color: var(--state-answering); color: var(--state-answering);
  box-shadow: 0 0 0 8px var(--info-soft);
  animation: orb-pulse 1.1s ease-in-out infinite;
}
.orb-wrap[data-state="error"] {
  border-color: var(--danger); color: var(--danger);
  box-shadow: 0 0 0 8px var(--danger-soft);
}

@keyframes orb-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
/* orb-spin is also used by .spinner (Phase 3's loading state, see below) —
   kept even though the orb itself no longer uses it. */
@keyframes orb-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes dot-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.75); } }
@media (prefers-reduced-motion: reduce) {
  .orb-wrap[data-state="answering"],
  .state-pill .dot { animation: none !important; }
}

.orb-hint { text-align: center; color: var(--ink-500); font-size: 0.88rem; margin: var(--space-sm) 0 0; }

.voice-controls { display: flex; gap: var(--space-xs); margin-top: var(--space-md); width: 100%; justify-content: center; flex-wrap: wrap; }
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2xs);
  background: var(--panel); border: 1px solid var(--border-strong); color: var(--ink-700);
  padding: var(--space-xs) var(--space-md); border-radius: var(--radius-sm); cursor: pointer;
  font-size: 0.86rem; font-weight: 600; min-height: var(--touch-min); min-width: 44px;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.icon-btn-icon { width: 18px; height: 18px; flex: none; }
.icon-btn:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-soft); }
.icon-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.icon-btn.muted { background: var(--danger-soft); border-color: var(--danger-border); color: var(--danger); }
.icon-btn.stop-btn { background: var(--danger-soft); border-color: var(--danger-border); color: var(--danger); }
.icon-btn.stop-btn:hover:not(:disabled) { border-color: var(--danger); background: #fee2e2; }

/* :not([hidden]) — see the note on .context-strip above. */
.error-banner:not([hidden]) {
  display: flex; align-items: flex-start; gap: var(--space-2xs);
  background: var(--danger-soft); border: 1px solid var(--danger-border); color: var(--danger);
  padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-md); margin-top: var(--space-md);
  width: 100%; font-size: 0.86rem; line-height: 1.45;
}

.caption-panel {
  background: var(--ink-100); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-md); min-height: 64px;
}
.caption-empty { color: var(--ink-500); font-size: 0.88rem; }
.caption-role { font-weight: 700; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-500); margin-bottom: var(--space-3xs); }
.caption-role.you { color: var(--accent-dark); }
.caption-text { font-size: 0.95rem; line-height: 1.5; color: var(--ink-900); }

/* ---- Live Transcription panel additions (Real-Time Speech Transcription
   Enhancement) -- worker.html-only markup (#captionLog, #captionPanel's
   head/listening-indicator, #captionTranscriptError); .caption-panel/
   .caption-empty/.caption-role/.caption-text above are untouched and still
   apply exactly as before, including on index.html (which has none of this
   new markup, so none of the rules below ever match there). */
.caption-panel-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); margin-bottom: var(--space-2xs); }
.caption-panel-title { margin: 0; font-size: 0.8rem; font-weight: 700; color: var(--ink-700); text-transform: uppercase; letter-spacing: 0.03em; }
.caption-listening-indicator {
  display: inline-flex; align-items: center; gap: var(--space-3xs);
  font-size: 0.72rem; font-weight: 700; color: var(--accent-dark);
  background: var(--accent-soft); border: 1px solid var(--accent-border);
  border-radius: 999px; padding: 2px var(--space-2xs);
}
.caption-listening-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-dark); animation: dot-pulse 1.1s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .caption-listening-dot { animation: none; } }

/* A per-utterance transcription failure (brief §7) -- amber, same severity
   language as .badge-pending elsewhere in this file, deliberately NOT the
   red .error-banner treatment: the session/mic are still fine, only one
   utterance's text didn't come through. */
.caption-transcript-error:not([hidden]) {
  display: flex; align-items: center; gap: var(--space-2xs);
  background: var(--warn-soft); border: 1px solid var(--warn-border); color: var(--warn);
  font-size: 0.82rem; font-weight: 600; border-radius: var(--radius-md);
  padding: var(--space-2xs) var(--space-xs); margin-bottom: var(--space-2xs);
}

/* Scrolling history of finalized turns -- bounded height so a long
   back-and-forth never pushes the rest of the (sticky, on desktop) voice
   column out of view (brief §6: "Avoid making the interface unnecessarily
   crowded"). Empty by default (no border/padding) until worker-app.js
   appends the first archived entry. */
.caption-log { display: flex; flex-direction: column; gap: var(--space-xs); max-height: 160px; overflow-y: auto; }
.caption-log:not(:empty) { margin-bottom: var(--space-sm); padding-bottom: var(--space-2xs); border-bottom: 1px solid var(--border); }
.caption-log-entry + .caption-log-entry { padding-top: var(--space-2xs); border-top: 1px dashed var(--border); }

/* The one actively-streaming line (#captionContent) -- distinct from the
   archived history above it. */
.caption-live-entry[hidden] { display: none; }
.caption-text.is-interim {
  color: var(--ink-700); font-style: italic;
  /* A left accent bar is enough to mark "still being recognized" without
     adding another animation to track/disable under reduced motion. */
  border-left: 2px solid var(--accent-border); padding-left: var(--space-2xs);
}

/* ------------------------------------------------------------ QC grid -- */
/* ==================================================== workbook panel ==
   Excel-first Capture redesign (web/gri-app.js's "Workbook panel"
   section). Same card/button language as every other panel on this
   screen (.card/.qc-btn/.link-btn are NOT redefined here, just reused). */
.workbook-panel {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-md); box-shadow: var(--shadow-sm);
}
.workbook-panel-head { margin-bottom: var(--space-sm); }
.workbook-panel-head h2 { font-size: 1rem; }
.workbook-panel-hint { color: var(--ink-500); font-size: 0.8rem; margin-top: var(--space-3xs); }

.workbook-empty p { color: var(--ink-500); font-size: 0.86rem; margin: 0 0 var(--space-xs); }
.workbook-empty-note { font-size: 0.78rem !important; margin-top: var(--space-xs) !important; }
.workbook-picker-row { display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.workbook-picker-select {
  flex: 1 1 220px; min-height: var(--touch-min);
  background: var(--panel); border: 1px solid var(--border-strong); color: var(--ink-900);
  border-radius: var(--radius-sm); padding: var(--space-2xs) var(--space-xs); font-size: 0.9rem;
}

/* Download QC Report feature (All Readings screen) — a stage picker + date
   picker beside the "Download QC Report" button. Reuses .workbook-picker-select
   for the stage <select>; only the date input and the row layout are new. */
.qc-report-controls {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs);
  width: 100%; margin-top: var(--space-2xs);
}
.qc-report-controls .workbook-picker-select { flex: 0 1 220px; min-width: 160px; }
.qc-report-date-input {
  min-height: var(--touch-min);
  background: var(--panel); border: 1px solid var(--border-strong); color: var(--ink-900);
  border-radius: var(--radius-sm); padding: var(--space-2xs) var(--space-xs); font-size: 0.9rem;
}
.workbook-picker-status {
  font-size: 0.8rem; margin-top: var(--space-xs); padding: var(--space-2xs) var(--space-xs);
  border-radius: var(--radius-sm); background: var(--panel-muted, var(--panel)); border: 1px solid var(--border-strong);
}
.workbook-picker-status.is-error {
  background: var(--danger-soft); border-color: var(--danger-border); color: var(--danger);
}

.workbook-file-info { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); margin-bottom: var(--space-sm); }
.workbook-file-name { font-weight: 600; color: var(--ink-900); overflow-wrap: anywhere; }

.workbook-field-preview { display: flex; flex-direction: column; gap: var(--space-2xs); margin-bottom: var(--space-sm); }
.workbook-field-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs);
  padding: var(--space-2xs) var(--space-xs); border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-size: 0.82rem; background: var(--neutral-soft);
}
.workbook-field-row.has-conflict { border-color: var(--danger-border); background: var(--danger-soft); }
.workbook-field-name { font-weight: 600; color: var(--ink-900); min-width: 0; overflow-wrap: anywhere; }
.workbook-field-cell { color: var(--ink-500); font-size: 0.74rem; white-space: nowrap; }

/* "Import readings" — the missing step between a linked workbook and real
   pending QC readings (backend/qc_import.py). Sits between the field
   preview and the Export button in the same workbook panel. */
.workbook-import-panel { margin-bottom: var(--space-sm); }
.workbook-import-status { color: var(--ink-500); font-size: 0.8rem; margin-bottom: var(--space-2xs); }
.workbook-import-status.is-ready { color: var(--ink-700); }
.workbook-field-value { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }

.workbook-conflict-banner {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs);
  background: var(--danger-soft); border: 1px solid var(--danger-border); color: var(--danger);
  border-radius: var(--radius-sm); padding: var(--space-xs); font-size: 0.82rem; margin: var(--space-2xs) 0;
}
.workbook-conflict-actions { display: flex; gap: var(--space-2xs); margin-left: auto; }

/* Compact per-row "mapped to the workbook" tag, appended inside each
   QcGrid row's .qc-row-info block — a new, additive element (never a
   replacement of info/value/status/edit-slot/actions, which qc-grid.js's
   data-role wiring and tests/test_frontend_contract.py both depend on). */
.qc-mapped-tag {
  display: inline-flex; align-items: center; gap: 4px; margin-top: 2px;
  font-size: 0.72rem; color: var(--info); background: var(--info-soft); border: 1px solid var(--info-border);
  border-radius: var(--radius-pill); padding: 1px 8px; white-space: nowrap;
}
.qc-mapped-tag.has-conflict { color: var(--danger); background: var(--danger-soft); border-color: var(--danger-border); }

/* Compact per-row conflict-resolution banner — also nested inside
   .qc-row-info (never a new direct .qc-row grid child, so the
   grid-template-areas below are untouched). Shown whenever the workbook's
   ORIGINAL cell for this field already held a value and the operator
   hasn't resolved it yet via POST .../resolve-conflict. This is
   deliberately independent of the row's pending/committed state and
   never gates the Confirm button: resolving it only records an audit
   answer (backend/excel_import_store.py's resolve_conflict docstring),
   it is not a second enforcement path over the QC confirm pipeline. */
.qc-row-conflict {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2xs);
  background: var(--danger-soft); border: 1px solid var(--danger-border); color: var(--danger);
  border-radius: var(--radius-sm); padding: 2px var(--space-xs); font-size: 0.72rem; margin-top: var(--space-2xs);
}
.qc-row-conflict-actions { display: flex; gap: 4px; }
.qc-row-conflict-actions button { font-size: 0.72rem; padding: 1px 8px; min-height: 0; }

.qc-grid-section {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-md); box-shadow: var(--shadow-sm);
}
.qc-grid-head { margin-bottom: var(--space-sm); }
.qc-grid-head h2 { font-size: 1rem; }
.qc-grid-subhead { color: var(--ink-500); font-size: 0.8rem; margin-top: var(--space-3xs); }
/* Component Integration pass: the Original Document card's header now has a
   real action slot (CardAction, in shadcn Card terms) next to its title —
   the "open in a new tab" escape hatch was previously a bare inline text
   link buried in a fallback paragraph below the (often tall) viewer; a
   small ghost icon+label button beside the title is both more discoverable
   and the conventional place for a card's one secondary action to live. */
.qc-grid-head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-sm); }
/* Qualified with the parent class (not just .document-open-btn alone) so
   this wins over .qc-btn's own flex:1 1 auto/min-height:var(--touch-min)
   base rule declared later in this file at equal specificity otherwise. */
.qc-grid-head-row .document-open-btn {
  flex: none; display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 5px var(--space-xs); font-size: 0.8rem; text-decoration: none;
}
.document-open-btn svg { width: 15px; height: 15px; flex: none; }

/* Critical Fix: "Display the Actual Original QC PDF in Worker Capture"
   pass. Same card chrome as .qc-grid-section (consistent light-theme
   container), placed above it so the real document is the first thing a
   worker sees. */
.official-document-section {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-md);
  /* Industrial Premium (Task E §10): bumped from --shadow-sm to --shadow-lg
     — this card holds the real original document and the brief asks for it
     to "look like an integrated product component," so it gets the same
     elevated treatment as the voice capture card and live-reading card
     rather than sitting as a plain flat panel. */
  box-shadow: var(--shadow-lg);
}
/* .official-document-section is worker-only (not shared with index.html),
   so this compound selector is naturally scoped without needing
   .worker-shell — it can't match anywhere else. A small teal rule beside
   "Original Document" for a touch more visual weight on the heading. */
.official-document-section .qc-grid-head h2 { display: flex; align-items: center; gap: 8px; }
.official-document-section .qc-grid-head h2::before {
  content: ''; width: 4px; height: 18px; border-radius: 2px; flex: none;
  background: var(--worker-teal-bright, var(--accent));
}
/* Worker Capture Layout Redesign (2026-11): previously this base
   (desktop, min-width: 1025px) rule was a flat `height: 70vh; min-height:
   420px; max-height: 760px` box — a reasonable guess for a desktop-wide
   viewer, but exactly the kind of "enlarge without checking the document's
   own shape" sizing the redesign brief explicitly asked NOT to do
   ("calculate a suitable fit based on viewport dimensions and document
   aspect ratio", not "do not simply enlarge the PDF beyond its available
   space"). The `aspect-ratio` approach the previous "Mobile Responsiveness"
   fix already proved out below 1025px (matching the iframe's box to the
   REAL on-disk page geometry — A4 landscape, 841.89 x 595.30pt, the same
   ratio verified across every file in backend/report_templates/ by
   tests/test_worker_pdf_mobile_responsiveness.py via pypdf.PdfReader) is
   now the one formula used at every breakpoint instead of two different
   strategies: width:100% + aspect-ratio means the box's HEIGHT is always
   derived from however wide .worker-document-primary actually renders at
   that breakpoint, so the browser's native PDF viewer can fill it edge to
   edge without over- or under-sizing relative to the real document. The
   min-width tiers below only raise the defensive min/max-height FLOOR and
   CEILING as more real vertical space becomes available (more room on a
   tablet than a phone, more again on desktop) — they never assume a fixed
   vh fraction. No change to officialDocumentPreview.src/worker-app.js's
   fetch/ETag/Blob-URL pipeline, the error/retry panel, or the download
   fallback link — this is a pure CSS sizing change, same as the original
   mobile-responsiveness fix this extends. */
.worker-pdf-preview {
  width: 100%;
  aspect-ratio: 841.89 / 595.30;
  height: auto;
  min-height: 220px;
  /* Automatic PDF Fit-to-Screen: max-height carries a ~44px cushion above
     the pure aspect-ratio value. Chrome's native PDF viewer (see
     worker-app.js's #view=FitH fragment) draws its own fixed-height toolbar
     INSIDE the iframe, on top of the page content, which the aspect-ratio
     formula alone can't account for — without this, a fit-to-width page
     would have its bottom edge sit ~44px below the visible content area,
     forcing a small scroll to reach it on every load. This doesn't remove
     that scroll entirely in every viewport shape (aspect-ratio + height:auto
     only reaches this ceiling in wide/short viewports), but it meaningfully
     reduces how often it's needed, at zero cost to the floor (min-height)
     or to the existing shape-correctness this box already had. */
  max-height: 604px;
  /* Industrial Premium: a touch more rounding (--radius-lg vs. the
     previous --radius-md) and a dark frame via box-shadow/padding-less
     border for a more "integrated viewer" feel. */
  border: 2px solid var(--border-strong); border-radius: var(--radius-lg);
  /* StageShow visual-polish pass: was var(--panel) (pure white), the exact
     same color as the card around it -- so with no PDF loaded yet (or one
     still fetching) the whole "Original Document" card read as one big
     blank void with no visible boundary for where the page itself is. A
     step down to var(--surface) gives the viewport its own "canvas" tone,
     the same convention most document/PDF viewers use (a gray tray the
     white page sits on) -- it reads as an intentional viewing surface
     whether a document is loaded or not, instead of changing anything
     about the document pipeline itself. */
  background: var(--surface); display: block;
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.04);
}
@media (min-width: 431px) {
  .worker-pdf-preview { min-height: 320px; max-height: 684px; }
}
@media (min-width: 1025px) {
  /* Worker Capture Layout Redesign: desktop used to stop at 760px tall no
     matter how much width .worker-document-primary actually had (it was
     only ~2/3 of .worker-main before, next to the 360px voice sidebar) —
     now that this column can be the page's (near-)full width, the ceiling
     is raised so a wide viewport's correctly-aspect-ratioed box isn't
     clipped short; the floor is unchanged from the old desktop min-height
     so this never renders smaller than the previous desktop box did. */
  .worker-pdf-preview { min-height: 420px; max-height: 944px; }
}
/* Blank-preview fix: an AUTHOR stylesheet rule's `display` always wins over
   the browser's own `[hidden] { display: none }` UA rule, regardless of
   selector specificity (origin beats specificity in the cascade) — so the
   unconditional `display: block` above meant `officialDocumentPreview.hidden
   = true` had NO visual effect at all, and this iframe (and, below, the
   error panel) stayed permanently on-screen together no matter what state
   worker-app.js thought it was in. A `[hidden]` rule scoped to the same
   class has higher specificity than the bare class selector, so it wins
   within this stylesheet and actually hides the element. */
.worker-pdf-preview[hidden] { display: none; }

.official-document-fallback-note {
  color: var(--ink-500); font-size: 0.78rem; margin-top: var(--space-xs);
}
.official-document-fallback-note a { color: var(--accent); }

/* Blank-preview fix: a clear, actionable error in place of a silent blank
   box whenever the document genuinely fails to load (server error, a
   non-PDF response, or a network failure) — never leave the worker staring
   at empty white space with no explanation. */
.official-document-error {
  min-height: 420px; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-sm); text-align: center; padding: var(--space-lg);
  border: 2px dashed var(--danger-border); border-radius: var(--radius-md); background: var(--danger-soft);
  color: var(--danger);
}
.official-document-error p { font-size: 0.9rem; max-width: 480px; }
/* Same cascade-origin fix as .worker-pdf-preview[hidden] above — without
   this, officialDocumentError.hidden = true never actually hid the panel. */
.official-document-error[hidden] { display: none; }

.qc-grid { display: flex; flex-direction: column; gap: var(--space-md); }

/* Stage-Specific QC Templates pass: groups a stage's rows under the same
   processing-step headers the source paper form prints (Stunning/Bleeding/
   Filleting, Skinning/Trimming/Cektor, ...) — see web/qc-grid.js's
   _renderSkeleton(). Shared by both the legacy Capture screen and the
   Worker page (QcGrid is the same component on both); an area with no
   section metadata still renders as the original flat .qc-grid list (no
   .qc-section wrapper at all), so this is purely additive. */
.qc-section { display: flex; flex-direction: column; gap: var(--space-xs); }
.qc-section-title {
  font-size: 0.82rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--ink-500); margin: 0; padding-bottom: 2px; border-bottom: 1px solid var(--border);
}
.qc-section-note { margin: 0; color: var(--ink-500); font-size: 0.76rem; font-style: italic; }

/* ============================================================================
   Exact Original QC PDF Template pass — Worker Capture only (QcGrid.
   useFormLayout; see web/qc-grid.js's _renderFormLayoutTable()). Reproduces
   the source QC-RK PDF's own table: merged header groups, a standards row,
   footnotes, and exactly one live, interactive data row. Every "field"
   column's cell is the SAME .qc-row markup every other rendering path uses
   (just compacted to fit a PDF-width column instead of a full-width card),
   so every existing .qc-row/[data-role] rule above still applies — this
   section only ADDS table chrome and a few new, narrowly-scoped classes.
   Mobile: the table scrolls horizontally in its own wrapper so it keeps its
   original column layout instead of being squeezed illegible, while the
   voice controls (a separate section in the page) stay fully in place and
   reachable. ========================================================== */
.qc-form-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--panel); border: 1px solid var(--border-strong); border-radius: var(--radius-md);
}
.qc-form-table {
  border-collapse: collapse;
  width: 100%;
  min-width: 720px; /* below this, labels stop being legible — scroll instead of squeeze */
}
/* Light-Colored, Glove-Friendly Interface pass: table borders strengthened
   from --border to --border-strong (and widened to 2px) so the original
   PDF's row/column structure stays visually crisp against the new light
   background, even under fluorescent fish-processing-floor lighting or a
   glove-smudged screen — not merely "same hairline, lighter page". */
.qc-form-table th,
.qc-form-table td {
  border: 2px solid var(--border-strong);
  padding: var(--space-2xs) var(--space-2xs);
  vertical-align: top;
  text-align: center;
}
/* Industrial Clarity (Phase 4): darkened from --neutral-soft/--ink-700 to
   --ink-200/--ink-900 and bumped to font-weight 800 with a touch of
   letter-spacing — clearer separation between the header row and the data
   rows beneath it, especially once a row's own state tint (pending/
   committed/out-of-spec/unresolved, below) starts competing for
   attention. Table structure, column order, and every th/td this reads
   from qc-grid.js are unchanged. */
.qc-form-table thead th {
  background: var(--ink-200);
  color: var(--ink-900);
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.01em;
  line-height: 1.25;
  white-space: normal;
  vertical-align: middle;
}
/* Sticky first column (Kode Supplier/Kode Karamba, or "Jam" on vacuum's
   form) so it stays visible while the rest of a wide form scrolls
   horizontally on a phone — a progressive enhancement, never load-bearing
   for correctness. */
.qc-form-table th:first-child,
.qc-form-table td:first-child {
  position: sticky; left: 0; z-index: 1;
  background: var(--panel);
}
.qc-form-table thead th:first-child { background: var(--ink-200); z-index: 2; }

.qc-form-cell { min-width: 84px; }
.qc-form-cell-field { padding: var(--space-3xs); }
.qc-form-cell-stacked { display: flex; flex-direction: column; gap: var(--space-2xs); }
.qc-form-cell-stacked > .qc-form-cell-summary + .qc-form-cell-summary {
  border-top: 1px dashed var(--border);
  margin-top: var(--space-2xs);
  padding-top: calc(var(--space-2xs) + 2px);
}

/* ----------------------------------------------------------------------
   QC Form Readability pass — a field's "cell" in the original-PDF table is
   now this small, tappable SUMMARY (value/unit/status only), never the full
   interactive card (label, standard, edit/confirm/note controls), which
   made every control illegible once squeezed into a ~90px PDF-width
   column. Tapping it opens the exact same, completely unmodified
   .qc-row card (see web/qc-grid.js's _buildFieldRow/_buildCompactCell) in
   the shared detail panel below the table — see .qc-field-detail-panel
   further down. ---------------------------------------------------------- */
.qc-form-cell-summary {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 100%;
  /* Light-Colored, Glove-Friendly Interface pass: bumped from the 44px/56px
     bare touch-target minimum to 48px/64px — a worker tapping this with a
     wet glove needs more margin for error than a bare-finger tap target. */
  min-width: 48px;
  min-height: 64px;
  padding: var(--space-xs) var(--space-2xs);
  background: var(--panel);
  border: 2px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font: inherit;
  color: inherit;
  cursor: pointer;
  /* Industrial Clarity (Phase 4): a baseline shadow plus a brief :active
     press-scale — the same tactile-feedback language .stage-card and
     .qc-btn already use elsewhere — so these read as tappable controls at
     a glance, not static table cells. Visual only: qc-grid.js's own click
     handling (and every data-role it reads) is untouched. */
  box-shadow: var(--shadow-sm);
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}
.qc-form-cell-summary:hover { border-color: var(--accent); background: var(--accent-soft); }
.qc-form-cell-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.qc-form-cell-summary:active { transform: scale(0.97); }
.qc-form-cell-summary.qc-form-cell-selected {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent);
}
/* Industrial Clarity (Phase 4): out-of-spec and rule-unresolved previously
   changed only the border color (2px, easy to miss at a glance across a
   wide scrolled table) while state-pending already had a soft background
   tint — brought the other two up to the same visibility, using the exact
   tokens their own .qc-form-cell-status badge below already uses for the
   same state, so the cell and its own status chip never disagree. */
.qc-form-cell-summary.state-pending { border-color: var(--warn-border); background: linear-gradient(180deg, var(--warn-soft), var(--panel) 70%); }
.qc-form-cell-summary.state-committed { border-color: var(--ok-border); }
.qc-form-cell-summary.out-of-spec { border-color: var(--danger-border); background: linear-gradient(180deg, var(--danger-soft), var(--panel) 70%); }
.qc-form-cell-summary.out-of-spec .qc-form-cell-value { color: var(--danger); }
.qc-form-cell-summary.rule-unresolved { border-color: var(--info-border); background: linear-gradient(180deg, var(--info-soft), var(--panel) 70%); }
.qc-form-cell-summary.rule-unresolved .qc-form-cell-value { color: var(--info); }
.qc-form-cell-summary.just-updated { animation: qc-row-flash 2.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .qc-form-cell-summary.just-updated { animation: none; outline: 2px solid var(--accent-border); outline-offset: 1px; }
}

.qc-form-cell-sublabel { font-size: 0.6rem; font-weight: 600; color: var(--ink-500); text-transform: uppercase; letter-spacing: 0.02em; }
.qc-form-cell-value { font-variant-numeric: tabular-nums; font-size: 0.88rem; font-weight: 700; color: var(--ink-900); overflow-wrap: anywhere; }
.qc-form-cell-unit { font-size: 0.62rem; color: var(--ink-500); }
.qc-form-cell-status {
  font-size: 0.6rem; font-weight: 700; padding: 1px 7px; border-radius: var(--radius-pill);
  background: var(--neutral-soft); color: var(--ink-500); border: 1px solid var(--neutral-border);
  white-space: nowrap;
}
.qc-form-cell-summary.state-pending .qc-form-cell-status { background: var(--warn-soft); color: var(--warn); border-color: var(--warn-border); }
.qc-form-cell-summary.state-committed .qc-form-cell-status { background: var(--ok-soft); color: var(--ok); border-color: var(--ok-border); }
.qc-form-cell-summary.out-of-spec .qc-form-cell-status { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-border); }
.qc-form-cell-summary.rule-unresolved .qc-form-cell-status { background: var(--info-soft); color: var(--info); border-color: var(--info-border); }
.qc-form-cell-note-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin-top: 1px; }

/* The shared reading-detail panel — sits BELOW the scrollable table (a
   sibling of .qc-form-wrap, never inside it), hosting whichever one
   field's full, unmodified .qc-row card is currently open. Hidden rows
   inside it need an explicit [hidden] override: .qc-row's own base rule
   sets `display: grid` with author-stylesheet specificity, which otherwise
   beats the `[hidden] { display: none }` the browser's UA stylesheet
   supplies — confirmed empirically (a class-only `display` declaration
   silently defeats the bare `hidden` attribute). Same reasoning applies to
   .qc-field-detail-header, which sets `display: flex`. */
.qc-field-detail-row[hidden] { display: none; }
.qc-field-detail-header[hidden] { display: none; }
/* Vertical padding only — NOT horizontal. .qc-row already pads itself
   comfortably on every side (the base .qc-row rule, same as every other
   screen). Padding the panel horizontally TOO would double that inset and
   measurably narrow the card at phone widths (confirmed: it was enough to
   make a wide status badge like "Pending · no confirmed threshold yet"
   crowd the value column into wrapping one digit per line) — the body
   below spans the panel's full width edge-to-edge instead, exactly like
   .qc-form-wrap's table has no extra padding layer around its own cells. */
.qc-field-detail-panel {
  margin-top: var(--space-sm);
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: var(--space-sm) 0;
}
.qc-field-detail-placeholder { margin: 0; padding: 0 var(--space-sm); color: var(--ink-500); font-size: 0.84rem; font-style: italic; }
.qc-field-detail-header {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm);
  margin: 0 var(--space-sm) var(--space-sm);
  padding-bottom: var(--space-xs); border-bottom: 1px solid var(--border);
}
.qc-field-detail-title { font-weight: 700; color: var(--ink-900); font-size: 0.95rem; }
/* The detail body's .qc-row gets no overrides here at all — it's the same
   full-width, spacious card every flat/sectioned QcGrid screen already
   uses (base .qc-row rule + its tablet/desktop breakpoints below), which is
   the whole point: readable labels, standards, and full-size buttons. */

.qc-form-context-value { font-weight: 600; font-size: 0.82rem; color: var(--ink-900); }
.qc-form-context-empty { font-weight: 400; font-style: italic; color: var(--ink-500); }
.qc-form-placeholder { font-style: italic; color: var(--ink-500); font-size: 0.74rem; }
.qc-form-footnote-marker { color: var(--ink-500); margin-left: 1px; }

/* "Paraf" — no backend concept at all (see FormColumn's docstring in
   backend/qc_schema.py); an inert visual placeholder, never a control. */
.qc-form-signature-line {
  margin-top: var(--space-2xs);
  height: 22px;
  border-bottom: 1px dashed var(--border-strong);
}

.qc-form-corrective-item { font-size: 0.74rem; text-align: left; margin-bottom: 2px; }
.qc-form-corrective-item:last-child { margin-bottom: 0; }

.qc-form-footnotes { padding: var(--space-xs) var(--space-sm) 0; }
.qc-form-footnotes p { margin: 0; color: var(--ink-500); font-size: 0.74rem; font-style: italic; }
.qc-form-signoff { padding: var(--space-2xs) var(--space-sm) var(--space-sm); color: var(--ink-500); font-size: 0.78rem; }

@media (min-width: 1025px) {
  /* Desktop: enough width that the form can breathe a little without
     losing its original column alignment — still the same table, still
     scrollable if a particular form is wider than the content area. */
  .qc-form-table { min-width: 100%; }
  .qc-form-table th,
  .qc-form-table td { padding: var(--space-2xs) var(--space-xs); }
}

/* Mobile: each reading is a stacked card. Grid-template-areas let us
   reflow the exact same DOM elements (qc-grid.js only ever queries
   [data-role="..."], never a layout-specific selector) into a completely
   different visual arrangement per breakpoint. */
/* Compact preview (Excel-first Capture redesign): shorter padding and a
   smaller value/label scale than before, so the full field list for a
   stage reads as a scannable dense list rather than a stack of big
   cards — the task's "replace the large, repetitive list of QC reading
   cards" requirement. The DOM structure and every [data-role="..."]
   element qc-grid.js wires up are unchanged; only sizing is compacted. */
.qc-row {
  display: grid;
  grid-template-areas:
    "info   info"
    "value  status"
    "edit   edit"
    "actions actions";
  grid-template-columns: 1fr auto;
  gap: var(--space-3xs) var(--space-sm);
  align-items: center;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: var(--space-xs) var(--space-sm);
}
.qc-row.state-pending { border-color: var(--warn-border); background: linear-gradient(180deg, var(--warn-soft), var(--panel) 65%); }
.qc-row.state-committed { border-color: var(--ok-border); }
/* Industrial Clarity (Phase 4): out-of-spec gets the same soft-gradient
   background state-pending already had — this is the one row state a
   supervisor most needs to spot without reading every value, so it
   shouldn't rely on a 1px border color alone. */
.qc-row.out-of-spec { border-color: var(--danger-border); background: linear-gradient(180deg, var(--danger-soft), var(--panel) 65%); }
.qc-row.out-of-spec .qc-value { color: var(--danger); font-weight: 700; }

.qc-row-info { grid-area: info; min-width: 0; }
.qc-field-label { font-weight: 600; color: var(--ink-900); font-size: 0.92rem; }
.qc-standard { color: var(--ink-500); font-size: 0.76rem; margin-top: 1px; }

/* Employee-Based Reading Traceability: the global Readings page's own
   per-row employee/session identification line — compact (single small
   line, wraps on narrow screens) so it adds identification, not visual
   weight, consistent with how .worker-assignment-banner above is styled
   for the same reason on the worker side. The name itself reuses .link-btn
   (already --accent/teal) rather than a new color token. */
.qc-row-employee {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2xs) var(--space-xs);
  margin-top: var(--space-3xs); font-size: 0.78rem;
}
.qc-row-employee-name { font-size: 0.82rem; font-weight: 600; padding: 0; min-height: 0; }
.qc-row-employee-meta { color: var(--ink-500); }
.qc-row-employee-none { color: var(--ink-500); font-style: italic; margin-top: var(--space-3xs); font-size: 0.78rem; }
.qc-row-session-id { font-variant-numeric: tabular-nums; word-break: break-all; }
/* The session ID doubles as the Readings screen's "filter to this session"
   shortcut: a .link-btn sized back down to the meta line it sits in (same
   reset .qc-row-employee-name uses), keeping the muted meta color until
   hovered so the employee name stays the line's one accent link. */
.qc-row-session-filter { font-size: inherit; padding: 0; min-height: 0; text-align: left; color: var(--ink-500); }
.qc-row-session-filter:hover { color: var(--accent); }
/* Export PDF sits at the end of the Readings filter row; without this it
   would stretch/shrink like the flexible inputs beside it. */
.admin-filter-row .qc-btn { flex: 0 0 auto; }

.qc-value {
  grid-area: value; font-variant-numeric: tabular-nums; font-size: 0.98rem; font-weight: 600;
  overflow-wrap: anywhere; color: var(--ink-900);
}
.qc-status-badge {
  grid-area: status; justify-self: end;
  font-size: 0.74rem; font-weight: 700; padding: var(--space-3xs) var(--space-xs); border-radius: var(--radius-pill);
  text-align: center; white-space: nowrap;
}
.qc-status-badge.empty { background: var(--neutral-soft); color: var(--ink-500); border: 1px solid var(--neutral-border); }
.qc-status-badge.pending { background: var(--warn-soft); color: var(--warn); border: 1px solid var(--warn-border); }
.qc-status-badge.committed { background: var(--ok-soft); color: var(--ok); border: 1px solid var(--ok-border); }
.qc-row.out-of-spec .qc-status-badge.pending,
.qc-row.out-of-spec .qc-status-badge.committed { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-border); }

/* Multi-stage pass: a THIRD, distinct row/badge state for in_spec === null
   — "no confirmed threshold yet" (backend/qc_schema.py's "unresolved" rule
   status). This must never look like either "in spec" (ok/green) or "out
   of spec" (danger/red) — those claim a verdict that wasn't given. Uses
   --info (blue), already defined for both color schemes, previously
   unused. Lower specificity than .out-of-spec is irrelevant here: a field
   is never both (false and null are mutually exclusive). */
.qc-row.rule-unresolved { border-color: var(--info-border); background: linear-gradient(180deg, var(--info-soft), var(--panel) 65%); }
.qc-row.rule-unresolved .qc-value { color: var(--info); }
.qc-row.rule-unresolved .qc-status-badge.pending,
.qc-row.rule-unresolved .qc-status-badge.committed,
.qc-status-badge.rule-unresolved { background: var(--info-soft); color: var(--info); border-color: var(--info-border); }

/* Real-Time Incremental QC Reading Population: ".is-provisional" is a
   purely visual "hearing…" preview — web/qc-grid.js's applyProvisional()
   deliberately never changes the row's real `state` (stays "empty"
   underneath), so this reuses --info (blue) rather than --warn (amber,
   reserved for a REAL pending-confirmation reading) to keep the two
   visually distinct: amber always means "a real reading exists and is
   awaiting your confirmation"; this blue/italic treatment always means
   "just a live guess, nothing recorded yet". Italic is the one additional
   cue (on top of the color and "Hearing…" badge text) that this value
   hasn't actually been recorded. */
.qc-row.is-provisional { border-color: var(--info-border); background: linear-gradient(180deg, var(--info-soft), var(--panel) 65%); }
.qc-row.is-provisional .qc-value { color: var(--info); font-style: italic; }
.qc-status-badge.provisional { background: var(--info-soft); color: var(--info); border: 1px solid var(--info-border); font-style: italic; }
.qc-form-cell-summary.is-provisional { border-color: var(--info-border); background: linear-gradient(180deg, var(--info-soft), var(--panel) 70%); }
.qc-form-cell-summary.is-provisional .qc-form-cell-value { color: var(--info); font-style: italic; }
.qc-form-cell-summary.is-provisional .qc-form-cell-status { background: var(--info-soft); color: var(--info); border-color: var(--info-border); }

.qc-edit-slot:not(:empty) { grid-area: edit; display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-2xs); }
.qc-value-input {
  flex: 1 1 120px; min-width: 100px; min-height: var(--touch-min);
  background: var(--panel); border: 1px solid var(--border-strong); color: var(--ink-900);
  border-radius: var(--radius-sm); padding: var(--space-2xs) var(--space-xs); font-size: 0.95rem;
}
/* CPD-8581: the corrective-action editor is a <textarea> sharing
   .qc-value-input's base look, widened and resizable for multi-line text
   instead of .qc-value-input's single-line sizing. */
.qc-note-input { flex-basis: 220px; resize: vertical; font-family: inherit; }

.qc-actions { grid-area: actions; display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-2xs); }
.qc-actions:empty { margin-top: 0; }

.qc-btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--panel); border: 1px solid var(--border-strong); color: var(--ink-700);
  border-radius: var(--radius-sm); padding: 0 var(--space-sm); cursor: pointer;
  font-size: 0.82rem; font-weight: 600; min-height: var(--touch-min); flex: 1 1 auto;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.qc-btn:hover { border-color: var(--accent); background: var(--accent-soft); }
.qc-btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast); } /* Industrial Premium: was hardcoded #fff, unreadable (~1.5:1) against the brighter dark-mode accent */
.qc-btn-primary:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
.qc-btn-ghost { background: transparent; }
.qc-btn-danger { color: var(--danger); border-color: var(--danger-border); }
.qc-btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }

/* Task 4 (CSS-only refinements): Overview's Export CSV / Reset filters
   buttons get dedicated accent colors. Scoped by their unique element IDs
   (#overviewExportBtn, #overviewFilterReset), NOT by redefining the shared
   .qc-btn/.qc-btn-ghost classes themselves — those classes are also used
   throughout index.html (legacy Capture) and worker.html (Worker Capture,
   e.g. its "Back to Voice Capture" and "Open Document" controls), so a
   change to the shared class would leak into both. Colors are reused from
   the Bold Accent Cards palette for visual consistency: #20A65A is the
   Completed Sessions card color, #DC5961 is the Total Anomalies card color. */
#overviewExportBtn {
  background: #20A65A; border-color: #20A65A; color: #ffffff;
}
#overviewExportBtn:hover { background: #1b8e4c; border-color: #1b8e4c; }
#overviewFilterReset {
  background: #DC5961; border-color: #DC5961; color: #ffffff;
}
#overviewFilterReset:hover { background: #c94950; border-color: #c94950; }

/* --------------------------------------------------------- state pill -- */
.state-pill {
  display: flex; align-items: center; gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-sm); border-radius: var(--radius-pill);
  background: var(--neutral-soft); border: 1px solid var(--neutral-border);
  font-size: 0.82rem; font-weight: 600; color: var(--ink-700);
  min-height: var(--touch-min);
}
.state-pill .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--state-ready); flex: none; }
.state-pill.ready { color: var(--ink-700); }
.state-pill.connecting { color: var(--state-connecting); border-color: var(--warn-border); background: var(--warn-soft); }
.state-pill.connecting .dot { background: var(--state-connecting); animation: dot-pulse 1s ease-in-out infinite; }
.state-pill.listening { color: var(--state-listening); border-color: var(--accent-border); background: var(--accent-soft); }
.state-pill.listening .dot { background: var(--state-listening); animation: dot-pulse 1.1s ease-in-out infinite; }
.state-pill.thinking { color: var(--state-thinking); border-color: var(--corrected-border); background: var(--corrected-soft); }
.state-pill.thinking .dot { background: var(--state-thinking); animation: dot-pulse 0.75s ease-in-out infinite; }
.state-pill.answering { color: var(--state-answering); border-color: #bae6fd; background: var(--info-soft); }
.state-pill.answering .dot { background: var(--state-answering); animation: dot-pulse 0.9s ease-in-out infinite; }
.state-pill.error { color: var(--danger); border-color: var(--danger-border); background: var(--danger-soft); }
.state-pill.error .dot { background: var(--danger); }

/* ============================================================ tablet === */
@media (min-width: 431px) {
  .main-content { padding: var(--space-lg) var(--space-md) var(--space-xl); }

  .stage-list { grid-template-columns: repeat(2, 1fr); }

  .orb-card { padding: var(--space-xl) var(--space-lg); }
  .orb-wrap { width: 120px; height: 120px; }

  .qc-row {
    grid-template-areas:
      "info  value  status"
      "edit  edit   edit"
      "actions actions actions";
    grid-template-columns: 1fr auto auto;
  }
  .qc-value { justify-self: end; }
}

/* ===================================================== sidebar/topbar == */
@media (max-width: 1024px) {
  .nav-backdrop[hidden] { display: none; }
  .nav-backdrop {
    display: block; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45); z-index: 40;
  }
}

/* ============================================================ desktop == */
@media (min-width: 1025px) {
  .app-shell { grid-template-columns: 296px 1fr; transition: grid-template-columns 0.15s ease; }
  /* Artifact 1 (shadcn dashboard-01) inset sidebar: on desktop the rail
     floats inside a 12px gutter as a rounded, bordered, shadowed card rather
     than sitting flush against the viewport edge. Mobile keeps the original
     fixed/off-canvas drawer mechanics (edited above) untouched. */
  .sidebar {
    position: sticky; left: auto; top: 12px; transform: none;
    height: calc(100vh - 24px); margin: 12px 0 12px 12px;
    border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
    transition: width 0.15s ease;
  }
  .mobile-nav-toggle { display: none; }

  /* Collapsible icon-rail: a 64px rail showing only icons, labels/brand text/
     group headings hidden via width:0 overflow (not display:none, so the
     real text stays in the DOM for assistive tech and the `title` attribute
     on each .nav-item still gives a native tooltip on hover). */
  .app-shell.sidebar-collapsed { grid-template-columns: 88px 1fr; }
  .app-shell.sidebar-collapsed .sidebar { width: 64px; padding-left: 10px; padding-right: 10px; }
  .app-shell.sidebar-collapsed .brand-text,
  .app-shell.sidebar-collapsed .nav-group-label,
  .app-shell.sidebar-collapsed .nav-item span,
  .app-shell.sidebar-collapsed .sidebar-footer-label,
  .app-shell.sidebar-collapsed .sidebar-user-chevron { display: none; }
  .app-shell.sidebar-collapsed .sidebar-brand { flex-direction: column; gap: 8px; justify-content: center; }
  .app-shell.sidebar-collapsed .sidebar-collapse-toggle { display: inline-flex; margin-left: 0; transform: rotate(180deg); }
  .app-shell.sidebar-collapsed .nav-item { justify-content: center; gap: 0; }
  .app-shell.sidebar-collapsed .nav-item.active::before { left: -10px; }
  .app-shell.sidebar-collapsed .sidebar-user-trigger { justify-content: center; }
  .app-shell.sidebar-collapsed .sidebar-user-avatar { margin: 0; }
  .app-shell.sidebar-collapsed .sidebar-user-popover { left: 10px; right: 10px; }
  .app-shell.sidebar-collapsed .sidebar-search { display: none; }

  .topbar { /* Phase 1: depth shadow added */ padding: var(--space-sm) var(--space-xl); }
  .main-content { padding: var(--space-xl) var(--space-lg) var(--space-xl); }

  .stage-list { grid-template-columns: repeat(3, 1fr); }

  .voice-stage-body {
    display: grid;
    grid-template-columns: 360px 1fr;
    align-items: start;
    gap: var(--space-xl);
  }
  .voice-workspace { position: sticky; top: calc(64px + var(--space-lg)); }
  .voice-stage-main { display: flex; flex-direction: column; gap: var(--space-lg); min-width: 0; }

  .qc-grid-section { padding: var(--space-lg); }

  .qc-row {
    grid-template-areas:
      "info value status actions"
      "edit edit   edit    edit";
    grid-template-columns: 1.6fr 0.8fr 0.9fr 1.1fr;
    gap: var(--space-md);
  }
  .qc-edit-slot:not(:empty) { margin-top: var(--space-2xs); }
  .qc-actions { justify-content: flex-end; margin-top: 0; }
  .qc-actions .qc-btn,
  .qc-edit-slot .qc-btn { flex: 0 0 auto; }
}

@media print {
  .sidebar, .topbar, .notice-region, .nav-backdrop { display: none; }
}

/* ============================================================================
   Excel-into-Capture integration (new): workbook-manage button, the generic
   modal shell (no .modal/.dialog/.drawer existed anywhere before this), the
   Live Reading Preview card, and the compact Recent Readings list. All reuse
   the existing design tokens (--panel/--border/--radius-*/--shadow-*/
   --space-*/--ink-*/status colors) rather than introducing new ones, so this
   reads as the same visual system as every other card on the page.
   ============================================================================ */

.workbook-panel-head-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); }
.workbook-panel-head-row h2 { margin: 0; }
.workbook-panel-head-row .link-btn { padding: 0; min-height: 0; font-size: 0.8rem; }

/* ---- generic modal shell ------------------------------------------------ */
body.modal-open { overflow: hidden; }

.modal-overlay {
  position: fixed; inset: 0; z-index: 100;
  background: rgba(15, 23, 42, 0.55);
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-md);
  animation: dialog-overlay-in 0.15s ease-out;
}
.modal-overlay[hidden] { display: none; }

/* Component Integration pass: shadcn's Dialog enters with a brief fade +
   scale (never just appearing instantly), which is what actually reads as
   a deliberate, built component rather than a plain toggled div — the
   structure/shell here (overlay, header, close button, scrollable body) was
   already right, so only the entrance motion and radius were missing. */
@keyframes dialog-overlay-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes dialog-panel-in { from { opacity: 0; transform: scale(0.97) translateY(6px); } to { opacity: 1; transform: scale(1) translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .modal-overlay, .modal { animation: none; }
}

.modal {
  background: var(--panel-elevated); border: 1px solid var(--border); border-radius: var(--radius-lg);
  /* Industrial Clarity: --shadow-lg (a real token now, see :root) instead of
     --shadow-md -- a modal sits a layer above the page's own cards, which
     already use --shadow-sm/--shadow-md, so it should read as clearly more
     elevated than either. Industrial Premium (Task D): background upgraded
     from --panel to the new --panel-elevated tier for the same reason —
     identical to --panel in light mode, but a real step lighter than the
     page's cards in dark mode, reinforcing the "above everything else"
     shadow with an actual surface-color difference too. */
  box-shadow: var(--shadow-lg);
  width: min(640px, 100%);
  max-height: calc(100vh - var(--space-xl));
  display: flex; flex-direction: column;
  overflow: hidden;
  animation: dialog-panel-in 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--border);
  flex: none;
}
.modal-head h2 { margin: 0; font-size: 1.05rem; }
.modal-close {
  background: none; border: none; color: var(--ink-500); cursor: pointer;
  font-size: 1.4rem; line-height: 1; padding: var(--space-2xs); min-width: var(--touch-min); min-height: var(--touch-min);
  display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius-sm);
}
.modal-close:hover { background: var(--neutral-soft); color: var(--ink-900); }
.modal-body {
  padding: var(--space-md);
  overflow-y: auto;
}
.modal-body .card:first-child { margin-top: 0; }

.capture-excel-use-card {
  margin-top: var(--space-sm); padding: var(--space-sm);
  background: var(--accent-soft); border: 1px solid var(--accent-border); border-radius: var(--radius-md);
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-sm);
}
.capture-excel-use-card p { margin: 0; font-size: 0.86rem; color: var(--ink-900); }

/* ---- Live Reading Preview ------------------------------------------------ */
.live-reading-card {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-md); box-shadow: var(--shadow-sm);
  width: 100%;
  /* StageShow visual-polish pass: same left-accent-on-status convention the
     admin side already uses for stage cards (.admin-stage-card:has(.badge-ok),
     see ~line 2628) and KPI tiles, extended here via :has() against this
     card's own .live-reading-status-badge -- no JS/HTML change, and the
     Original Document card beside it already carries a similar (static)
     teal accent, so this brings the one plain-white card in the worker
     capture flow in line with everything around it. Transparent/neutral
     until a reading actually exists (is-pending/-confirmed/-rejected), so
     an empty "waiting" card stays neutral, never implying a result that
     hasn't happened yet. */
  border-left: 3px solid transparent;
  transition: border-left-color 0.15s ease;
}
.live-reading-card:has(.live-reading-status-badge.is-pending) { border-left-color: var(--warn); }
.live-reading-card:has(.live-reading-status-badge.is-confirmed) { border-left-color: var(--ok); }
.live-reading-card:has(.live-reading-status-badge.is-rejected) { border-left-color: var(--danger); }
.live-reading-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); margin-bottom: var(--space-xs); }
.live-reading-head h2 { margin: 0; font-size: 1rem; }

/* Industrial Clarity (Phase 3): same uppercase/letter-spaced "state chip"
   treatment as .worker-session-status above, for the same reason — this
   is the clearest, most-glanced-at status in the whole capture flow (is
   what I just said pending, confirmed, or rejected?) and should read as a
   state, not a label. Colors/variants unchanged. */
.live-reading-status-badge {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
  padding: var(--space-3xs) var(--space-xs); border-radius: var(--radius-pill);
  white-space: nowrap; background: var(--neutral-soft); color: var(--ink-500); border: 1px solid var(--neutral-border);
}
.live-reading-status-badge.is-pending { background: var(--warn-soft); color: var(--warn); border-color: var(--warn-border); }
.live-reading-status-badge.is-confirmed { background: var(--ok-soft); color: var(--ok); border-color: var(--ok-border); }
.live-reading-status-badge.is-rejected { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-border); }

.live-reading-empty { color: var(--ink-500); font-size: 0.88rem; margin: 0; }

.live-reading-body { display: flex; flex-direction: column; gap: var(--space-2xs); }
.live-reading-field { font-weight: 600; color: var(--ink-900); font-size: 0.95rem; }
.live-reading-value-row { display: flex; align-items: center; gap: var(--space-xs); flex-wrap: wrap; }
.live-reading-value { font-variant-numeric: tabular-nums; font-size: 1.4rem; font-weight: 700; color: var(--ink-900); }
.live-reading-spec-badge {
  font-size: 0.74rem; font-weight: 700; padding: var(--space-3xs) var(--space-xs); border-radius: var(--radius-pill);
  white-space: nowrap;
}
.live-reading-spec-badge:empty { display: none; }
.live-reading-spec-badge.out-of-spec { background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger-border); }
.live-reading-spec-badge.rule-unresolved { background: var(--info-soft); color: var(--info); border: 1px solid var(--info-border); }

.live-reading-excel {
  font-size: 0.82rem; color: var(--ink-700); margin: 0;
  background: var(--neutral-soft); border: 1px solid var(--neutral-border); border-radius: var(--radius-sm);
  padding: var(--space-2xs) var(--space-xs);
}
.live-reading-reject-reason { color: var(--danger); font-size: 0.84rem; margin: 0; }

.live-reading-actions { display: flex; gap: var(--space-xs); flex-wrap: wrap; margin-top: var(--space-2xs); }

/* ---- Recent Readings ------------------------------------------------------ */
.recent-readings-card {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--space-md); box-shadow: var(--shadow-sm);
}
.recent-readings-card h2 { margin: 0 0 var(--space-xs); font-size: 0.95rem; }
.recent-readings-empty { color: var(--ink-500); font-size: 0.84rem; margin: 0; }

/* Component Integration pass: Recent Readings now renders the shared
   .data-table component (see worker-app.js's renderRecentReadings() and the
   .data-table rule block near Admin Overview's Sessions table) instead of
   its own bespoke flex-row list — the empty/#recentReadingsEmpty state and
   the card shell itself are unchanged. */
.recent-readings-table tbody tr.is-rejected td { background: var(--danger-soft); }
.recent-readings-table .data-table-ts { font-variant-numeric: tabular-nums; }
/* This table always lives in a narrow ~300-360px side column (even on a
   1440px desktop — see .worker-document-side), so it uses the stacked
   label/value layout unconditionally rather than the viewport-width
   @media (max-width: 559px) breakpoint .data-table normally switches on,
   which would never fire here regardless of how cramped the column gets. */
.recent-readings-table thead { display: none; }
.recent-readings-table, .recent-readings-table tbody, .recent-readings-table tr, .recent-readings-table td { display: block; width: 100%; }
.recent-readings-table tbody tr { border-bottom: 1px solid var(--border); padding: var(--space-3xs) 0; }
.recent-readings-table tbody tr:last-child { border-bottom: none; }
.recent-readings-table tbody td { border-bottom: none; padding: 2px var(--space-3xs); display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); font-size: 0.82rem; }
.recent-readings-table tbody td[data-label]::before {
  content: attr(data-label); font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-500); flex: none;
}

/* Transient highlight (web/qc-grid.js's _flashRow) so the grid row a reading
   just landed on — via voice OR a manual edit — is visually distinct from
   the rest of the grid for a couple of seconds (task §3: "make the current
   reading visually distinct from previous readings"), independent of (and
   in addition to) the dedicated Live Reading Preview card above. */
@keyframes qc-row-flash {
  0% { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent-border); }
  100% { background: transparent; box-shadow: none; }
}
.qc-row.just-updated { animation: qc-row-flash 2.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .qc-row.just-updated { animation: none; outline: 2px solid var(--accent-border); outline-offset: 2px; }
}

@media (max-width: 1024px) {
  .modal { max-height: calc(100vh - var(--space-lg)); }
}

/* ===========================================================================
   Worker Mobile Capture Interface (redesign §4, web/worker.html /
   web/worker-app.js) — a standalone, single-column, mobile-first page with
   no sidebar at all (unlike .app-shell's grid, which reserves a sidebar
   column from the 1025px breakpoint up). Deliberately minimal: everything
   else this page uses (.orb-*, .icon-btn*, .error-banner, .caption-*,
   .live-reading-*, .qc-*, .recent-reading-*, .state-pill, .notice-region,
   .context-*) is already shared, unscoped component CSS from the sections
   above and needs nothing new here.
=========================================================================== */
/* Enterprise Admin Dashboard implementation (Artifact 1), §5 "Worker
   Capture": the worker page previously had its own separate "Industrial
   Premium" teal-branded identity (a dark teal gradient header/orb card,
   distinct from the admin app's chrome) — striking on its own, but it no
   longer reads as part of the SAME product once the admin dashboard became
   a light, enterprise/shadcn surface. This pass retires the dark-gradient
   surfaces below (.worker-topbar, .worker-shell .orb-card and friends) in
   favor of the exact same light panel/border/shadow chrome the admin side
   uses, so a worker and an admin looking at their phones side by side see
   one coherent design system — while every operational element (voice
   states, mic controls, Live Reading, Live Transcription, the original
   document, the readings grid) is completely untouched. --worker-teal-soft/
   -soft-border remain: they're just light teal tints, already consistent
   with the enterprise accent, used for a couple of small badge-style
   accents further down. */
.worker-shell {
  --worker-teal-bright: #14B8A6;
  --worker-teal-soft: #E0F7F5;
  --worker-teal-soft-border: #99E6DD;
}
.worker-shell {
  min-height: 100vh; display: flex; flex-direction: column;
  background: var(--surface);
}
/* Phase 1: Increase base font-size for the worker page only — the processing
   floor demands larger text for legibility at arm's length and under gloves.
   16px is the floor, not 15px. All relative (rem/em) child sizes scale with it
   automatically. Admin and index.html are untouched (body rule stays 15px). */
.worker-shell body,
.worker-shell,
.worker-shell .qc-field-label { font-size: 16px; }
.worker-shell .qc-field-label { font-size: 1rem; }
/* Stage-Specific QC Templates pass (redesign §6): this used to be a flat
   `max-width: 640px` with NO responsive override anywhere in this file —
   so even on a tablet or a desktop browser window, worker.html's content
   (including the two-column `.voice-stage-body` grid and the desktop
   `.qc-row` layout both defined above) was squeezed into a phone-width
   column, which is exactly why the QC reading cards read as "too narrow,
   causing labels/text to wrap awkwardly" (the task's own description of
   the problem). Phone stays single-column/640px (unchanged default,
   correct for this page's primary use case); tablet/desktop widen to the
   SAME max-width the legacy Capture screen's `.main-content` already uses,
   so the existing `.voice-stage-body`/`.qc-row` breakpoints above (431px/
   1025px) have real room to lay out multi-column instead of collapsing
   back to their own narrowest form inside a too-narrow parent. */
/* ---- Worker header (redesign §4) ------------------------------------------
   .worker-topbar is a modifier combined with the shared .topbar class in
   worker.html's markup only (`<header class="topbar worker-topbar">`) — it
   exists nowhere else. Enterprise pass: this now matches the admin
   dashboard's own topbar almost exactly (light panel, bottom hairline,
   blur) rather than carrying its own separate dark-gradient identity — the
   one visible difference is the brand mark sitting directly in the topbar
   (this page has no sidebar to put it in). */
.worker-topbar {
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(6px);
  padding: var(--space-sm) var(--space-md);
}
.worker-topbar .topbar-route-title { color: var(--ink-900); font-weight: 700; font-size: 1.15rem; letter-spacing: -0.01em; }
.worker-topbar-subtitle { color: var(--ink-500); font-size: 0.78rem; margin-top: 1px; font-weight: 500; }
/* Same 32px teal chip as the admin sidebar's .brand-mark (~line 358) — one
   brand mark style used everywhere this app shows its own logo. */
.worker-brand-mark {
  width: 36px; height: 36px; border-radius: var(--radius-sm); flex: none;
  background: var(--accent); color: var(--accent-contrast);
  font-weight: 700; font-size: 0.8rem; letter-spacing: 0.02em;
  display: flex; align-items: center; justify-content: center;
  box-shadow: none;
}
.worker-topbar .topbar-left { gap: var(--space-sm); }
/* The status pill's per-state colors (.state-pill.listening, etc., defined
   earlier in this file) already pair a soft tint with a matching border —
   the exact same convention as the admin dashboard's badges — so on this
   now-light topbar they need no override at all; removing the old
   frosted-white chrome (only needed to stay visible against the dark
   gradient header this page no longer has) lets each state read in its own
   real color here too. */
/* A small decorative audio-activity glyph (brief §4: "subtle waveform or
   audio activity visual") — a purely cosmetic sibling of #stateLabel, never
   touched by worker-app.js (which only sets statePill.className and
   stateLabel.textContent, never anything about this element's presence),
   so it's safe to add without any JS change. Animates continuously; this
   is deliberately not tied to the real connection state (that's already
   communicated by the label text and the dot) — it is ambient texture, not
   a second status indicator. */
.worker-waveform {
  display: inline-flex; align-items: flex-end; gap: 2px; height: 12px; margin-left: 2px;
}
.worker-waveform span {
  width: 2px; background: currentColor; border-radius: 1px;
  animation: worker-waveform-bounce 1s ease-in-out infinite;
}
.worker-waveform span:nth-child(1) { height: 40%; animation-delay: 0s; }
.worker-waveform span:nth-child(2) { height: 100%; animation-delay: 0.15s; }
.worker-waveform span:nth-child(3) { height: 65%; animation-delay: 0.3s; }
.worker-waveform span:nth-child(4) { height: 85%; animation-delay: 0.45s; }
@keyframes worker-waveform-bounce { 0%, 100% { transform: scaleY(0.4); } 50% { transform: scaleY(1); } }
@media (prefers-reduced-motion: reduce) {
  .worker-waveform span { animation: none; }
}

.worker-main { max-width: 640px; }
@media (min-width: 431px) {
  .worker-main { max-width: 900px; }
}
@media (min-width: 1025px) {
  .worker-main { max-width: 1180px; }
}
/* Worker Capture Layout Redesign: management feedback explicitly called
   out "too much unused space on left/right" and asked this page be
   "optimized for wide screens" using "maximum practical screen space" —
   .worker-main previously capped at the same 1180px as every other
   page's .main-content no matter how wide the viewport got, which left a
   growing, unused margin on genuinely wide displays. Worker-only
   (.main-content's own 1180px cap was, at the time this comment was
   written, shared unmodified with admin.html/index.html; the Admin
   Dashboard layout-width correction pass later gave admin.html its own
   `.main-content.admin-main` override near the base `.main-content` rule
   above — see that rule's comment — while index.html's copy is still the
   original untouched 1180px cap). 1600px/1800px are generous but still
   leave a comfortable side gutter rather than running the document
   edge-to-edge on an ultra-wide monitor. */
@media (min-width: 1280px) {
  .worker-main { max-width: 1440px; }
}
@media (min-width: 1440px) {
  .worker-main { max-width: 1600px; }
}

.worker-status-card {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm); padding: var(--space-xl) var(--space-md); margin: var(--space-lg) auto 0;
  max-width: 480px; text-align: center;
}
.worker-status-card h2 { margin: 0 0 var(--space-2xs); font-size: 1.05rem; }
.worker-status-card p { margin: 0; color: var(--ink-500); line-height: 1.5; }
.worker-status-error { border-color: var(--danger-border); background: var(--danger-soft); }
.worker-status-error h2 { color: var(--danger); }
.worker-status-error p { color: var(--ink-700); }

/* Industrial Clarity (Phase 3): uppercase + letter-spacing marks this as a
   STATE chip (binary active/ended) at a glance, distinct from the two
   numeric counters beside it (which read as counts, not states) — the
   same state-vs-count distinction the admin dashboard's badges already
   draw. Sizing/colors/the is-active::before dot are unchanged. */
.worker-session-status {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  white-space: nowrap; padding: 2px 10px; border-radius: var(--radius-pill);
  border: 1px solid var(--ok-border); background: var(--ok-soft); color: var(--ok); flex: none;
}
.worker-session-status.is-active::before { content: '●'; margin-right: 4px; font-size: 0.6rem; }

/* ---- Template header (redesign §3) ---------------------------------------
   The exact official form this session's readings belong to — title, doc
   code, revision, effective date — shown once at the top of the capture
   screen so the worker always knows which template/version they're
   filling, never has to guess.
   Industrial Clarity (Phase 3): promoted from a bare bottom-border row to
   a compact "letterhead" block — its own panel background, border and
   radius — so it reads as a distinct, authoritative header rather than
   blending into the page background above the status pills. Still not a
   full .card (no shadow, tighter padding): it's reference context the
   worker glances at once, not an interactive surface competing with the
   orb/live-reading card below it. */
.template-header {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2xs) var(--space-sm);
  margin-bottom: var(--space-xs); padding: var(--space-xs) var(--space-sm);
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-md);
  /* Industrial Premium: a subtle teal accent on the left (brief §5) so this
     reads as a deliberate "letterhead" surface, not a flat strip blending
     into the page. Same convention as the admin Settings page's
     .card-info left accent. */
  border-left: 3px solid var(--worker-teal-bright, var(--accent));
  box-shadow: var(--shadow-sm);
}
.template-header-title { font-weight: 700; font-size: 1.02rem; color: var(--ink-900); letter-spacing: -0.005em; }
.template-header-meta {
  font-size: 0.78rem; font-weight: 600; color: var(--ink-500); font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

/* ---- Numbered Recordings (redesign §5/§6) ---------------------------------
   Industrial Clarity (Phase 3): tabular-nums so "Recording 9" → "Recording
   10" doesn't reflow its pill width mid-session, plus a small leading dot
   (CSS-only ::before, no markup change) so this reads as a live counter at
   a glance, the same dot language .state-pill already uses elsewhere on
   this page. */
.worker-recording-counter {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.78rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap;
  padding: 2px 10px; border-radius: var(--radius-pill);
  border: 1px solid var(--accent-border); background: var(--accent-soft); color: var(--accent-dark);
}
.worker-recording-counter::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }

/* ---- Automatic Worker Voice Recording (§4/§5) -- "Readings: N", a finer-
   grained counter alongside the existing "Recording N" above: each
   individually-confirmed field, vs. a full reviewed-and-submitted set. ---- */
.worker-reading-counter {
  font-size: 0.78rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap;
  padding: 2px 10px; border-radius: var(--radius-pill);
  /* Industrial Premium: a soft-teal tint (was a plain neutral white/gray
     pill) so this reads as part of the same badge family as the recording
     counter and active-session status beside it, instead of looking
     unstyled by comparison. */
  border: 1px solid var(--worker-teal-soft-border, var(--border)); background: var(--worker-teal-soft, var(--panel)); color: var(--ink-700);
}

/* §3: a clear, explicit "Start Capture" control alongside the existing orb
   tap -- same action, just more discoverable. Hidden once a voice session
   is connected (worker-app.js toggles `hidden`); the orb and Pause/Resume/
   Stop controls take over from there.
   Industrial Clarity (Phase 3): a dedicated shadow (scoped to this one
   selector, never the shared .qc-btn-primary rule it's combined with in
   worker.html's class list — that rule is also used on the admin
   dashboard and must not pick up a worker-only treatment) marks this as
   THE primary call to action on first load, when nothing else on the page
   is yet interactive. */
.start-capture-btn {
  margin-top: var(--space-sm);
  /* Industrial Premium: was a blue halo (rgba(37,99,235,...)), left over
     from before the light theme's --accent itself became teal — this
     button already inherits a teal fill from --accent/.qc-btn-primary
     (Task D), so its own glow should match rather than clash. */
  box-shadow: 0 4px 14px rgba(8, 127, 116, 0.35);
}
.start-capture-btn:hover:not(:disabled) { box-shadow: 0 6px 18px rgba(8, 127, 116, 0.42); }

/* ---- Voice capture card (redesign §7) ------------------------------------
   The voice capture card is now the shared, plain-white .orb-card (its own
   base rule above already gives it --shadow-lg to outrank the cards below
   it) — the same chrome every other card on this page and on the admin
   dashboard uses. The orb itself (.orb-wrap) keeps its own fixed dark
   backdrop (#0b1220) regardless of the card around it — that's a deliberate
   focal/"stage light" treatment for the one control the worker looks at
   most, not card chrome — and its canvas animation, hint text, and
   Capturing/Stop icon buttons all already use their normal (shared, light-
   card) colors with no override needed here. */

/* ---- Live Reading card (redesign §8) ---------------------------------
   Scoped to .worker-shell — .live-reading-card is shared with index.html.
   The badge/value/status colors (pending=amber/confirmed=green/
   rejected=red) are already correctly semantic and untouched; this only
   raises the card's own elevation so it reads as the primary confirm/
   correct surface the brief asks for, sitting visually above the plain
   qc-grid-section/recent-readings-card beneath it. */
.worker-shell .live-reading-card {
  /* Enterprise pass: plain var(--border) now (was a light-teal-tinted
     border, a leftover accent from the old teal-branded page) — matches
     every other card's border on this page and on the admin dashboard.
     box-shadow stays --shadow-lg so this still outranks the plainer cards
     beneath it, per the StageShow comment this replaces. */
  box-shadow: var(--shadow-lg);
  /* StageShow visual-polish pass: this rule needs its own border-left-color
     (even though it no longer touches the other three sides) because the
     new status-driven left accent's neutral/transparent default
     (.live-reading-card { border-left: 3px solid transparent }, specificity
     0-1-0) would otherwise lose to THIS rule's higher specificity (0-2-0)
     if this rule set `border-color` as a shorthand — setting only
     border-left-color here keeps the other three sides on the plain base
     border while still being explicit about the default left edge; the
     :has() rules that light this edge up for is-pending/-confirmed/
     -rejected are more specific again (0-3-0) and still win when a reading
     actually has a status. */
  border-left-color: transparent;
}

/* Worker Capture Layout Redesign: these three cards now live in the
   narrower .worker-document-side column beside the document instead of a
   full-width sidebar, so their padding/heading size are trimmed slightly
   to suit that column — purely a tightened-spacing tweak; every id and all
   confirm/edit/note logic inside them is completely untouched. Scoped by
   the .worker-document-side ancestor, which exists only in worker.html, so
   this can never reach index.html's own (unrelated) use of these classes. */
.worker-document-side .live-reading-card,
.worker-document-side .caption-panel,
.worker-document-side .recent-readings-card {
  padding: var(--space-sm) var(--space-md);
}
.worker-document-side .live-reading-head h2,
.worker-document-side .recent-readings-card h2 { font-size: 0.92rem; }
.worker-document-side .live-reading-value { font-size: 1.2rem; }

/* ---- GRI response / caption panel (redesign §9) -----------------------
   Scoped to .worker-shell — .caption-panel is shared with index.html. Was
   a flat var(--ink-100) gray block; now a soft blue "information surface"
   with a small drawn info-glyph (CSS ::before, matching the dot-based icon
   language already used elsewhere in this file for .notice/.worker-
   recording-counter — no markup change, so #captionEmpty/#captionContent/
   #captionRole/#captionText, all read and written directly by worker-
   app.js, are completely untouched). */
.worker-shell .caption-panel {
  background: var(--info-soft); border: 1px solid var(--info-border);
}
/* Worker Capture layout refinement: tighten the session-info spacing above
   the voice strip/Live Reading so they sit higher on the page. Scoped to
   .worker-shell since both .active-stage-banner and .context-strip are
   shared with index.html's legacy Capture screen, which keeps its own
   spacing unchanged. */
.worker-shell .active-stage-banner { margin-bottom: var(--space-xs); }
.worker-shell .context-strip:not([hidden]) { margin-bottom: var(--space-xs); }
.worker-shell .caption-empty {
  display: flex; align-items: flex-start; gap: var(--space-2xs); color: var(--ink-700);
}
.worker-shell .caption-empty::before {
  content: 'i'; flex: none; width: 16px; height: 16px; border-radius: 50%;
  background: var(--info); color: #fff; font-size: 0.7rem; font-weight: 800; font-style: normal;
  display: flex; align-items: center; justify-content: center; line-height: 1; margin-top: 1px;
}
/* .caption-role.you already resolves to var(--accent-dark), which is teal
   on this page since Task D — no override needed here. */
/* Enterprise pass: plain var(--danger-soft)/-border/var(--danger) now (was
   a translucent dark-red "glass" wash + pale pink text, tuned to stay
   legible on the old dark gradient card) — the exact same danger-surface
   convention every badge/icon-btn/alert elsewhere in the app already uses,
   now that this banner sits on a plain white card. */
.worker-shell .error-banner:not([hidden]) {
  background: var(--danger-soft); border-color: var(--danger-border); color: var(--danger);
}

/* Audit and Complete the Hands-Free Worker Voice Capture Workflow, step 2
   (requirement 2, "the microphone state is clearly visible"): a verified,
   real gap found via Playwright, not assumed -- toggling `hidden` on this
   button (worker-app.js's startOrIgnore()/the 'connected' handler) left it
   fully VISIBLE on screen (getComputedStyle().display: 'flex', not 'none')
   the entire time a session was actively connected, because .qc-btn's own
   unconditional `display: inline-flex` (an author-origin rule) beats the
   browser's UA-stylesheet default `[hidden] { display: none }` at equal
   specificity -- the exact same cascade-origin gap already fixed elsewhere
   in this file for .worker-pdf-preview, .qc-field-detail-row, and
   .modal-overlay (see each of those rules' own comments); this button's own
   base rule above was simply never given the same explicit override. A
   worker mid-session, already hands-free and "Listening…", would have kept
   seeing a live, clickable "Start Capture" button the whole time -- directly
   undermining a clear, unambiguous microphone-state display, even though
   tapping it was always harmless (startOrIgnore()'s `connected`/`connecting`
   guards never let it open a second connection). */
.start-capture-btn[hidden] { display: none; }

.qc-grid-footer { margin-top: var(--space-sm); }
.qc-btn-large { width: 100%; min-height: 48px; font-size: 0.92rem; }

/* :not([hidden]) guard — same reason as .context-strip's above: without
   it this unconditional `display: flex` (an author rule) would beat the
   UA default `[hidden]{display:none}`, leaving the overlay visibly
   covering the page (and intercepting clicks) even while `hidden`. */
.recording-review-overlay:not([hidden]) {
  position: fixed; inset: 0; z-index: 40; background: rgba(10, 14, 20, 0.55);
  display: flex; align-items: flex-end; justify-content: center; padding: 0;
}
.recording-review-panel {
  background: var(--panel); border-top: 1px solid var(--border); border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  /* Explicit, directional (cast upward from the sheet's own top edge) --
     NOT var(--shadow-lg), which is now a real, generic, omnidirectional
     token (see :root) and would look wrong on a bottom sheet. This was
     previously written as var(--shadow-lg, 0 -4px 24px rgba(0,0,0,0.25))
     when --shadow-lg had no real declaration anywhere, so the fallback
     value (this exact shadow) was always what actually rendered; written
     out directly now that --shadow-lg means something else. */
  box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.25);
  width: 100%; max-width: 640px;
  max-height: 88vh; overflow-y: auto; padding: var(--space-md);
  position: relative;
}
/* Industrial Clarity (Phase 3): a plain CSS grab-handle bar — the standard
   affordance marking "this sheet can be dismissed/is a sheet, not a full
   page" on a touch device. Decorative only (aria-hidden via :: pseudo, not
   a real element, so no new markup or tab stop); the actual close/confirm
   controls are still the existing real buttons below. */
.recording-review-panel::before {
  content: ''; display: block; width: 40px; height: 4px; border-radius: var(--radius-pill);
  background: var(--border-strong); margin: 0 auto var(--space-sm);
}
.recording-review-panel h2 { margin: 0 0 var(--space-2xs); font-size: 1.1rem; font-weight: 700; letter-spacing: -0.005em; }
.recording-review-hint { margin: 0 0 var(--space-sm); color: var(--ink-500); font-size: 0.84rem; }

.recording-review-list { display: flex; flex-direction: column; gap: var(--space-2xs); margin-bottom: var(--space-sm); }
.recording-review-row {
  display: flex; align-items: center; gap: var(--space-xs); padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--neutral-soft);
}
.recording-review-row.state-empty { border-color: var(--danger-border); background: var(--danger-soft); }
.recording-review-row.state-pending { border-color: var(--warn-border); background: var(--warn-soft); }
.recording-review-row.state-committed { border-color: var(--ok-border); background: var(--ok-soft); }
.recording-review-row-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.recording-review-row-label { font-weight: 600; color: var(--ink-900); font-size: 0.88rem; }
.recording-review-row-status { font-size: 0.76rem; color: var(--ink-500); }
.recording-review-row-value { font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }

.recording-review-blocked {
  color: var(--danger); font-size: 0.84rem; margin: 0 0 var(--space-sm);
  background: var(--danger-soft); border: 1px solid var(--danger-border); border-radius: var(--radius-sm);
  padding: var(--space-2xs) var(--space-xs);
}

.recording-review-actions { display: flex; gap: var(--space-xs); }
.recording-review-actions .qc-btn { flex: 1 1 0; }
.recording-review-actions .qc-btn:disabled { opacity: 0.5; cursor: not-allowed; }

@media (min-width: 640px) {
  .recording-review-overlay { align-items: center; }
  .recording-review-panel { border-radius: var(--radius-lg); }
}

/* ===========================================================================
   Admin Dashboard (redesign §3, web/admin.html / web/admin-app.js) — reuses
   .app-shell, .sidebar, .topbar, .card, .badge-*, .qc-btn*, .qc-row and
   .empty-state wholesale (same dark-navy nav, same component language as
   the legacy app). Only the pieces below are genuinely new.
=========================================================================== */
.admin-text-input {
  width: 100%; min-height: var(--touch-min); padding: 0 var(--space-xs); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); font-size: 0.9rem; background: var(--panel); color: var(--ink-900);
}

/* ---- Admin login — a dedicated, full-viewport dark screen, not just
   another .card inside the normal sidebar/topbar layout. Visual direction
   (per the task's reference screenshot): a premium midnight-navy background
   with layered blue/teal ambient glows and an understated dotted pattern, a
   glass card with a thin blue-to-cyan gradient border, a larger brand
   mark/heading, Google's own button restyled to the solid white pill with
   the official multi-color G (see web/admin-app.js's renderButton() call —
   only its theme/shape/text options changed, never the init/callback
   wiring), an "OR" divider, and a small security footer.

   --login-blue/--login-blue-dark/--login-teal are LOCAL custom properties
   (declared on .admin-login-screen below) rather than reuses of the shared
   --accent/--accent-dark tokens: those tokens flip to teal under this
   file's `@media (prefers-color-scheme: dark)` block (see :root), but this
   screen is always dark regardless of the visitor's OS theme, so its
   blue+teal palette must never depend on that media query.

   Scoped entirely to its own classes so none of this touches .qc-btn*,
   .admin-text-input, or any other shared component used elsewhere in this
   dashboard. -------- */
.admin-login-screen {
  --login-blue: #3b82f6;
  --login-blue-dark: #1e3a8a;
  --login-teal: #2dd4bf;
}
/* :not([hidden]) — same guard as .context-strip/.error-banner above: the
   screen is shown/hidden via the native `hidden` attribute (navigateScreen()
   in web/admin-app.js), and without this guard this unconditional
   position:fixed/display:flex would beat the UA default [hidden]{display:none}
   (equal specificity, author origin wins), leaving the login overlay covering
   the whole viewport — and intercepting clicks — even while "hidden" and the
   dashboard is showing underneath it. */
.admin-login-screen:not([hidden]) {
  position: fixed; inset: 0; z-index: 500;
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-md); overflow-y: auto; overflow-x: hidden;
  background:
    radial-gradient(900px circle at 50% -8%, rgba(59, 130, 246, 0.32), transparent 62%),
    radial-gradient(640px circle at 90% 6%, rgba(45, 212, 191, 0.15), transparent 60%),
    radial-gradient(700px circle at 8% 100%, rgba(29, 78, 216, 0.2), transparent 60%),
    linear-gradient(180deg, #0b1324 0%, #0a0f1f 55%, #070b16 100%);
}
/* Understated decorative pattern: a faint dotted grid that only shows
   through near the bottom-left and top-right corners (masked out
   elsewhere so it never competes with the card), plus one thin diagonal
   teal sheen. Both are ::before/::after on the screen itself — never real
   DOM in web/admin.html — so every admin-login-* screen (boot-loading,
   setup-error, google-login, login, create-admin) gets the same backdrop
   with zero markup duplication, and nothing here is exposed to assistive
   tech (generated content isn't part of the accessibility tree). */
.admin-login-screen:not([hidden])::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image: radial-gradient(rgba(148, 197, 255, 0.18) 1px, transparent 1.4px);
  background-size: 26px 26px;
  -webkit-mask-image:
    radial-gradient(640px circle at 6% 104%, #000 0%, transparent 70%),
    radial-gradient(640px circle at 100% -4%, #000 0%, transparent 70%);
  mask-image:
    radial-gradient(640px circle at 6% 104%, #000 0%, transparent 70%),
    radial-gradient(640px circle at 100% -4%, #000 0%, transparent 70%);
}
.admin-login-screen:not([hidden])::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(120deg, transparent 42%, rgba(45, 212, 191, 0.07) 50%, transparent 58%);
}
.admin-login-card {
  position: relative; z-index: 1;
  width: 100%; max-width: 400px;
  border: 1px solid transparent; border-radius: 22px;
  background:
    linear-gradient(rgba(15, 23, 42, 0.78), rgba(15, 23, 42, 0.78)) padding-box,
    linear-gradient(135deg, rgba(96, 165, 250, 0.65), rgba(45, 212, 191, 0.45)) border-box;
  box-shadow: 0 24px 64px rgba(2, 6, 23, 0.55);
  backdrop-filter: blur(10px);
  padding: var(--space-xl) var(--space-lg);
  color: #e2e8f0;
}
.admin-login-brand { text-align: center; margin-bottom: var(--space-lg); }
.admin-login-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 68px; height: 68px; border-radius: 20px;
  background: linear-gradient(135deg, var(--login-blue), var(--login-teal));
  color: #fff; font-weight: 800; font-size: 1.1rem; letter-spacing: 0.02em;
  margin-bottom: var(--space-md); box-shadow: 0 10px 28px rgba(29, 78, 216, 0.45);
}
.admin-login-brand h1 { font-size: 1.5rem; font-weight: 800; margin: 0 0 8px; color: #fff; letter-spacing: -0.01em; }
.admin-login-subtitle { font-size: 0.92rem; color: #93a3b8; margin: 0; line-height: 1.5; }
.admin-login-subtitle-accent { color: #7dd3fc; font-weight: 600; }

.admin-login-divider {
  display: flex; align-items: center; gap: var(--space-sm);
  margin: var(--space-md) 0; color: #64748b; font-size: 0.7rem;
  font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
}
.admin-login-divider::before, .admin-login-divider::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.35), transparent);
}

.admin-login-footer {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  margin-top: var(--space-lg); padding-top: var(--space-md);
  border-top: 1px solid rgba(148, 163, 184, 0.14);
  color: #64748b; font-size: 0.74rem;
}
.admin-login-footer-icon { width: 14px; height: 14px; color: var(--login-teal); flex-shrink: 0; }

.admin-login-hint {
  background: rgba(251, 191, 36, 0.1); border: 1px solid rgba(251, 191, 36, 0.35); color: #fde68a;
  border-radius: var(--radius-md); padding: var(--space-xs) var(--space-sm); font-size: 0.82rem; line-height: 1.5;
  margin-bottom: var(--space-md);
}
.admin-login-hint code { background: rgba(255, 255, 255, 0.1); border-radius: 4px; padding: 1px 5px; color: #fff; }

.admin-login-field { display: block; margin-bottom: var(--space-sm); }
.admin-login-field span {
  display: block; font-size: 0.72rem; font-weight: 600; color: #93a3b8; margin-bottom: 6px;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.admin-login-input {
  width: 100%; min-height: var(--touch-min); padding: 0 var(--space-sm);
  border: 1px solid rgba(148, 163, 184, 0.3); border-radius: var(--radius-sm);
  font-size: 0.92rem; background: rgba(255, 255, 255, 0.05); color: #f1f5f9;
}
.admin-login-input::placeholder { color: #64748b; }
.admin-login-input:focus { outline: none; border-color: #60a5fa; box-shadow: var(--focus-ring); }

.admin-login-password-wrap { position: relative; }
.admin-login-password-wrap .admin-login-input { padding-right: 42px; }
.admin-login-password-toggle {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  width: 38px; height: 38px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; color: #93a3b8; cursor: pointer; border-radius: var(--radius-sm);
}
.admin-login-password-toggle:hover { color: #e2e8f0; }
.admin-login-eye-icon { width: 18px; height: 18px; }

.admin-login-error {
  background: rgba(248, 113, 113, 0.12); border: 1px solid rgba(248, 113, 113, 0.4); color: #fecaca;
  border-radius: var(--radius-md); padding: var(--space-xs) var(--space-sm); font-size: 0.84rem; margin-bottom: var(--space-sm);
}

.admin-login-submit {
  width: 100%; min-height: var(--touch-min); margin-top: var(--space-xs);
  border: none; border-radius: var(--radius-md); color: #fff; font-weight: 700; font-size: 0.92rem;
  letter-spacing: 0.02em; cursor: pointer;
  background: linear-gradient(135deg, var(--login-blue), var(--login-blue-dark));
  box-shadow: 0 10px 26px rgba(29, 78, 216, 0.38);
  transition: filter 0.15s ease, box-shadow 0.15s ease, transform 0.08s ease;
}
.admin-login-submit:hover { filter: brightness(1.08); box-shadow: 0 12px 30px rgba(29, 78, 216, 0.48); }
.admin-login-submit:active { transform: translateY(1px); }
.admin-login-submit:disabled { opacity: 0.6; cursor: not-allowed; filter: none; }

/* Create Admin Account screen's "Back to sign in" link only — everything
   else on that screen reuses the .admin-login-* rules above as-is. */
.admin-login-back-row { text-align: center; margin-top: var(--space-sm); font-size: 0.85rem; }

/* Replaces the inline style="color:#93c5fd"/"color:#fde68a" that used to sit
   directly on these buttons in web/admin.html — same elements/ids/click
   handlers, just a real class so :hover can actually change the color (an
   inline style attribute outranks any stylesheet rule, including a :hover
   one, so the old markup had no hover feedback at all on these links). */
.admin-login-link {
  background: none; border: none; cursor: pointer; text-decoration: underline;
  color: #93c5fd; font: inherit; font-size: inherit;
  padding: var(--space-2xs) 2px; min-height: var(--touch-min);
  display: inline-flex; align-items: center; gap: 6px;
}
.admin-login-link:hover { color: #bfdbfe; }
.admin-login-link-amber { color: #fde68a; text-decoration: underline; padding: 0; }
.admin-login-link-amber:hover { color: #fef3c7; }
.admin-login-link-icon { width: 15px; height: 15px; flex-shrink: 0; }

/* Google Sign-In screen: Google's own renderButton() draws a fixed-width
   button/iframe inside this div (see web/admin-app.js — width:352 now
   matches the card's inner content width: max-width:400px minus the
   card's left/right padding). It's left-aligned by default, which looks
   off-center against the centered .admin-login-brand above it when the
   button is narrower than the card; centering the slot itself (rather than
   trying to style Google's own iframe contents, which isn't supported)
   keeps the whole card visually centered without touching the button. */
.google-signin-button-slot {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-sm);
  min-height: var(--touch-min);
}

@media (max-width: 380px) {
  .admin-login-card { padding: var(--space-lg) var(--space-md); }
  .admin-login-mark { width: 58px; height: 58px; }
  .admin-login-brand h1 { font-size: 1.3rem; }
}

.admin-stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-md); margin-bottom: var(--space-md); }
/* Industrial Clarity: a top accent stripe, transparent by default (so the
   Session Detail modal's own re-use of .admin-stat-tile — .admin-stat-grid
   without .dashboard-kpi-grid, see further down — stays neutral/uncategorized)
   and colored only for the Overview row's category-coded tiles below. */
.admin-stat-tile {
  background: var(--panel); border: 1px solid var(--border); border-top: 3px solid transparent;
  border-radius: var(--radius-lg); padding: var(--space-md); box-shadow: var(--shadow-sm);
}
.admin-stat-value { font-size: 1.6rem; font-weight: 700; color: var(--ink-900); font-variant-numeric: tabular-nums; }
.admin-stat-label { font-size: 0.8rem; color: var(--ink-500); margin-top: 4px; letter-spacing: 0.04em; }
/* Employee-Based Reading Traceability: the session detail modal's own
   "Anomalies" stat tile should read as a warning the moment it's non-zero,
   same --danger color every out-of-spec value elsewhere already uses. */
.admin-stat-value.is-danger { color: var(--danger); }
@media (min-width: 560px) { .admin-stat-grid { grid-template-columns: repeat(4, 1fr); } }

/* Component Integration pass (see kpiTileHtml() in admin-app.js for the full
   rationale): three prior passes all kept the same flat-div-plus-CSS-order
   shape and only changed its paint (a gradient+icon-chip tile → a flat card
   with a left rail → a whole-card soft-tint wash) — correctly rejected each
   time for looking "almost identical." This pass replaces the generated
   markup itself with a real metric-card structure (CardHeader/CardTitle/
   CardFooter, see the "Metric card" rule block below), not another repaint
   of the same three stacked divs. --kpi-color is still assigned by tile
   POSITION, matching admin-app.js's `tiles` array in renderOverviewKpis() (1
   total, 2 active, 3 completed, 4 confirmed, 5 pending, 6 anomalies, 7 avg
   temperature) — now consumed by the header dot, the hover border, and the
   footer icon rather than a background wash. --kpi-soft/--kpi-border are no
   longer read anywhere (kept defined, harmless, in case a future pass wants
   a soft badge accent) now that no element is background-tinted by them. */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(1) { --kpi-color: var(--accent); --kpi-soft: var(--accent-soft); --kpi-border: var(--accent-border); } /* total sessions: teal */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(2) { --kpi-color: var(--chart-bright-teal); --kpi-soft: var(--chart-bright-teal-soft); --kpi-border: var(--chart-bright-teal-border); } /* active sessions: blue (Styling/Color Theme Replacement pass: was bright teal) */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(3) { --kpi-color: var(--ok); --kpi-soft: var(--ok-soft); --kpi-border: var(--ok-border); } /* completed sessions: green */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(4) { --kpi-color: var(--chart-emerald); --kpi-soft: var(--chart-emerald-soft); --kpi-border: var(--chart-emerald-border); } /* confirmed readings: indigo (Styling/Color Theme Replacement pass: was emerald) */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(5) { --kpi-color: var(--warn); --kpi-soft: var(--warn-soft); --kpi-border: var(--warn-border); } /* pending readings: amber (unchanged, semantic) */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(6) { --kpi-color: var(--danger); --kpi-soft: var(--danger-soft); --kpi-border: var(--danger-border); } /* total anomalies: red (unchanged, semantic) */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(7) { --kpi-color: var(--chart-cyan); --kpi-soft: var(--chart-cyan-soft); --kpi-border: var(--chart-cyan-border); } /* avg temperature: orange (Styling/Color Theme Replacement pass: was cyan) */

/* "Implement Artifact 2: Bold Accent Cards" pass: explicit request, with its
   own reference screenshot ("Option 2 - Accent Cards (Vibrant & Clear)"), to
   reverse the StageShow pass's deliberate choice further below ("a dashboard
   with 7 solid-tinted cards reads as 'painted'... neutral cards with
   meaningful dots reads as a real analytics product") back to full
   solid-color tiles, with these exact hex values given by the task (not the
   existing --accent/--chart-* design tokens, which are close but not
   identical to these) and white text/icons for contrast. Scoped to
   .dashboard-kpi-grid only (Overview's 7 KPI tiles) per explicit scope —
   the Session Detail modal's own plain .admin-stat-grid (no
   .dashboard-kpi-grid class) is untouched, as is every other screen.
   --kpi-bg/--kpi-bg-2 are new, separate from --kpi-color above (still used,
   unchanged, by .stat-card-dot/.stat-card-foot-icon's color before this
   pass overrides those to white below) so neither set of tokens has to be
   reinterpreted for its other purpose. */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(1) { --kpi-bg: #079B8E; --kpi-bg-2: #0FB7A6; } /* total sessions: teal gradient, as specified */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(2) { --kpi-bg: #2685D9; } /* active sessions: blue */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(3) { --kpi-bg: #20A65A; } /* completed sessions: green */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(4) { --kpi-bg: #7955C7; } /* confirmed readings: purple */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(5) { --kpi-bg: #E99A25; } /* pending readings: orange */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(6) { --kpi-bg: #DC5961; } /* total anomalies: coral red */
.dashboard-kpi-grid > .admin-stat-tile:nth-child(7) { --kpi-bg: #13A6B5; } /* average temperature: cyan/teal */

.dashboard-kpi-grid > .admin-stat-tile:not(.is-unavailable) {
  background: var(--kpi-bg);
  border-color: transparent;
}
.dashboard-kpi-grid > .admin-stat-tile:nth-child(1):not(.is-unavailable) {
  background: linear-gradient(160deg, var(--kpi-bg-2) 0%, var(--kpi-bg) 100%);
}
.dashboard-kpi-grid > .admin-stat-tile:not(.is-unavailable) .stat-card-label { color: rgba(255, 255, 255, 0.92); }
.dashboard-kpi-grid > .admin-stat-tile:not(.is-unavailable) .stat-card-dot { background: rgba(255, 255, 255, 0.88); }
.dashboard-kpi-grid > .admin-stat-tile:not(.is-unavailable) .stat-card-value { color: #ffffff; }
.dashboard-kpi-grid > .admin-stat-tile:not(.is-unavailable) .stat-card-foot {
  color: rgba(255, 255, 255, 0.88);
  border-top-color: rgba(255, 255, 255, 0.3);
}
.dashboard-kpi-grid > .admin-stat-tile:not(.is-unavailable) .stat-card-foot-icon { color: rgba(255, 255, 255, 0.88); }
/* Hover/focus on a now-solid-colored tile: the generic hover rules further
   below paint a teal (--accent) or per-tile --kpi-color border, which would
   mismatch or vanish against these new backgrounds -- a translucent white
   ring reads as "lifted" against any of the 7 colors instead. Three classes
   in the selector (.dashboard-kpi-grid, .admin-stat-tile, .is-clickable)
   outrank the 2-class generic rules regardless of source order, so this
   wins without needing !important. */
.dashboard-kpi-grid > .admin-stat-tile:not(.is-unavailable):hover { border-color: rgba(255, 255, 255, 0.55); }
.dashboard-kpi-grid > .admin-stat-tile.is-clickable:not(.is-unavailable):hover,
.dashboard-kpi-grid > .admin-stat-tile.is-clickable:not(.is-unavailable):focus-visible {
  border-left-color: rgba(255, 255, 255, 0.55);
  border-right-color: rgba(255, 255, 255, 0.55);
  border-bottom-color: rgba(255, 255, 255, 0.55);
}
.dashboard-kpi-grid > .admin-stat-tile.is-clickable:not(.is-unavailable):focus-visible { outline-color: rgba(255, 255, 255, 0.85); }

/* StageShow visual-polish pass: review feedback on this round was that the
   flat-white-card-plus-left-rail treatment above (shadcn stage) reads as
   "almost identical" at a glance — the category color only shows up in a
   3px sliver. Rather than reintroduce the gradient wash + icon chip this
   same file already tried and explicitly rejected (see the comment above),
   this reuses a pattern the app ALREADY has everywhere else: badges
   (.badge-ok/-warn/-danger, see ~line 492) pair a soft tinted background
   with a matching colored border. Applying that same soft/border pairing to
   the KPI tiles is bringing a component in line with the rest of the app's
   own visual language, not inventing a new one — each tile's whole surface
   now carries its category color (meaningful: status/severity), while the
   number stays var(--ink-900) for full-strength contrast against the very
   light (Tailwind-100-equivalent) tint. The left rail is kept at reduced
   weight (2px) as a secondary edge marker, not the only cue anymore. */
.dashboard-kpi-grid > .admin-stat-tile {
  background: var(--panel-elevated);
  border-color: var(--border);
}
.dashboard-kpi-grid > .admin-stat-tile.is-unavailable {
  background: var(--panel); border-color: var(--border); border-style: dashed;
}

/* Review feedback (redesign stage 3): the previous flexbox approach grew
   every tile to fill whatever width its own row had — avoiding an empty
   gap, but at the cost of the 4-tile row and the 3-tile row ending up
   visibly different widths per tile (a ~220px tile next to a ~290px one),
   which read as uneven rather than "a 4+3 layout." A real CSS Grid with a
   fixed column count gives every tile in every row the SAME width, so the
   partial last row sits at the same scale as the row above it instead of
   stretching to fill the leftover space — the standard, deliberate-looking
   way dashboards (Stripe, Vercel, etc.) present a non-multiple-of-4 metric
   count. Phone widths keep 2 columns, which halves the KPI row's total
   height compared to the old 1-per-row mobile stack. Tablet widths
   (StageShow pass: 560-859px, e.g. an 820px tablet) get a 3-column tier
   instead of inheriting the phone's 2 columns — at 2 columns a 7-tile grid
   ran 4 rows deep and left roughly half the tablet's extra width over a
   phone unused; 3 columns uses that width and brings it to 3 rows (3+3+1)
   while every tile in a row still stays the same size. The 7th tile
   (Average temperature, the lone odd one out below the 4-column desktop
   tier) spans the full row at both the 2- and 3-column tiers so the last
   row always reads as "N+N+full," not an awkward partial row.
   Scoped to .dashboard-kpi-grid only — the Session Detail modal's own
   .admin-stat-grid (always exactly 4 tiles, a clean 2x2 or 1x4, and never
   carries the .dashboard-kpi-grid class) is untouched throughout. */
.dashboard-kpi-grid.admin-stat-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-sm);
}
@media (min-width: 560px) and (max-width: 859px) {
  .dashboard-kpi-grid.admin-stat-grid { grid-template-columns: repeat(3, 1fr); gap: var(--space-md); }
}
@media (min-width: 860px) {
  .dashboard-kpi-grid.admin-stat-grid { grid-template-columns: repeat(4, 1fr); gap: var(--space-md); }
}
@media (max-width: 859px) {
  .dashboard-kpi-grid > .admin-stat-tile:nth-child(7) { grid-column: 1 / -1; }
}

/* ---- Metric card (shadcn "dashboard-01" block structure) -------------------
   Component Integration pass: previously each KPI was a flat tinted tile
   with value/label/meta stacked and re-sequenced by CSS `order` alone — the
   same three text nodes, just repainted. This replaces the generated markup
   itself (admin-app.js's kpiTileHtml(), see the comment there) with a real
   three-zone card: a CardHeader row (muted label + a meaningful colored
   status dot), a large CardTitle-equivalent value, and a CardFooter row
   (a small trend-style icon + context line) — the actual structural
   contract shadcn's own metric-card examples use, not a CSS reflow of a
   flat tile. The category color now lives ONLY in the header dot (status
   signal) and the hover border, never as a whole-tile wash — a deliberate,
   written choice: a dashboard with 7 solid-tinted cards reads as "painted,"
   a dashboard with 7 neutral cards and 7 distinct meaningful indicator dots
   reads as a real analytics product (Linear/Vercel/Stripe all follow this
   exact convention). */
.dashboard-kpi-grid > .admin-stat-tile {
  display: flex; flex-direction: column; gap: var(--space-2xs);
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.dashboard-kpi-grid > .admin-stat-tile:hover {
  border-color: var(--kpi-color, var(--border-strong));
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}
.stat-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2xs); }
.stat-card-label {
  text-transform: uppercase; font-weight: 600; font-size: 0.72rem; letter-spacing: 0.05em;
  color: var(--ink-500);
}
.stat-card-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--kpi-color, var(--ink-300)); }
.is-unavailable .stat-card-dot { background: var(--ink-300); }
.stat-card-value {
  font-size: 1.95rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.15;
  color: var(--ink-900); font-variant-numeric: tabular-nums;
}
.stat-card-value-na { color: var(--ink-400, var(--ink-500)); }
.stat-card-foot {
  display: flex; align-items: center; gap: 5px;
  font-size: 0.76rem; color: var(--ink-500); padding-top: var(--space-3xs);
  border-top: 1px solid var(--border); margin-top: 2px;
}
.stat-card-foot-icon { width: 13px; height: 13px; flex: none; color: var(--kpi-color, var(--ink-500)); }
.stat-card-foot-muted { font-size: 0.72rem; line-height: 1.4; border-top-color: transparent; padding-top: 0; }
.stat-card-foot-empty { border-top-color: transparent; padding-top: 0; }

.admin-stage-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-md); }
@media (min-width: 740px) { .admin-stage-grid { grid-template-columns: repeat(2, 1fr); } }
/* The grid's own gap spaces these cards; the generic .card + .card margin
   on top of it pushed every card after the first down out of its row. */
.admin-stage-grid > .card + .card { margin-top: 0; }
/* Industrial Clarity: a left-accent stripe that lights up green for whichever
   stage cards currently have an active session -- CSS-only via :has(), keyed
   to the existing "Active session" badge (.badge-ok, see admin-app.js's
   renderStageCard()) rather than a new class/attribute, so no JS/HTML change
   is needed. :has() is supported by every browser this app targets; on the
   rare one that doesn't, the stripe is simply absent (the badge text itself
   still says "Active session" either way, so nothing is lost, only the
   extra at-a-glance cue). */
.admin-stage-card { border-left: 3px solid transparent; transition: border-left-color 0.15s ease; }
.admin-stage-card:has(.admin-stage-card-head .badge-ok) { border-left-color: var(--ok); }
.admin-stage-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs); margin-bottom: 4px; }
.admin-stage-card-head h3 { margin: 0; font-size: 1rem; }
/* Industrial Clarity: a touch more structure -- a hairline separator and
   wider tracking so the form-ref/revision/effective-date line reads like a
   compact spec strip rather than ordinary muted body text. */
.admin-stage-card-ref {
  color: var(--ink-500); font-size: 0.76rem; font-weight: 600; letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums; margin: 0 0 var(--space-sm); padding-top: var(--space-2xs);
  border-top: 1px dashed var(--border);
}
.admin-stage-card-body p { margin: 0 0 6px; font-size: 0.86rem; color: var(--ink-700); }
.admin-stage-card-actions { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-xs); }
/* Responsible Person and Shift Assignment: the per-stage "start session"
   form grew from a single optional label input into several required
   personnel/shift fields -- a small label+input grid keeps each field
   identifiable at a glance rather than a wall of unlabeled text boxes. */
/* Start-session form usability pass: the old auto-fit grid gave all six
   fields equal weight and wrapped wherever the card width happened to land
   (four on one row, two orphaned below). Now a fixed 6-track grid whose
   rows follow who / when / label+action, sized off the CARD's own width
   (container query) rather than the viewport, since the same card renders
   one- or two-up depending on .admin-stage-grid. Narrow cards stack
   everything except the two shift times, which stay side by side. */
.admin-stage-card-body { container-type: inline-size; }
.admin-start-session-form {
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--space-sm) var(--space-xs); align-items: start;
}
.admin-start-session-field { grid-column: span 6; display: flex; flex-direction: column; gap: var(--space-3xs); min-width: 0; }
.admin-start-session-field--time { grid-column: span 3; }
.admin-start-session-field label { font-size: 0.8rem; font-weight: 600; color: var(--ink-700); }
.admin-start-session-field .admin-text-input { min-width: 0; }
.admin-start-session-field input[type="date"],
.admin-start-session-field input[type="time"] { font-variant-numeric: tabular-nums; }
/* font-size/color restated on these two: .admin-stage-card-body p above
   outranks the shared .form-error-text/.form-help-text classes. */
.admin-start-session-field .form-error-text { margin: 0; font-size: 0.78rem; color: var(--danger); }
.admin-field-optional { font-weight: 400; color: var(--ink-500); margin-left: 2px; }
/* Shift-length readout ("8 h shift") -- pulled up against the time row it
   describes; takes no space until both times are filled in. */
.admin-start-session-form .admin-start-session-shift-hint {
  grid-column: 1 / -1; margin: calc(var(--space-2xs) - var(--space-sm)) 0 0;
  font-size: 0.78rem; color: var(--ink-500);
}
.admin-start-session-shift-hint:empty { display: none; }
.admin-start-session-actions { grid-column: 1 / -1; display: flex; }
@container (min-width: 460px) {
  .admin-start-session-field--person,
  .admin-start-session-field--label { grid-column: span 4; }
  .admin-start-session-field--employee,
  .admin-start-session-field--date,
  .admin-start-session-field--time { grid-column: span 2; }
  .admin-start-session-form .admin-start-session-shift-hint { grid-column: 3 / -1; }
  /* Sits on the label field's row, bottom-aligned with its input. */
  .admin-start-session-actions { grid-column: span 2; align-self: end; }
}
.admin-start-session-row { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
.admin-start-session-row .admin-text-input { flex: 1 1 160px; }
.admin-join-link { margin-top: var(--space-sm); background: var(--ink-100); border-radius: var(--radius-md); padding: var(--space-xs); }
.admin-join-link-hint { font-size: 0.78rem; color: var(--ink-500); margin: 0 0 4px; }
.admin-join-link-row { display: flex; gap: 6px; }
.admin-join-link-row .admin-text-input { flex: 1; font-size: 0.78rem; }
/* Admin-First Startup, §2: the QR code rendered next to the copyable join
   link -- an inline <svg> fetched from GET /api/admin/sessions/<id>/qr. */
.admin-join-link-qr { margin-top: var(--space-sm); max-width: 160px; }
.admin-join-link-qr svg { display: block; width: 100%; height: auto; background: #fff; border-radius: var(--radius-sm); }
.admin-backup-result { margin-top: var(--space-xs); font-size: 0.82rem; color: var(--ink-700); }

/* ---- Numbered recordings (admin view, redesign §7) ------------------------ */
.admin-recordings-panel { margin-top: var(--space-sm); background: var(--ink-100); border-radius: var(--radius-md); padding: var(--space-xs); }
.admin-recordings-loading, .admin-recordings-empty { margin: 0; font-size: 0.82rem; color: var(--ink-500); }
.admin-recordings-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2xs); }
.admin-recordings-item { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--panel); padding: var(--space-2xs) var(--space-xs); }
.admin-recordings-row { display: flex; align-items: center; gap: var(--space-xs); flex-wrap: wrap; }
.admin-recordings-number { font-weight: 700; color: var(--ink-900); font-size: 0.84rem; }
.admin-recordings-meta { flex: 1 1 auto; color: var(--ink-500); font-size: 0.78rem; }
.admin-recordings-row .qc-btn { flex: none; min-height: 32px; padding: 0 var(--space-sm); font-size: 0.78rem; }
.admin-recording-detail:not([hidden]) { margin-top: var(--space-2xs); }
.admin-recording-detail-rows { display: flex; flex-direction: column; gap: 4px; }

/* Review feedback (redesign stage 4, Readings & Audit Trail): now that the
   Overview filter toolbar has been flattened to a borderless inline row
   (the .admin-filter-row.dashboard-filter-row override further down), the
   plain dropdown rows these two screens use — which share the bare
   .admin-filter-row class — kept the OLD boxed/tinted pill treatment
   (Industrial Clarity's "subtle grouping pill"), which now reads as
   inconsistent with Overview right next to it in the nav. Flattened the
   same way: no background/border/radius of its own, just the inline flex
   row of controls. Also lightens the select border from --border-strong to
   --border to match the date/search inputs already lightened on Overview
   (this selector is shared, so the fix lands on Overview's own stage
   dropdown too, for the same reason). */
/* Mobile overflow fix, part 1 (caught in stage-4 screenshot verification,
   not called out by the reviewer): .admin-filter-row is itself a flex ITEM
   of the parent `.screen-head-row` (display:flex; flex-wrap:wrap). A flex
   item's default min-width is `auto`, not 0 — so even though this row can
   wrap its own children, as a whole it was floored at its own content's
   single-line min width (~420px, driven by its 3 controls) and couldn't
   shrink to fit a 390px viewport, forcing horizontal page scroll on Readings
   and Audit Trail. min-width: 0 lets `.screen-head-row` wrap it onto its own
   full-width line below the title, same as the Overview filter row already
   did (Overview never hit this because `.dashboard-filter-row` had the
   narrower, already-wrapping Industrial Clarity markup). */
.admin-filter-row {
  display: flex; gap: var(--space-xs); flex-wrap: wrap; align-items: flex-end; min-width: 0;
}
/* Mobile overflow fix, part 2: Readings' and Audit Trail's stage <select>
   options include long stage names (e.g. "Tengah II (Grading, Layering,
   Washing V/VI, Infeed Tunnel)"), so each select's own content-based
   min-width also needed flooring to 0 to let it shrink/wrap like the
   search input beside it; max-width: 100% stops it from exceeding the
   row's own width once wrapped onto its own line. */
.admin-filter-row select {
  min-height: var(--touch-min); padding: 0 var(--space-xs); border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--panel); color: var(--ink-900); font-size: 0.86rem;
  min-width: 0; max-width: 100%; flex: 1 1 200px;
}
/* Readings' #readingsSearchInput previously had no dedicated rule at all
   (only Overview's own search box, scoped to .dashboard-filter-row, was
   ever styled) — it rendered in the bare browser-default search input
   style, which looked unstyled next to the select beside it. Gives it the
   same height/border/radius/font as the select so the row reads as one
   consistent set of controls. */
.admin-filter-row input[type="search"] {
  min-height: var(--touch-min); padding: 0 var(--space-xs); border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--panel); color: var(--ink-900); font-size: 0.86rem;
  min-width: 0; max-width: 100%; flex: 1 1 200px;
}
.admin-filter-row select:focus-visible,
.dashboard-filter-field input:focus-visible,
.dashboard-filter-row input[type="search"]:focus-visible {
  border-color: var(--accent);
}

.audit-timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2xs); }
.audit-timeline-item {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-xs);
  padding: var(--space-2xs) 0; border-bottom: 1px solid var(--border);
}
.audit-timeline-item:last-child { border-bottom: none; }
.audit-timeline-type { font-weight: 600; font-size: 0.8rem; color: var(--ink-700); }
.audit-timeline-detail { font-size: 0.84rem; color: var(--ink-700); flex: 1 1 200px; }
.audit-timeline-ts { font-size: 0.76rem; color: var(--ink-500); white-space: nowrap; margin-left: auto; }
.audit-timeline-lg .audit-timeline-item { padding: var(--space-xs) 0; }

/* Local QC Document Storage ("Implement Local QC Document Storage and
   Admin Console Access"): per-row View/Download/Retry controls on the
   Local Documents history list — same scoped-override convention as
   .admin-recordings-row .qc-btn above (a smaller .qc-btn variant, never a
   redefinition of .qc-btn itself), plus a flex wrapper so the actions sit
   on their own line on narrow/mobile widths instead of overlapping the
   timestamp. */
.audit-timeline-actions { display: flex; gap: var(--space-2xs); flex: 1 0 100%; }
.audit-timeline-actions .qc-btn-sm { flex: none; min-height: 32px; padding: 0 var(--space-sm); font-size: 0.78rem; text-decoration: none; }

/* ============================================================================
   Admin Dashboard Operational Analytics redesign — Overview screen (filter
   bar, KPI tiles, inline-SVG charts, quality-alert tabs, sessions list) and
   the Session Detail modal. Reuses .card/.admin-stat-grid/.admin-stat-tile/
   .admin-filter-row/.badge-* /.empty-state/.audit-timeline/.link-btn/.qc-btn
   wholesale — the rules below only add what those don't already cover. ===== */

/* shadcn/ui redesign (stage 2, Admin Overview): the previous pass gave this
   one screen a gradient underline and a gradient "spine" bar in front of the
   heading — decorative flourishes with no counterpart in shadcn's own page
   headers, which rely on plain typographic weight/size and a single hairline
   rule, never color, to establish hierarchy. Both pseudo-elements are
   removed outright (no DOM/JS depends on them); the heading itself is kept
   bolder/larger than the shared .screen-head default so Overview still reads
   as the primary page, just without the gradient paint. Still scoped to
   #screen-overview only — Stages & Sessions / Readings / Audit Trail /
   Reports & Backups / Settings & Help are a later, separate approval. */
#screen-overview .screen-head {
  display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between;
  gap: var(--space-md); border-bottom: 1px solid var(--border); margin-bottom: var(--space-lg);
  padding: 0 0 var(--space-md);
}
/* Review feedback (redesign stage 3, generalized in stage 4): the topbar's
   own route title (.topbar-route-title, bumped to font-weight 700 in stage
   2) already names the current page directly above this heading, so every
   screen repeated its own name twice before any real content appeared —
   not just Overview. Each <h2> stays in the DOM and keeps announcing the
   page to screen readers/the accessibility tree (the topbar's title is a
   plain <h1>-less label, not a heading element, so removing these would
   leave the page without a real heading at all) — only taken out of VISUAL
   flow with the standard "visually hidden" clip pattern, never
   `display:none`. Page navigation is unaffected: the sidebar's highlighted
   row and the topbar title are still there; this only removes each
   screen's redundant second copy of its own name.

   Stages & Sessions / Readings / Reports & Backups are scoped by id alone
   (none of those ids exist in the legacy single-device app, web/index.html,
   which shares this same stylesheet). Audit Trail and Settings & Help ARE
   reused as id names by web/index.html's own (untouched, out of scope)
   Audit and Settings screens, so those two are additionally qualified with
   :has() against an element id admin-app.js actually wires up (#auditStageFilter
   / #createAdditionalAdminForm) that only this dashboard's markup contains
   — without it, this rule would also hide the heading on the legacy app's
   same-named screens, which this pass must not touch. */
#screen-overview .screen-head h2,
#screen-stages .screen-head h2,
#screen-readings .screen-head h2,
#screen-reports .screen-head h2,
#screen-audit:has(#auditStageFilter) .screen-head h2,
#screen-settings:has(#createAdditionalAdminForm) .screen-head h2 {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
#screen-overview .screen-head p { font-size: 0.88rem; max-width: 52ch; margin-top: 0; color: var(--ink-500); }

/* Task 4 (CSS-only refinements): the description previously wrapped to two
   lines on desktop because of the 52ch max-width. Desktop has plenty of
   horizontal room now that .admin-main removed the 1180px content cap, so
   let it run on one line there; mobile/tablet keep the original wrapping
   behavior (max-width removed there too, but no white-space override, so
   the browser wraps naturally at the narrower viewport). */
#screen-overview .screen-head p { max-width: none; }
@media (min-width: 1025px) {
  #screen-overview .screen-head p { white-space: nowrap; }
}

/* shadcn redesign: shadcn's own data-toolbar convention (seen above tables
   throughout the component docs) is a borderless, inline row of controls —
   never a boxed/tinted container. The previous "elevated card" toolbar
   (gradient fill, strong border, shadow) is dropped in favor of a plain row
   separated from the KPI/content below by one hairline, consistent with the
   flatter chrome used elsewhere in this pass. Selector changed from the bare
   `.dashboard-filter-row` to the compound `.admin-filter-row.dashboard-filter-row`
   so this stays scoped to the Overview toolbar specifically (which carries
   both classes) and does not touch Readings'/Audit Trail's plain
   `.admin-filter-row` toolbars — those are a later, separate approval. */
.admin-filter-row.dashboard-filter-row {
  align-items: flex-end;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: none;
  padding: var(--space-sm) var(--space-sm);
}
.dashboard-filter-field input[type="date"],
.dashboard-filter-row input[type="search"],
.dashboard-filter-row select {
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
  height: 38px;
}
/* Component Integration pass: leading icons on the date/search controls and
   a real custom chevron on the stage <select> (appearance:none + a drawn
   SVG, matching shadcn's Select trigger look) — a plain native select with
   no visual cue that it opens a list, and bare text inputs with no
   affordance for what they filter by, read as unfinished toolbar controls
   next to Reset filters' icon-equipped button. The underlying elements stay
   native (real keyboard/screen-reader semantics, zero JS risk), only their
   paint changed. */
.dashboard-filter-field input[type="date"] {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%2364748b' stroke-width='1.4'%3E%3Crect x='3' y='4.5' width='14' height='12' rx='1.5'/%3E%3Cline x1='3' y1='8' x2='17' y2='8'/%3E%3Cline x1='6.5' y1='2.5' x2='6.5' y2='5.5'/%3E%3Cline x1='13.5' y1='2.5' x2='13.5' y2='5.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: 9px center; background-size: 15px;
  padding-left: 30px;
}
.dashboard-filter-row input[type="search"] {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%2364748b' stroke-width='1.5'%3E%3Ccircle cx='8.5' cy='8.5' r='5.5'/%3E%3Cline x1='12.7' y1='12.7' x2='17' y2='17' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: 10px center; background-size: 14px;
  padding-left: 32px;
}
.dashboard-filter-row select {
  -webkit-appearance: none; appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%2364748b' stroke-width='1.6'%3E%3Cpath d='M5.5 8l4.5 4.5L14.5 8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center; background-size: 14px;
  padding-right: 32px;
}
/* Admin Overview Premium fix: a native <select> with no explicit width sizes
   its CLOSED box to fit its widest <option> — harmless with the original
   "All stages"-only placeholder, but once real stage labels exist as
   options (e.g. "Tengah I (Skinning, Trimming, Cektor)") that intrinsic
   width (~420px) overflowed the 375px mobile breakpoint, since a flex item's
   default min-width:auto also stops it from ever shrinking below that
   content size. max-width:100% + min-width:0 caps the CLOSED box to the
   toolbar's own width (the opened dropdown still shows full option text,
   unaffected) — this was a latent bug the richer Overview data this pass
   adds now actually exercises, not something introduced by the redesign
   itself, but it's squarely inside this pass's §5/§11 responsive scope. */
.dashboard-filter-row select { width: 100%; max-width: 100%; min-width: 0; flex: 1 1 100%; }
.dashboard-filter-row select:focus-visible,
.dashboard-filter-field input:focus-visible,
.dashboard-filter-row input[type="search"]:focus-visible {
  box-shadow: 0 0 0 3px var(--accent-soft);
}
#overviewFilterReset:hover { border-color: var(--danger); color: var(--danger); }
.dashboard-filter-field {
  display: flex; flex-direction: column; gap: 3px; font-size: 0.72rem; font-weight: 600;
  color: var(--ink-500); text-transform: uppercase; letter-spacing: 0.04em;
}
.dashboard-filter-field input[type="date"] {
  min-height: var(--touch-min); padding: 0 var(--space-xs); border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--panel); color: var(--ink-900); font-size: 0.84rem;
}
.dashboard-filter-row input[type="search"] {
  min-height: var(--touch-min); padding: 0 var(--space-xs); border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--panel); color: var(--ink-900); font-size: 0.84rem;
  min-width: 180px; flex: 1 1 180px;
}

/* KPI tiles: an "unavailable" tile (quantity in kg, pressure-dependent
   metrics, or a metric with zero supporting data) is deliberately styled as
   visually distinct from a real zero — dashed border, muted value, and its
   `reason` always printed inline, never only in a title= tooltip, so the
   explanation survives on touch devices where hover tooltips don't fire. */
.admin-stat-tile.is-unavailable { border-style: dashed; background: var(--neutral-soft); }
.admin-stat-value-na { color: var(--ink-500); }
.admin-stat-reason { font-size: 0.72rem; color: var(--ink-500); margin-top: var(--space-2xs); line-height: 1.4; }
.admin-stat-meta { font-size: 0.72rem; color: var(--ink-500); margin-top: 2px; }
.admin-stat-tile.is-clickable { cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s; }
/* Industrial Clarity: border-left/right/bottom-color only here (not the
   `border-color` shorthand) so a hovered/focused category-coded tile (see
   the .dashboard-kpi-grid nth-child rules above) keeps its own top-accent
   color instead of it flashing to generic accent blue on every hover --
   the lifted shadow alone is enough interactivity feedback. */
.admin-stat-tile.is-clickable:hover,
.admin-stat-tile.is-clickable:focus-visible {
  border-left-color: var(--accent); border-right-color: var(--accent); border-bottom-color: var(--accent);
  box-shadow: var(--shadow-lg);
}
.admin-stat-tile.is-clickable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Industrial Premium (Task D): CSS Grid's default align-items:stretch was
   stretching every chart card to match the tallest card in its row (e.g.
   "Processing trend," a plain chart with no legend, was stretched to match
   "Temperature trend," which has an extra legend row beneath it) — the
   actual root cause of the "excessive empty space" the brief called out,
   not a chart-sizing problem. align-items:start lets each card size to its
   own content instead. */
.dashboard-charts-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-md); margin-bottom: var(--space-md); align-items: start; }
/* StageShow pass: the charts were single-column below 860px, so an 820px
   tablet stacked both trend cards full-width even though it has much more
   room than a phone. The charts themselves have no fixed width (svg is
   width:100%/height:auto, see .dashboard-chart-svg) and carry at most a
   short legend line, so they read fine at a tablet half-width; lowering
   the breakpoint to 700px brings the two trend cards side by side on
   tablet while phones (<700px) keep the single stacked column. */
@media (min-width: 700px) { .dashboard-charts-grid { grid-template-columns: repeat(2, 1fr); } }

/* Artifact 1 chart row: the two headline trend charts (Processing Trend,
   Temperature Trend) get their own row above the secondary grid, at the
   1.3fr/1fr ratio the reference template uses, each with a quick date-range
   segmented control in its header. Separate from .dashboard-charts-grid
   (which now lays out the 2 remaining secondary cards, Completion &
   pending status and Anomalies by stage, after Quantity by stage and
   Pressure trend were removed) so the two trend charts read as the
   dashboard's headline row, not a block of equal tiles. */
.dashboard-charts-primary-row { display: grid; grid-template-columns: 1fr; gap: var(--space-md); margin-bottom: var(--space-md); align-items: start; }
@media (min-width: 860px) { .dashboard-charts-primary-row { grid-template-columns: 1.3fr 1fr; } }
.dashboard-chart-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-xs); }
.dashboard-chart-head h3 { margin-bottom: 0; }
.dashboard-chart-titles { min-width: 0; }
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 2px; background: var(--surface); flex: none; }
.segmented button {
  border: none; background: none; padding: 5px 11px; font-size: 0.72rem; font-weight: 600; color: var(--ink-500);
  border-radius: 5px; cursor: pointer; min-height: 26px;
}
.segmented button.active { background: var(--panel); color: var(--ink-900); box-shadow: var(--shadow-sm); }
.segmented button:hover:not(.active) { color: var(--ink-700); }
/* Admin Overview Premium (§7/§8/§9): a shared min-height so the Processing
   Trend/Temperature Trend line charts and the bar/meter charts beside them
   (anomalies by stage, quantity by stage, completion distribution) read as
   one consistent chart row instead of whichever has the least content
   looking visibly shorter — content that's naturally taller (e.g. an extra
   legend row) still grows the card normally, this only raises the floor. */
.dashboard-chart-card { min-height: 230px; display: flex; flex-direction: column; }
.dashboard-chart-card h3 { margin-bottom: 2px; }
/* StageShow visual-polish pass: Temperature Trend already identifies its
   line's color via the per-stage swatch legend below the chart (see
   .dashboard-legend above) -- Processing Trend has no legend (always a
   single untitled "Readings" series), so it had no color identity in its
   heading at all. Reuses the exact same swatch look as the legend
   (10px rounded square) rather than inventing a second marker style, tied
   to the chart's real series color (CHART_COLORS.accent in admin-app.js),
   never a decorative hardcoded hue. */
#chartProcessingTrend h3 { display: flex; align-items: center; gap: 8px; }
#chartProcessingTrend h3::before {
  content: ''; width: 10px; height: 10px; border-radius: 3px; flex: none;
  background: var(--accent); box-shadow: 0 0 0 1px rgba(0,0,0,0.08);
}
.dashboard-chart-sub { font-size: 0.78rem; color: var(--ink-500); margin: 0 0 var(--space-xs); }
.dashboard-chart-svg { width: 100%; height: auto; display: block; margin-top: var(--space-2xs); }
/* Admin Overview Premium: rounder line joins/caps (previously the SVG path's
   own inline style only ever set stroke/stroke-width, leaving the browser
   default miter/butt joins — sharp on a zig-zagging trend line) and a soft
   drop-shadow on each data point so points stand off the line/gridlines a
   little, consistent with the "improved data-point visibility" ask. Applies
   to every buildLineChartSvg()-rendered chart (Processing Trend, Temperature
   Trend, and any future line chart) without touching admin-app.js. */
.dashboard-chart-svg path { stroke-linecap: round; stroke-linejoin: round; }
.dashboard-chart-svg circle { filter: drop-shadow(0 1px 2px rgba(15, 23, 42, 0.25)); }
/* Admin Overview Premium: a gentle fade/rise-in so a freshly (re)rendered
   chart (initial load, or a filter change replacing the SVG) doesn't just
   pop into place — purely decorative, re-triggers correctly because
   admin-app.js replaces each chart card's innerHTML wholesale on every
   render, which re-inserts a fresh <svg> element each time. */
.dashboard-chart-svg { animation: dashboard-chart-in 0.35s ease-out; }
@keyframes dashboard-chart-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
/* Review feedback (redesign stage 3): "keep the existing data-validation
   messages, but make the empty state visually informative rather than
   leaving large blank areas." The wording admin-app.js generates
   (singleDataPointNote()/unavailableChartHtml()/the inline "No … in this
   filter window" strings) is completely unchanged — only how these three
   states are PRESENTED is addressed here. */

/* 1) A single-data-point line chart still draws a real (if sparse) chart —
   this just turns the explanatory caption from a throwaway italic line
   under it into a small, deliberate callout, so it reads as a designed
   state rather than an apology. */
.dashboard-chart-note {
  display: flex; align-items: center; gap: 6px;
  font-size: 0.78rem; font-style: normal; color: var(--ink-700);
  margin: var(--space-xs) 0 0; padding: 5px var(--space-xs);
  background: var(--neutral-soft); border-radius: var(--radius-sm);
}
.dashboard-chart-note::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--ink-300); flex: none; }

/* .dashboard-chart-unavailable (the centered icon+reason empty state used by
   the Quantity by stage / Pressure trend chart cards) was removed along with
   those two cards and admin-app.js's unavailableChartHtml() — see that
   file's renderOverviewCharts() comment. No other caller used this class. */

.empty-state-inline { color: var(--ink-500); font-size: 0.84rem; margin: var(--space-xs) 0 0; }
/* 3) A chart with zero rows in the current filter window (Processing trend/
   Temperature trend/Anomalies by stage, when admin-app.js renders only an
   <h3> + one <p class="empty-state-inline"> and nothing else) gets the same
   centered, iconed treatment as the "unavailable" case above, for the same
   reason — otherwise it's one line of gray text pinned to the top of an
   otherwise-blank 230px card. Scoped narrowly via "h3 + …:last-child" so it
   matches ONLY that direct empty-card shape and never the unrelated
   .empty-state-inline note renderCompletionDistributionHtml() sometimes
   appends AFTER its two real distribution rows (that one stays a small
   inline note under real content, not a replacement for it). A plain CSS
   dashed-circle stands in for an icon here since this particular markup
   (unlike unavailableChartHtml()) has no <svg> to restyle. */
.dashboard-chart-card > h3 + .empty-state-inline:last-child {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-xs); text-align: center; margin: 0; padding: var(--space-md);
}
.dashboard-chart-card > h3 + .empty-state-inline:last-child::before {
  content: ''; width: 34px; height: 34px; border-radius: 50%; border: 2px dashed var(--ink-300);
}

/* Industrial Premium: grouped into a subtle pill (matching the filter-row
   treatment elsewhere) instead of floating directly on the card background,
   and given a touch more breathing room — reads more like a deliberate
   legend and less like an afterthought beneath the chart. */
.dashboard-legend { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-sm); padding: var(--space-2xs) var(--space-xs); background: var(--neutral-soft); border: 1px solid var(--neutral-border); border-radius: var(--radius-sm); }
.dashboard-legend-item { display: inline-flex; align-items: center; gap: 6px; font-size: 0.76rem; font-weight: 600; color: var(--ink-700); }
.dashboard-legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; box-shadow: 0 0 0 1px rgba(0,0,0,0.08); }

.dashboard-distribution-row { margin-bottom: var(--space-sm); }
.dashboard-distribution-row:last-child { margin-bottom: 0; }
.dashboard-distribution-label { display: flex; justify-content: space-between; gap: var(--space-xs); font-size: 0.82rem; color: var(--ink-700); margin-bottom: 4px; }
.dashboard-distribution-label span { color: var(--ink-500); font-size: 0.78rem; }
.dashboard-distribution-meter { display: flex; height: 10px; border-radius: var(--radius-pill); overflow: hidden; background: var(--neutral-soft); border: 1px solid var(--neutral-border); }
.dashboard-distribution-seg { height: 100%; transition: width 0.2s; }
.dashboard-distribution-seg.seg-active { background: var(--accent); }
.dashboard-distribution-seg.seg-completed { background: var(--ok); }
.dashboard-distribution-seg.seg-confirmed { background: var(--ok); }
.dashboard-distribution-seg.seg-pending { background: var(--warn); }

.dashboard-alert-tabs { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-bottom: var(--space-sm); }
.dashboard-alert-tab {
  background: var(--neutral-soft); border: 1px solid var(--neutral-border); color: var(--ink-700);
  border-radius: var(--radius-pill); padding: 6px 14px; font-size: 0.8rem; font-weight: 600; cursor: pointer;
}
.dashboard-alert-tab:hover { border-color: var(--accent); color: var(--accent-dark); }
.dashboard-alert-tab.active { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast); }

.dashboard-session-row { cursor: pointer; border-radius: var(--radius-sm); }
.dashboard-session-row:hover, .dashboard-session-row:focus-visible { background: var(--neutral-soft); }
.dashboard-session-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* StageShow visual-polish pass: an initials avatar so "Sessions in range"
   can be scanned by who's behind each row, not just read. Scoped to
   .dashboard-session-row specifically (Overview's list) rather than the
   shared .audit-timeline-item class, so the Audit Trail screen's own
   timeline (session_created/reading_confirmed/... events, most of which
   have no single "person" to show) is completely unaffected. One flat
   accent-soft tint for every avatar, not a per-person color -- initials
   are the identifying signal; a different hue per name would just be
   decoration with no real meaning (and would eventually collide anyway). */
.dashboard-session-avatar {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--accent-soft); color: var(--accent-dark);
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.01em;
}

/* ---- Data table (shadcn Table component) -----------------------------------
   Component Integration pass: a genuine reusable <table> primitive — thin
   row dividers only (no vertical rules), a muted uppercase header row, and a
   hover wash per body row — used by "Sessions in range" (admin-app.js's
   renderOverviewSessions()) and Worker Capture's Recent Readings
   (worker-app.js's renderRecentReadings()). Deliberately a different visual
   contract from .qc-form-table (the bordered grid that reproduces the
   official paper QC form) — that one needs full ruled borders to look like
   the printed document; this one is an ordinary data list and should read
   like one. */
.data-table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
.data-table thead th {
  text-align: left; font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--ink-500); padding: 0 var(--space-sm) var(--space-2xs);
  border-bottom: 1px solid var(--border);
}
.data-table tbody td { padding: var(--space-xs) var(--space-sm); border-bottom: 1px solid var(--border); vertical-align: middle; }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr { transition: background 0.12s ease; }
.data-table-primary { font-weight: 600; color: var(--ink-900); }
.data-table-secondary { font-size: 0.78rem; color: var(--ink-500); margin-top: 1px; }
.data-table-person { display: inline-flex; align-items: center; gap: 8px; color: var(--ink-700); }
.data-table-ts { font-size: 0.8rem; color: var(--ink-500); white-space: nowrap; font-variant-numeric: tabular-nums; }
.dashboard-session-row td:first-child { width: 1%; white-space: nowrap; }

/* Below 560px a 4-column table gets cramped — fold it into stacked "label:
   value" rows instead of scrolling horizontally, so Sessions in range (and
   Recent Readings on Worker Capture, which shares this component) stays a
   single comfortable reading column on a phone. data-label attributes on
   each <td> (emitted by the JS above) supply the printed label via
   generated content, so no duplicate markup is needed per breakpoint. */
@media (max-width: 559px) {
  .data-table thead { display: none; }
  .data-table, .data-table tbody, .data-table tr, .data-table td { display: block; width: 100%; }
  .data-table tbody tr { border-bottom: 1px solid var(--border); padding: var(--space-2xs) 0; }
  .data-table tbody tr:last-child { border-bottom: none; }
  .data-table tbody td { border-bottom: none; padding: 3px var(--space-2xs); display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); }
  .data-table tbody td[data-label]::before {
    content: attr(data-label); font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--ink-500); flex: none;
  }
}

/* Avoid exposing raw internal JSON event payloads directly in the activity
   feed (redesign §5): the readable label is the primary, always-visible
   text; the raw qc_store event payload stays reachable but collapsed. */
.audit-raw-detail { margin-top: 4px; }
.audit-raw-detail summary { font-size: 0.74rem; color: var(--ink-500); cursor: pointer; }
.audit-raw-detail pre {
  margin: 4px 0 0; padding: var(--space-2xs) var(--space-xs); background: var(--ink-100); border-radius: var(--radius-sm);
  font-size: 0.72rem; color: var(--ink-700); overflow-x: auto; white-space: pre-wrap; word-break: break-word;
}

/* Review feedback (redesign stage 3), mobile readability + vertical rhythm.
   Everything below is scoped to #screen-overview (or to classes — like
   .dashboard-kpi-grid.admin-stat-grid — that only #overviewKpis carries) so
   none of it reaches the real Audit Trail screen's own #auditList/
   .audit-timeline-lg, even though Quality Alerts and the "Sessions in
   range" list reuse the same .audit-timeline-item/.audit-timeline-detail/
   .audit-timeline-ts classes Audit Trail does. Phone-width cutoff (560px)
   matches the one already used for the Field Reference/Standards screens
   elsewhere in this file. */
@media (max-width: 560px) {
  /* Readability: the uppercase+letter-spacing treatment that reads fine at
     desktop sizes gets cramped at phone sizes — loosen it back down. */
  .stat-card-label { font-size: 0.76rem; letter-spacing: 0.03em; }
  /* Quality Alerts tabs: a touch more padding so the 4-tab strip (which
     wraps to two rows at this width) is comfortably tappable, and a half
     step up in weight/size so the active/inactive tabs stay legible at a
     glance rather than disappearing into the muted track color. */
  #screen-overview .dashboard-alert-tab { padding: 8px 14px; font-size: 0.82rem; }
  /* Quality Alerts items and "Sessions in range" rows: both render via
     .audit-timeline-item/-detail/-ts (see admin-app.js's renderAlertItem()/
     renderOverviewSessions()) at a size tuned for Audit Trail's denser,
     wider desktop table — a touch larger and with more line-height reads
     far better in a single narrow mobile column. */
  #screen-overview .audit-timeline-detail { font-size: 0.86rem; line-height: 1.5; }
  #screen-overview .audit-timeline-ts { font-size: 0.78rem; }
  /* Filter controls: the uppercase field labels and the native date/search/
     select controls below them both get a small bump for phone legibility
     (16px+ on the inputs also avoids iOS Safari's auto-zoom-on-focus). */
  #screen-overview .dashboard-filter-field { font-size: 0.76rem; }
  #screen-overview .dashboard-filter-field input[type="date"],
  #screen-overview .dashboard-filter-row input[type="search"],
  #screen-overview .dashboard-filter-row select { font-size: 1rem; }

  /* Vertical rhythm: with the KPI grid now 2-across (see
     .dashboard-kpi-grid.admin-stat-grid above) the Overview page is already
     noticeably shorter than the old 1-per-row phone stack, but the fixed
     gaps/heights tuned for desktop are still wider than a phone screen
     needs. Tightened here rather than changed at the base/desktop rule, so
     nothing about the approved desktop layout moves. */
  #screen-overview .screen-head { margin-bottom: var(--space-sm); }
  .dashboard-kpi-grid.admin-stat-grid { gap: var(--space-2xs); margin-bottom: var(--space-sm); }
  .dashboard-charts-grid { gap: var(--space-sm); margin-bottom: var(--space-sm); }
  .dashboard-chart-card { min-height: 190px; }
}

/* ---- Session Detail modal content (reuses the generic .modal-* shell) --- */
#sessionDetailModal { width: min(760px, 100%); }
#sessionDetailDocument .qc-btn-sm { margin-right: var(--space-2xs); text-decoration: none; }

/* ============================================================================
   shadcn/ui-inspired Component System (UI Improvement pass, stage 1 of a
   larger redesign — Anti-Slop/StageShow are deliberately NOT started here).

   This app already has a mature design-token system (--accent/--ok/--warn/
   --danger/--info/--corrected, --radius-*, --shadow-*, --space-*, built up
   over many prior passes — Industrial Premium, Industrial Clarity, Admin
   Overview Premium) and a working set of shared component classes (.card,
   .badge-*, .qc-btn, .admin-stat-tile, .modal, .nav-item, .audit-timeline,
   .empty-state). None of that is replaced or redefined here — shadcn/ui's
   OWN design philosophy is "copy the primitive, don't add a dependency,"
   and the closest equivalent in a token-less vanilla-CSS app is: reuse the
   existing tokens, complete the existing primitives' missing states, don't
   invent a second, parallel color/radius system that would fight the first.

   What THIS section adds, studied directly from shadcn/ui's component
   conventions and adapted to classes/ids that already exist in this app
   (never new ones, except where noted) --
     - A single, consistent focus-visible ring applied to every interactive
       element app-wide (shadcn: every control shares one `--ring` treatment;
       this app had at least four different ad hoc focus treatments before
       this pass — box-shadow, 2px outline, border-color-only, and nothing
       at all on some controls).
     - The missing Button variants/states: outline (named explicitly; the
       bare .qc-btn was already visually an outline button but had no name
       for it), secondary, disabled, and a loading state with a spinner
       primitive (wired into the three admin account forms below as real,
       working examples — see web/admin-app.js's loginForm/createAdminForm/
       createAdditionalAdminForm submit handlers).
     - Card variants: elevated (modals/popovers), interactive (hover-lift,
       for a card that is itself a click target), info (a quieter,
       accent-edged card for read-only/explanatory content).
     - Form-field polish: consistent height/radius/focus for every text
       input and select sharing the app's existing field wrapper classes,
       plus an opt-in aria-invalid/validation-message primitive (not forced
       onto any field — provided for future use, see the delivery report).
     - A segmented-tab treatment for the existing Quality Alerts tab strip
       (shadcn's TabsList: a muted track, the active tab raised in a solid
       surface) built on the SAME markup/classes, no HTML change.
     - Hover/transition polish for reading rows, audit-timeline rows, and
       the sidebar nav, so clickable rows read as clickable the way
       shadcn's Table row-hover does.
     - A `[data-tooltip]` primitive (shadcn tooltips are driven by a Radix
       primitive this app has no equivalent of; a pure-CSS title-like bubble
       is the vanilla equivalent) — defined but not yet attached to any
       element; available for the next pass.
     - prefers-reduced-motion guards on every animation/transition this
       section adds, including the ones already in this file that lacked one
       (shadcn's own Radix-based primitives respect this by default).
   ============================================================================ */

/* ---- Unified focus-visible ring -------------------------------------------
   One ring, one color, one width, used by every interactive control this
   pass touches. Replaces nothing — elements that already had a bespoke
   focus-visible rule (admin-stat-tile.is-clickable, dashboard-session-row,
   qc-form-cell-summary) keep working exactly as before; this just extends
   the same visual language to everything that had none. */
.qc-btn:focus-visible,
.icon-btn:focus-visible,
.link-btn:focus-visible,
.nav-item:focus-visible,
.modal-close:focus-visible,
.sidebar-user-trigger:focus-visible,
.sidebar-user-popover-item:focus-visible,
.sidebar-collapse-toggle:focus-visible,
.dashboard-alert-tab:focus-visible,
.admin-text-input:focus-visible,
.admin-filter-row input:focus-visible,
.admin-filter-row select:focus-visible,
.dashboard-filter-row input:focus-visible,
.dashboard-filter-row select:focus-visible,
.dashboard-filter-field input:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-color: var(--accent);
}
.nav-item:focus-visible { box-shadow: var(--focus-ring); }

/* ---- Button system completion ---------------------------------------------
   shadcn/ui's Button has six named variants (default, destructive, outline,
   secondary, ghost, link) and a disabled/loading state shared by all of
   them. This app's .qc-btn already covers default (.qc-btn-primary), ghost
   (.qc-btn-ghost), destructive (.qc-btn-danger), and — unnamed — outline
   (the bare .qc-btn). This pass names outline explicitly and adds the two
   that were missing: secondary and loading. .qc-btn-sm already exists
   elsewhere in this file as the compact size variant (shadcn: size="sm"). */
.qc-btn-outline { /* identical to the bare .qc-btn — named for clarity/consistency with the other variant classes, never a behavior change */ }
.qc-btn-secondary {
  background: var(--neutral-soft); border-color: var(--neutral-border); color: var(--ink-900);
}
.qc-btn-secondary:hover { background: var(--ink-200); border-color: var(--border-strong); }

.qc-btn:disabled, .qc-btn[aria-disabled="true"] {
  opacity: 0.55; cursor: not-allowed; pointer-events: none;
}

/* Loading: adds a small spinner in place of the usual hover affordance and
   keeps the label (so the button doesn't change width/jump), same
   "disable + show a spinner, never hide the control" convention shadcn's
   own Button loading state follows. Wired into the three admin
   account-management submit buttons (web/admin-app.js) as real, verified
   examples — see that file's loginForm/createAdminForm/
   createAdditionalAdminForm submit handlers. */
.qc-btn.is-loading, .admin-login-submit.is-loading {
  position: relative; color: transparent !important; pointer-events: none;
}
.qc-btn.is-loading::after, .admin-login-submit.is-loading::after {
  content: ''; position: absolute; top: 50%; left: 50%; width: 16px; height: 16px;
  margin: -8px 0 0 -8px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  color: var(--ink-700); animation: qc-spin 0.6s linear infinite;
}
.qc-btn-primary.is-loading::after, .admin-login-submit.is-loading::after { color: var(--accent-contrast); }
@keyframes qc-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .qc-btn.is-loading::after, .admin-login-submit.is-loading::after { animation-duration: 1.4s; }
}

/* ---- Card variants ---------------------------------------------------------
   shadcn's Card is one structural primitive with the visual weight coming
   from context (a popover/dialog's Card reads "above" a page's own cards).
   This app's .card already covers the base case; these are opt-in modifiers
   for the handful of places that need a different weight. */
.card-elevated { box-shadow: var(--shadow-lg); border-color: var(--border-strong); }
.card-interactive {
  cursor: pointer; transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.card-interactive:hover { box-shadow: var(--shadow-md); border-color: var(--accent-border); transform: translateY(-1px); }
.card-interactive:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.card-info { border-left: 3px solid var(--accent-border); background: var(--accent-soft); }
.card-info h3 { color: var(--accent-dark); }
@media (prefers-reduced-motion: reduce) {
  .card-interactive, .card-interactive:hover { transition: none; transform: none; }
}

/* ---- Form-field polish -----------------------------------------------------
   Consistent radius/transition across every text input and select that
   already shares one of this app's existing field classes/containers —
   no new markup, this only completes states those selectors were missing. */
.admin-text-input,
.admin-filter-row input,
.admin-filter-row select,
.dashboard-filter-row input,
.dashboard-filter-row select,
.dashboard-filter-field input {
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.admin-text-input:disabled { opacity: 0.6; cursor: not-allowed; background: var(--neutral-soft); }

/* Opt-in validation primitive (shadcn Input: aria-invalid drives the error
   ring, never a separate "error" class to keep in sync with real validity).
   Not applied to any field by this pass — available for the next one. */
.admin-text-input[aria-invalid="true"],
.admin-filter-row input[aria-invalid="true"] {
  border-color: var(--danger); box-shadow: 0 0 0 3px rgba(185, 28, 28, 0.14);
}
.form-help-text { font-size: 0.78rem; color: var(--ink-500); margin-top: var(--space-3xs); }
.form-error-text { font-size: 0.78rem; color: var(--danger); margin-top: var(--space-3xs); font-weight: 600; }

/* ---- Segmented tabs (Quality Alerts strip) ---------------------------------
   shadcn's TabsList is a muted track with the active trigger raised onto a
   solid surface + shadow, rather than a solid-accent-filled pill. Same
   .dashboard-alert-tabs/.dashboard-alert-tab markup, no HTML change. */
.dashboard-alert-tabs {
  background: var(--neutral-soft); border: 1px solid var(--neutral-border);
  border-radius: var(--radius-md); padding: 4px; gap: 2px;
}
.dashboard-alert-tab {
  background: transparent; border-color: transparent;
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.dashboard-alert-tab:hover { background: var(--panel); border-color: transparent; color: var(--accent-dark); }
.dashboard-alert-tab.active {
  background: var(--panel); border-color: var(--border); color: var(--ink-900);
  box-shadow: var(--shadow-sm);
}

/* ---- Row hover polish (Reading rows / Audit Trail) -------------------------
   shadcn's Table gives every body row a subtle hover wash so a dense list
   reads as scannable; these rows (.qc-row, .audit-timeline-item) had none. */
.qc-row { transition: border-color 0.15s ease, box-shadow 0.15s ease; }
.audit-timeline-item { transition: background 0.15s ease; border-radius: var(--radius-sm); }
.audit-timeline-item:hover { background: var(--neutral-soft); }
@media (prefers-reduced-motion: reduce) {
  .qc-row, .audit-timeline-item, .nav-item, .nav-icon, .dashboard-alert-tab, .sidebar-user-trigger, .sidebar-user-chevron, .app-shell, .sidebar { transition: none; }
}

/* ---- Dialog entrance (Session Detail / Excel-import modals) ----------------
   shadcn's Dialog fades/scales in rather than appearing instantly. Guarded
   by prefers-reduced-motion per this app's existing accessibility
   convention (see web/voice-orb.js and the state-pill pulse animations). */
@media (prefers-reduced-motion: no-preference) {
  .modal-overlay:not([hidden]) { animation: qc-fade-in 0.15s ease; }
  .modal-overlay:not([hidden]) .modal { animation: qc-modal-in 0.15s ease; }
}
@keyframes qc-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes qc-modal-in { from { opacity: 0; transform: scale(0.98) translateY(4px); } to { opacity: 1; transform: none; } }

/* ---- Tooltip primitive (defined, not yet attached anywhere) ----------------
   shadcn's Tooltip is a Radix popover; the vanilla equivalent here is a
   pure-CSS bubble keyed off a `data-tooltip` attribute, shown on
   hover/focus-visible so it's keyboard-reachable too. Add
   `data-tooltip="…"` to any element to use it — nothing in this pass
   adds that attribute anywhere yet. */
[data-tooltip] { position: relative; }
[data-tooltip]:hover::after, [data-tooltip]:focus-visible::after {
  content: attr(data-tooltip); position: absolute; bottom: calc(100% + 6px); left: 50%;
  transform: translateX(-50%); background: var(--panel-inverse); color: #fff;
  font-size: 0.74rem; font-weight: 600; padding: 4px 8px; border-radius: var(--radius-sm);
  white-space: nowrap; box-shadow: var(--shadow-md); pointer-events: none; z-index: 20;
}
