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

:root {
  /* ── University of Iowa brand palette (DARK — default) ───────────────
     Primary:   Old Gold #FFCD00 + Black #000000
     Surfaces warmed toward black to harmonise with gold.
     Text is warm off-white (gold-tinted) for readability and on-brand feel.
     Semantic colours (green/red/amber) sit aside from gold so warnings
     don't clash with the primary accent.

     Light theme lives in `[data-theme="light"]` below — adding new colour
     literals anywhere outside these two blocks will break theme switching.
     Prefer rgba() blends of the tokens defined here. */
  --bg:        #0d0e10;        /* near-black, faint warm tint */
  --surface:   #1a1b1f;
  --surface2:  #262830;
  --border:    #6e7280;

  /* Primary accent: Old Gold */
  --accent:    #FFCD00;        /* Old Gold */
  --accent-dim:#8a6d00;        /* darker gold for subdued areas */
  --accent-hi: #FFD83A;        /* brighter gold for hover/highlight */
  --accent-ink:#111111;        /* text that sits on gold backgrounds */

  /* Keyboard focus ring (WCAG 2.4.7 / 1.4.11). 12.86:1 on --bg,
     11.46:1 on --surface. */
  --focus: #FFCD00;

  --green:  #4ac68e;
  --red:    #e25a4d;
  --yellow: #e89b2b;           /* amber — distinct from Old Gold */
  /* The Setup ✓ (.nav-badge.complete) and the sidebar brains (.nav-marker).
     1.4.11, measured by axe-core's colour engine: 4.38:1 on --surface, 3.75:1
     on --surface2 (the current / hovered button); #9d00ff was 3.18:1 and
     2.71:1. */
  --purple: #b04dff;
  --error: var(--red);         /* semantic alias for error text/borders */

  /* Ink that sits on the SOLID --green/--red fills (btn-success/danger,
     annotation pins, semantic tag badges). Dark near-black reads far
     better on these mid-tone fills than white did. Per-theme token
     because light theme must keep white ink on its deeper green/red. */
  --green-ink:  #111111;
  --red-ink:    #111111;
  /* Yellow used as PLAIN TEXT (not a tint pill). Same as --yellow on
     dark; the split only bites in light, where --yellow stays a fill. */
  --yellow-text:#e89b2b;

  --text:     #f2eddc;         /* warm off-white */
  --text-dim: #a49f91;
  --text-hint:#9a9486;

  /* Surface-specific tokens.
     The PDF canvas wraps need a neutral non-white backdrop so a white
     PDF page reads as a sheet of paper on a desk; the AI annotation pin
     uses a teal that's distinct from the green positive pin. */
  --pdf-canvas-bg:       #1a1a1a;
  --approval-preview-bg: #0d1117;
  --splash-inner:        #1a1d24;
  --splash-outer:        #0b0d12;
  --annot-pin-ai-bg:     #0ea5a5;
  --shadow:              rgba(0, 0, 0, .5);

  /* Text colour for content sitting on a translucent gold tint (chips,
     alerts, info boxes). On dark surfaces the lightened gold reads;
     on light surfaces we need a deeper gold so the text doesn't
     vanish into a pale-on-pale wash. Same idea for the semantic
     green/yellow/red variants — the raw --green/--yellow/--red are
     tuned for solid-fill buttons, not for text on a 12%-tinted pill. */
  --accent-text-on-tint: #ffe48a;
  --green-text-on-tint:  #7ad9b0;
  --yellow-text-on-tint: #f4c285;
  --red-text-on-tint:    #f08a7e;

  /* Score-distribution axis bars (cluster inspector). Distinct from the
     semantic palette so the bars don't read as gold/yellow/red status
     cues. Purple works on both themes and stays out of the way of the
     status chips that share the row. */
  --axis-band:           #a78bfa;

  /* Chart palette — read by static/views/stats.js at render time and
     re-resolved on `themechange`. Keep these distinct hues; they
     identify grade kinds (manual/AI/handgraded/total). */
  --chart-manual:     #4e9af1;
  --chart-ai:         #1abc9c;
  --chart-handgraded: #e67e22;
  --chart-total:      #9b59b6;
  --chart-grid:       rgba(255, 255, 255, .08);
  --chart-text:       #a49f91;

  --radius: 8px;
  --font:   "Inter", system-ui, sans-serif;

  /* Mirrored from JS by applyUiScale() / the inline boot cache script.
     Display scale is CSS `zoom` on <html>, and `vh` resolves against the
     UNZOOMED viewport, so a raw viewport height is wrong by
     100vh x (1 - scale) at every scale but exactly 1. Default 1 (no zoom)
     covers users who never touch the text-size adjuster. */
  --ui-zoom: 1;
  /* The corrected viewport height. Every height rule outside a breakpoint
     block reads from here rather than from `vh` directly: hand-copying the
     division is how two rules were missed the first time this was fixed and
     five more were never converted at all. */
  --vh100: calc(100vh / var(--ui-zoom, 1));
}

[data-theme="ugraid"] {
  /* ── uGraid base palette ─────────────────────────────────────────────
     Placeholder — duplicates the dark (Iowa) palette verbatim. The real
     uGraid colour tokens are TBD; override the values below in place
     when they're ready. Keep the same token names so the rest of the
     stylesheet doesn't need to change. */
  --bg:        #0d0e10;
  --surface:   #1a1b1f;
  --surface2:  #262830;
  --border:    #6e7280;

  --accent:    #FFCD00;
  --accent-dim:#8a6d00;
  --accent-hi: #FFD83A;
  --accent-ink:#111111;

  --focus: #FFCD00;

  --green:  #4ac68e;
  --red:    #e25a4d;
  --yellow: #e89b2b;
  --purple: #b04dff;

  --green-ink:  #111111;
  --red-ink:    #111111;
  --yellow-text:#e89b2b;

  --text:     #f2eddc;
  --text-dim: #a49f91;
  --text-hint:#9a9486;

  --pdf-canvas-bg:       #1a1a1a;
  --approval-preview-bg: #0d1117;
  --splash-inner:        #1a1d24;
  --splash-outer:        #0b0d12;
  --annot-pin-ai-bg:     #0ea5a5;
  --shadow:              rgba(0, 0, 0, .5);

  --accent-text-on-tint: #ffe48a;
  --green-text-on-tint:  #7ad9b0;
  --yellow-text-on-tint: #f4c285;
  --red-text-on-tint:    #f08a7e;

  --axis-band:           #a78bfa;

  --chart-manual:     #4e9af1;
  --chart-ai:         #1abc9c;
  --chart-handgraded: #e67e22;
  --chart-total:      #9b59b6;
  --chart-grid:       rgba(255, 255, 255, .08);
  --chart-text:       #a49f91;
}

[data-theme="light"] {
  /* ── Iowa-brand LIGHT palette ────────────────────────────────────────
     Old Gold deepened to #8a6d00 so it carries enough contrast on white
     (≥4.5:1 as text; WCAG 1.4.3).
     Semantic colours (green/red/amber) darkened from their dark-mode
     counterparts — the 4ac68e / e25a4d / e89b2b values used on dark
     surfaces wash out on white. */
  --bg:        #faf8f2;        /* warm off-white */
  --surface:   #ffffff;
  --surface2:  #f1ede2;        /* warm cream for raised surfaces */
  --border:    #8f887a;

  --accent:    #806400;        /* deepened Old Gold (≥4.5:1 as text on white) */
  --accent-dim:#8a6d00;
  --accent-hi: #6f5800;        /* hover: slightly darker, not lighter */
  --accent-ink:#ffffff;        /* white text on deep gold */

  /* Keyboard focus ring (WCAG 2.4.7 / 1.4.11). 4.92:1 on white,
     4.63:1 on --bg. */
  --focus: #8a6d00;

  --green:  #1f7a4d;
  --red:    #c0392b;
  --yellow: #b07a1d;
  /* Deepened like the rest of this theme's palette: #9d00ff is 3.02:1 on
     white, which reads thin for a 18px glyph. #7a00c8 carries at 6.34:1. */
  --purple: #7a00c8;

  /* Light theme keeps WHITE ink on its deeper green/red fills — #111
     would fail (3.55/3.47). This is why the ink swap is a per-theme
     token, not a literal. Yellow-as-text deepens to carry on white. */
  --green-ink:  #ffffff;
  --red-ink:    #ffffff;
  --yellow-text:#8a5f00;

  --text:     #1a1814;         /* near-black, warm tint */
  --text-dim: #5a5448;
  --text-hint:#716b5c;

  --pdf-canvas-bg:       #e8e6df;
  --approval-preview-bg: #e8e6df;
  --annot-pin-ai-bg:     #0e7878;
  --shadow:              rgba(0, 0, 0, .12);

  --accent-text-on-tint: #6b4d00;
  --green-text-on-tint:  #0d4d2d;
  --yellow-text-on-tint: #5e3f00;
  --red-text-on-tint:    #7a1f12;

  --axis-band:           #6f3fa8;

  --chart-manual:     #2a6fc4;
  --chart-ai:         #10795f;   /* was #138a72 (4.03:1 as text) — darkened to
                                    clear AA where the stats "kind" column
                                    renders the label in the series color;
                                    still a valid chart mark (>4.5:1 vs bg). */
  --chart-handgraded: #a8500f;   /* was #b85a14 (4.38:1 as text) — same reason. */
  --chart-total:      #6f3f8a;
  --chart-grid:       rgba(0, 0, 0, .08);
  --chart-text:       #5a5448;
}

/* ─── .btn-unstyled — render a real <button> like the div/span it replaced ──
   MUST stay BEFORE the component style sections below. Component rules have
   the same specificity (a single class) and appear later in source, so they
   WIN for every property they set (padding, border, display:flex, font-size,
   width). This utility therefore only strips the UA <button> chrome that the
   components don't set. If this block moved BELOW the components its resets
   would clobber their styling and break pixel-parity (WP-6 / Pattern P4).
   Two variants — pick by the element's ORIGINAL display:
     .btn-unstyled          former block element that filled its container
     .btn-unstyled--inline  former inline span, absolute pin, or flex child */
.btn-unstyled,
.btn-unstyled--inline {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
}
.btn-unstyled         { display: block;  width: 100%; }
.btn-unstyled--inline { display: inline; width: auto; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  min-height: var(--vh100);
}

/* ─── Theme toggle (settings modal) ──────────────────────────────────── */
.theme-toggle {
  display: inline-flex;
  gap: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface2);
}
.theme-toggle-opt {
  position: relative;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  user-select: none;
}
.theme-toggle-opt input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.theme-toggle-opt span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  font-size: 13px;
  color: var(--text-dim);
  transition: background .12s ease, color .12s ease;
}
.theme-toggle-opt input:checked + span {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 500;
}
.theme-toggle-opt:hover input:not(:checked) + span {
  color: var(--text);
}

.ui-scale-control {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.ui-scale-control .btn-sm {
  min-width: 32px;
  padding: 4px 10px;
  font-size: 16px;
  font-weight: 600;
}
.ui-scale-readout {
  display: inline-block;
  min-width: 56px;
  text-align: center;
  font-family: ui-monospace, monospace;
  font-size: 13px;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface2);
  color: var(--text);
}

/* ─── Layout ────────────────────────────────────────────────────────── */
.app-shell {
  display: grid;
  grid-template-rows: 56px 1fr;
  grid-template-columns: 220px 1fr;
  height: var(--vh100);
}

.topbar {
  grid-column: 1 / -1;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  padding: 0 24px;
  gap: 12px;
}

.topbar h1 {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.3px;
}

.topbar h1.topbar-logo {
  cursor: pointer;
  user-select: none;
  transition: opacity .15s ease;
  display: inline-flex;
  align-items: center;
  line-height: 1;
}
.topbar h1.topbar-logo:hover { opacity: .75; }
.topbar h1.topbar-logo img {
  display: block;
  height: 64px;
  width: auto;
}

/* Theme-driven logo (BUG-001), resolved pre-JS by the inline <head> script's
   data-theme so the correct asset paints on the first frame — no dark flash.
   Default markup src is the dark file (correct for dark / no attribute).
   _applyBranding() in boot.js stays as a JS fallback for engines that ignore
   content:url() on <img>; its THEME_LOGOS names the same files. The topbar's
   data-theme-logo="mark" takes the tagline-free wordmark (#590); the
   [data-theme-logo="mark"] rules come second so they win at equal
   specificity. */
:root[data-theme="light"]  img[data-theme-logo] { content: url("/static/img/logo-light.svg"); }
:root[data-theme="ugraid"] img[data-theme-logo] { content: url("/static/img/logo-dark.svg"); }
:root[data-theme="light"]  img[data-theme-logo="mark"] { content: url("/static/img/logo-light-notagline.svg"); }
:root[data-theme="ugraid"] img[data-theme-logo="mark"] { content: url("/static/img/logo-dark-notagline.svg"); }

.topbar h1 span { color: var(--accent); }

.topbar .spacer { flex: 1; }

.topbar .tag {
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 20px;
  background: var(--surface2);
  color: var(--text-dim);
  border: 1px solid var(--border);
}

.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 16px 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow-y: auto;
}

.nav-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 20px;
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 13.5px;
  font-family: inherit;
  border-radius: 0;
  transition: background .15s, color .15s;
  width: 100%;
  text-align: left;
  position: relative;
}

.nav-btn .icon { font-size: 16px; width: 20px; text-align: center; }
/* The label takes the room, so a step's state mark sits at the right edge in
   one column (07-sidebar mock-up) and a long label wraps beside it. */
.nav-btn .nav-label { flex: 1; min-width: 0; }

.nav-btn:hover { background: var(--surface2); color: var(--text); }

.nav-btn.active {
  background: var(--surface2);
  color: var(--accent);
}

.nav-btn.active::before {
  content: "";
  position: absolute;
  left: 0; top: 6px; bottom: 6px;
  width: 3px;
  background: var(--accent);
  border-radius: 0 3px 3px 0;
}

/* Sidebar nav count/status pill. Shared by three owners, so it lives here with
   the rest of the nav chrome rather than under any one view:
     - #nav-approvals-badge   (templates/index.html, driven by views/approvals.js)
     - nav-submissions        (views/submissions.js, via core/dom.js#updateBadge)
     - nav-setup              (views/setup.js "✓", via the same helper)
   This used to be declared twice — a gold --accent-dim pill here and a red
   re-declaration ~3100 lines later filed under "Approvals view". Equal
   specificity meant source order won, so the red rule painted every badge in
   the app and the gold one was unreachable. Collapsed to the one that was
   actually rendering; appearance is unchanged. */
.nav-badge {
  display: inline-block;
  min-width: 18px;
  padding: 1px 6px;
  margin-left: 6px;
  border-radius: 999px;
  background: var(--red, #d73a49);
  color: var(--red-ink);
  font-size: 11px;
  font-weight: 700;
  text-align: center;
  vertical-align: middle;
}

/* Completion state (nav-setup "✓") is an icon, not a pill: the backdrop, the
   padding and the min-width all go, leaving the glyph alone in the sidebar.
   Scoped to a variant class rather than restyling .nav-badge, because the base
   class is shared with the numeric pending-count badge on nav-submissions —
   recolouring a *count* purple would be semantically wrong.
   `color` is the ONLY place the purple is set: the inlined SVG is
   fill="currentColor", so this one declaration themes it in every mode. */
.nav-badge.complete {
  background: transparent;
  color: var(--purple);
  min-width: 0;
  padding: 0;
  border-radius: 0;
}
.nav-badge.complete .nav-badge-icon {
  width: 18px;
  height: 18px;
  display: block;
}

/* The sidebar's step states (core/dom.js: updateBadge, updateMarker,
   placeNextStep), one per step at a time, all in the ✓'s purple:
     done      the ✓ (above);
     working   the brain turning on the stylesheet's one rotation keyframe
               until the step ends;
     needs you the exclamation mark whose dot is a small brain (owner,
               2026-11-01: a tapered OUTLINE stroke over a small brain), with
               the count pill beside it. It waits one settle delay, then shakes
               ONCE for .8 s ("a single shake any time the view changes. No
               recurrence until the user navigates to a new view", UX-W3 §1.2);
               the helper builds a fresh mark only when the state is entered
               again or its count changes, and navigate() replays the shake
               when the view in focus changes (dom.js replayAttention);
     next      the hollow brain (the duotone glyph without its fill layer),
               still — one per sidebar;
     later     nothing.
   A brain outranks the ✓, which stays in the markup (it is still "done" for
   placing the next mark) but is not drawn beside it. The reduced-motion
   blanket stops both motions, which is why no rule here may claim importance
   over it; the per-browser "Pause automatic list refresh" preference
   (html.motion-paused, the app's 2.2.2 control) stops the shake too.
   justifications: SIDEBAR-ATTENTION §1.1; UX-W3 §1.1, §1.2 */
.nav-marker {
  --nav-marker-settle: 1s;
  display: inline-flex;
  margin-left: 6px;
  color: var(--purple);
  font-size: 16px;
  line-height: 1;
}
.nav-marker-icon { display: inline-block; }
.nav-marker.attention .nav-bang {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  animation: nav-marker-shake .8s ease-in-out var(--nav-marker-settle) 1;
}
:root.motion-paused .nav-marker .nav-bang { animation: none; }
/* The mark is 26 px tall; it lays out as the brain's 16 px so a row never
   grows (and nothing below it moves) when something starts waiting. */
.nav-marker.attention { margin-block: -5px; }
.nav-bang-stroke { display: block; width: 12px; height: 16px; }
.nav-bang-stroke path { fill: none; stroke: currentColor; stroke-width: 1.7; }
.nav-bang .nav-marker-icon { font-size: 9px; }
.nav-marker.busy .nav-marker-icon {
  animation: spin 1.6s linear infinite;
}
/* One .8 s burst: three wags, then still. */
@keyframes nav-marker-shake {
  0%, 100%      { transform: translateX(0) rotate(0); }
  10%, 43%, 76% { transform: translateX(-2px) rotate(-10deg); }
  26%, 59%, 92% { transform: translateX(2px) rotate(10deg); }
}
.nav-marker ~ .nav-badge.complete { display: none; }
.nav-next {
  display: inline-flex;
  margin-left: 6px;
  color: var(--purple);
  font-size: 16px;
  line-height: 1;
}
.nav-next-icon::before { opacity: 0; }

/* The scope header above the steps: what they are for. Plain text; long
   names wrap rather than being cut off. */
.nav-scope {
  padding: 10px 16px 4px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.nav-scope-name {
  display: block;
  margin-top: 2px;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* The foot's signed-in name: a label styled as an entry, never a control. */
.nav-btn.sidebar-user { cursor: default; font-weight: 600; color: var(--text); overflow-wrap: anywhere; }
.nav-btn.sidebar-user:hover { background: none; }

.stats-tab-bar {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
}

.stats-tab-btn {
  padding: 5px 14px;
  background: var(--surface2);
  border: none;
  border-radius: 6px;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 13px;
  font-family: inherit;
  transition: background .15s, color .15s;
}

.stats-tab-btn:hover { background: var(--surface3, var(--surface2)); color: var(--text); }

.stats-tab-btn.active {
  /* Gold-tinted "selected" pill. The former solid --accent-dim fill with
     --accent text failed AA (3.27:1 dark, 1.14:1 light — the light --accent
     is nearly the same hue as --accent-dim). Route to the accent-tint
     background + accent-text-on-tint pattern used by the app's other active
     chips (wiz-phase-chip.active, tag-*): 7.5:1 dark/ugraid, 5.3:1 light. */
  background: color-mix(in srgb, var(--accent) 18%, var(--surface2));
  color: var(--accent-text-on-tint);
  font-weight: 600;
}

.main-content {
  overflow-y: auto;
  padding: 28px 32px;
}

.view { display: none; }
.view.active { display: block; }

/* ─── Components ────────────────────────────────────────────────────── */
.section-title {
  font-size: 18px;
  font-weight: 600;
  margin-bottom: 6px;
}

.section-sub {
  color: var(--text-dim);
  margin-bottom: 20px;
  font-size: 13px;
}

/* Cards */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  margin-bottom: 16px;
}

.card-title {
  font-weight: 600;
  margin-bottom: 12px;
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Buttons */
.btn {
  padding: 8px 16px;
  border-radius: 6px;
  border: none;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: opacity .15s, filter .15s;
}

.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn:not(:disabled):hover { filter: brightness(1.1); }

.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}
.btn-primary:not(:disabled):hover { background: var(--accent-hi); filter: none; }
.btn-success { background: var(--green); color: var(--green-ink); font-weight: 600; }
.btn-danger  { background: var(--red);   color: var(--red-ink); }
.btn-ghost   { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
.btn-sm      { padding: 5px 10px; font-size: 12px; }
/* De-emphasised text INSIDE a button takes the button's own ink, never the
   page's dim token. --text-dim is calibrated against the page surface; on a
   filled gold or red button it lands at 1.3–1.8:1, which is unreadable. Each
   button variant already pairs its background with an ink calibrated against
   it, so inheriting is correct for every variant and every theme at once —
   and the token stays untouched, so none of its many other consumers move.
   The de-emphasis itself survives in the weight difference: these spans set
   font-weight 400 against the button's 600. */
.btn .text-dim { color: inherit; }
/* The rubric picker's two clickable rows: the criterion disclosure header and
   each common-error button under it. Both rendered 21px tall against the 24px
   pointer-target floor (WCAG 2.2 SC 2.5.8). min-height only — `summary` keeps
   its default display so the disclosure marker survives, and the buttons keep
   their full-width row. The rubric view's Errors / Awards tabs take the same
   floor: unstyled buttons, they relied on the spacing exception until each
   panel gained its own Generate button directly under the tab strip (#703).
   min-height only — the shared inline-flex floor below would collapse the
   space between a tab's label and its badge. */
.pick-criterion > summary { min-height: 24px; padding: 2px 0; }
.pick-error-btn,
.rubric-channel-tab       { min-height: 24px; }

/* Force-regenerate / other irreversible actions: red text on the ghost
   background so the affordance is unambiguous without becoming a giant
   red button — those tend to attract clicks. */
.rubric-danger-btn {
  /* Readable-red text token: --red on --surface2 is only 4.06:1 (dark/ugraid).
     --red-text-on-tint clears AA (6.05:1 dark) while staying in the red family.
     Border keeps --red (non-text, meets 3:1). */
  color: var(--red-text-on-tint);
  border-color: color-mix(in srgb, var(--red) 30%, var(--border));
}
.rubric-danger-btn:hover {
  background: color-mix(in srgb, var(--red) 10%, var(--surface2));
}

/* Upload zone */
.upload-zone {
  border: 2px dashed var(--border);
  border-radius: var(--radius);
  padding: 36px 24px;
  text-align: center;
  cursor: pointer;
  transition: border-color .2s, background .2s;
}

.upload-zone:hover, .upload-zone.drag-over {
  border-color: var(--accent);
  background: rgba(255, 205, 0,.06);
}

.upload-zone .upload-icon { font-size: 32px; margin-bottom: 10px; }
.upload-zone p { color: var(--text-dim); font-size: 13px; }
/* P6: the file input is hidden but stays focusable (was `display: none`,
   which removed it from the focus order and made upload keyboard-impossible).
   Tab reaches it; Enter/Space opens the native picker (built-in file-input
   behavior — no JS needed). setupDragZone's zone.onclick still proxies mouse
   clicks to input.click(), and drag-and-drop is untouched. */
.upload-zone { position: relative; }
.upload-zone input[type=file] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  overflow: hidden;
  clip-path: inset(50%);
}
/* Keyboard focus on the input lights the whole zone, mirroring the existing
   hover/drag-over affordance plus the P2 ring. */
.upload-zone:has(input[type=file]:focus-visible) {
  border-color: var(--accent);
  background: rgba(255, 205, 0, .06);
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Tags / chips */
.chip {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 20px;
  font-size: 11px;
  font-weight: 500;
}

/* Chips use a light tinted background (10% mix with the surface) and
   darken the *text* via the semantic-text-on-tint tokens — those resolve
   to a brighter shade in dark mode and a deeper shade in light mode so
   the label always reads against the pale tint. Earlier revisions
   pushed the background opacity higher, which produced yellow-on-yellow
   in light mode. */
.chip-blue   { background: color-mix(in srgb, var(--accent) 14%, var(--surface2));
               color: var(--accent-text-on-tint); }
.chip-green  { background: color-mix(in srgb, var(--green) 12%, var(--surface2));
               color: var(--green-text-on-tint); }
.chip-yellow { background: color-mix(in srgb, var(--yellow) 12%, var(--surface2));
               color: var(--yellow-text-on-tint); }
.chip-red    { background: color-mix(in srgb, var(--red) 12%, var(--surface2));
               color: var(--red-text-on-tint); }

/* AI self-assessed confidence badge — appears on submission cards next
   to the grade chip. Tone (high/mid/low) is picked from the 0-5 score
   in the JS render path. The star glyph sits inside the badge so it
   reads as one unit even at small sizes. */
.confidence-badge {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px 7px;
  border-radius: 20px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  border: 1px solid transparent;
  cursor: help;
}
.confidence-badge .confidence-star {
  font-size: 10px;
  margin-right: 1px;
}
.confidence-high { background: rgba(74, 198, 142,.18); color: var(--green-text-on-tint);  border-color: rgba(74, 198, 142,.4); }
.confidence-mid  { background: rgba(232, 155, 43,.18); color: var(--yellow-text-on-tint); border-color: rgba(232, 155, 43,.4); }
.confidence-low  { background: rgba(226, 90, 77,.18);  color: var(--red-text-on-tint);    border-color: rgba(226, 90, 77,.4); }

/* Progress / spinner */
.spinner {
  width: 20px; height: 20px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .7s linear infinite;
  display: inline-block;
  vertical-align: middle;
  margin-right: 8px;
}

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

.loading-row {
  display: flex;
  align-items: center;
  color: var(--text-dim);
  font-size: 13px;
  padding: 12px 0;
}

/* Alerts */
.alert {
  padding: 12px 16px;
  border-radius: var(--radius);
  font-size: 13px;
  margin-bottom: 14px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

.alert-info    { background: rgba(255, 205, 0,.12); border: 1px solid rgba(255, 205, 0,.3); color: var(--accent-text-on-tint); }
.alert-success { background: rgba(74, 198, 142,.1); border: 1px solid rgba(74, 198, 142,.3); color: var(--green-text-on-tint); }
.alert-error   { background: rgba(226, 90, 77,.1); border: 1px solid rgba(226, 90, 77,.3); color: var(--red-text-on-tint); }

/* The one notice region (#661, core/dom.js flash). Fixed over the page so a
   notice arriving or leaving moves nothing; dom.js picks the corner
   (data-corner) that covers neither the focused control nor the pointer.
   Above dialogs (their notices show over them), below the offline overlay.
   The region itself takes no pointer events; only its notices do. Each
   notice is opaque: the tint is laid over the page colour, so its ink keeps
   the contrast it was measured at inline. */
.notice-tray {
  position: fixed;
  z-index: 1200;
  top: 64px;
  right: 16px;
  width: min(440px, calc(100vw - 32px));
  max-height: calc(var(--vh100) * 0.45);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
}
.notice-tray[data-corner$="left"]   { right: auto; left: 16px; }
.notice-tray[data-corner^="bottom"] { top: auto; bottom: 16px; }
.notice-tray > div { display: contents; }
.notice-tray .alert {
  pointer-events: auto;
  margin: 0;
  background-color: var(--bg);
  box-shadow: 0 4px 16px var(--shadow);
}
.notice-tray .alert-info    { background-image: linear-gradient(rgba(255, 205, 0,.12), rgba(255, 205, 0,.12)); }
.notice-tray .alert-success { background-image: linear-gradient(rgba(74, 198, 142,.1), rgba(74, 198, 142,.1)); }
.notice-tray .alert-error   { background-image: linear-gradient(rgba(226, 90, 77,.1), rgba(226, 90, 77,.1)); }
.notice-tray .alert-error   .alert-body::before { content: "Error: ";   font-weight: 700; }
.notice-tray .alert-warning .alert-body::before { content: "Warning: "; font-weight: 700; }

/* Dismiss button sits flush against the right edge of any flash alert. It
   takes the alert's own ink undimmed: an opacity dim took the x below 4.5:1,
   as low as 2.55:1 (#626); the ink is 5.41:1 or better on every ground in
   every theme. Its hover is shared with .rubric-stale-dismiss. */
.alert-body { flex: 1; min-width: 0; word-wrap: break-word; }
.alert-close {
  background: none;
  border: none;
  color: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
  margin-left: 8px;
  flex-shrink: 0;
}

/* Tags list */
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }

/* ─── Submissions view ──────────────────────────────────────────────── */
.cluster-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  padding: 8px 10px;
  background: var(--surface);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}

.cluster-group {
  margin-bottom: 24px;
  padding: 12px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  transition: border-color .15s, background .15s;
}
.cluster-group.drop-target {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.cluster-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
  font-weight: 600;
  font-size: 14px;
}

.cluster-label-edit {
  flex: 1;
  min-width: 140px;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text);
  font-weight: 600;
  font-size: 13px;
  padding: 4px 6px;
  border-radius: 4px;
}
.cluster-label-edit:hover { border-color: var(--border); }
.cluster-label-edit:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--surface2);
}
.cluster-label-edit:disabled { color: var(--text-dim); }

.cluster-desc-edit {
  width: 100%;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.4;
  padding: 6px 8px;
  margin: 0 0 10px;
  border-radius: 4px;
  resize: vertical;
  font-family: inherit;
}
.cluster-desc-edit:hover { border-color: var(--border); }
.cluster-desc-edit:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--surface2);
  color: var(--text);
}

.cluster-del-btn,
.cluster-edit-btn {
  background: transparent;
  border: none;
  color: var(--text-dim);
  width: 24px;
  height: 24px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  opacity: 0;
  transition: opacity .15s, color .15s, background .15s;
}
.cluster-group:hover .cluster-del-btn,
.cluster-group:hover .cluster-edit-btn,
.cluster-group:focus-within .cluster-del-btn,
.cluster-group:focus-within .cluster-edit-btn,
.cluster-del-btn:focus-visible,
.cluster-edit-btn:focus-visible { opacity: 1; }
.cluster-del-btn:hover {
  color: var(--red);
  background: color-mix(in srgb, var(--red) 15%, var(--surface2));
}
.cluster-edit-btn:hover {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 15%, var(--surface2));
}

.cluster-desc-text {
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.4;
  padding: 0 8px 8px;
  white-space: pre-wrap;
}
.cluster-desc-empty { display: none; }

.submission-card[draggable="true"] { cursor: grab; }
.submission-card.dragging {
  opacity: 0.5;
  cursor: grabbing;
}

.submissions-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 12px;
}

.submission-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
  cursor: pointer;
  transition: border-color .15s, background .15s;
}

.sub-delete-btn {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 22px;
  height: 22px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 22px;
  text-align: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity .15s, background .15s, color .15s;
}
.submission-card:hover .sub-delete-btn,
.submission-card:focus-within .sub-delete-btn,
.sub-delete-btn:focus-visible { opacity: 1; }
.sub-delete-btn:hover {
  background: color-mix(in srgb, var(--red) 15%, var(--surface2));
  color: var(--red);
}

.submission-card:hover {
  border-color: var(--accent);
  background: var(--surface2);
}

.submission-card.graded  { border-left: 3px solid var(--green); }
.submission-card.pending { border-left: 3px solid var(--yellow); }
.submission-card.auto    { border-left: 3px solid var(--accent); }
.submission-card.failed  { border-left: 3px solid var(--red); }

/* Pre-analysis states — opaque-but-reading visually, click disabled.  */
.submission-card.awaiting-approval {
  border-left-color: var(--yellow);
  background: color-mix(in srgb, var(--yellow, #bf8700) 5%, var(--surface));
}
.submission-card.analyzing,
.submission-card.grading {
  border-left-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 5%, var(--surface));
  pointer-events: none;
  opacity: 0.85;
}
/* Delete button + action buttons should still work on in-flight cards */
.submission-card.analyzing .sub-delete-btn,
.submission-card.analyzing .sub-retry-btn,
.submission-card.analyzing .sub-kickback-btn,
.submission-card.analyzing .sub-inspect-btn,
.submission-card.grading .sub-delete-btn,
.submission-card.grading .sub-retry-btn,
.submission-card.grading .sub-kickback-btn,
.submission-card.grading .sub-inspect-btn {
  pointer-events: auto;
}

.inline-spinner {
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-left: 4px;
  border: 2px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: inline-spinner-rotate 0.8s linear infinite;
  vertical-align: -1px;
}
@keyframes inline-spinner-rotate {
  to { transform: rotate(360deg); }
}

/* Kick-back button sits alongside the retry button; same visual family. */
.sub-kickback-btn {
  position: absolute;
  top: 6px;
  right: 54px;
  width: 22px;
  height: 22px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 22px;
  text-align: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity .15s, background .15s, color .15s;
}
.submission-card:hover .sub-kickback-btn,
.submission-card:focus-within .sub-kickback-btn,
.sub-kickback-btn:focus-visible { opacity: 1; }
.sub-kickback-btn:hover {
  background: color-mix(in srgb, var(--yellow, #bf8700) 20%, var(--surface2));
  color: var(--yellow-text-on-tint);
}
/* Make it louder when the analysis stage auto-flagged residual PII. */
.submission-card.pii .sub-kickback-btn {
  opacity: 1;
  color: var(--red);
  background: color-mix(in srgb, var(--red) 12%, var(--surface2));
}
.submission-card.pii .sub-kickback-btn:hover {
  background: color-mix(in srgb, var(--red) 25%, var(--surface2));
}

.sub-retry-btn {
  position: absolute;
  top: 6px;
  right: 30px;
  width: 22px;
  height: 22px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 22px;
  text-align: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity .15s, background .15s, color .15s;
}
.submission-card:hover .sub-retry-btn,
.submission-card:focus-within .sub-retry-btn,
.sub-retry-btn:focus-visible { opacity: 1; }
.sub-retry-btn:hover {
  background: color-mix(in srgb, var(--accent) 15%, var(--surface2));
  color: var(--accent);
}
.sub-retry-btn.failed {
  opacity: 1;
  color: var(--red);
  background: color-mix(in srgb, var(--red) 12%, var(--surface2));
}
.sub-retry-btn.failed:hover {
  background: color-mix(in srgb, var(--red) 25%, var(--surface2));
}
/* #788: the Submissions page's "Show only" filter. A filtered-out card or an
   emptied category is `hidden`, which these rules' own display would beat. */
.subs-filter {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  margin: 4px 0 12px;
}
.subs-filter label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
.submission-card[hidden],
.cluster-group[hidden] { display: none; }

/* #786: an approved paper's Re-analyze is offered with its reason, not run. */
.sub-retry-btn[aria-disabled="true"],
.sub-retry-btn[aria-disabled="true"]:hover {
  cursor: not-allowed;
  background: transparent;
  color: var(--text-dim);
}

.sub-filename {
  font-weight: 500;
  font-size: 13px;
  margin-bottom: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 1.4.13: reserve room for the hover/focus-revealed card action row so it
   never obscures the filename and nothing shifts on reveal. The four
   absolutely-positioned buttons span 100px from the card's right edge
   (right: 6/30/54/78px, each 22px wide); the card has 14px padding. */
.submission-card .sub-filename { padding-right: 86px; }

/* #772: why a paper's analysis or redaction stopped, as text on its card. */
.sub-error-reason {
  font-size: 12px;
  color: var(--text);
  margin: 0 0 8px;
  overflow-wrap: anywhere;
}
.sub-desc {
  font-size: 12px;
  color: var(--text-dim);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-bottom: 8px;
}

.student-name-tag {
  display: inline-block;
  font-size: 11px;
  font-weight: 500;
  color: var(--accent-text-on-tint);
  background: rgba(255, 205, 0, 0.10);
  border: 1px solid rgba(255, 205, 0, 0.28);
  border-radius: 4px;
  padding: 1px 6px;
  margin-bottom: 5px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ─── Grading view ──────────────────────────────────────────────────── */
/* UX wave 3 (owner rulings 2026-11-01): the paper is the primary canvas. No
   toolbar row and no page padding: a narrow tool column | the paper, from the
   header to the bottom of the screen | the grading panel. */
.main-content:has(> #view-grade.active) { padding: 0; overflow: hidden; }
#view-grade.active { height: 100%; }
.grade-layout {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr) 360px;
  height: 100%;
}

/* The tool column: groups of rows, small-caps group headers. */
.grade-col {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 8px;
  /* Not a scroll box: the ⋯ menu opens beside the column, over the paper. */
  overflow: visible;
  background: var(--surface);
  border-right: 1px solid var(--border);
}
.grade-col-grp { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.grade-col-h {
  align-self: flex-start;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.grade-col-row { display: flex; align-items: center; justify-content: center; gap: 2px; }
.grade-col-pos {
  min-width: 40px;
  font-size: 12px;
  color: var(--text-dim);
  text-align: center;
  white-space: nowrap;
}
.grade-col-ib { min-width: 32px; min-height: 32px; padding: 4px 6px; justify-content: center; }
.grade-col-ib i, .grade-col-wide i { font-size: 17px; }
.grade-col-stack { display: flex; flex-direction: column; gap: 4px; }
.grade-col-wide { width: 100%; min-height: 32px; justify-content: flex-start; gap: 6px; }
.grade-col-rule { height: 1px; flex-shrink: 0; background: var(--border); }
.grade-col .grade-sub-select { width: 100%; font-weight: 600; }
/* The tool in use: filled and outlined, not colour alone. */
.pdf-tool-btns [aria-pressed="true"] {
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: inset 0 0 0 2px var(--text);
}
/* A shortcut whose section cannot be used here: greyed, and still focusable
   so its reason (the tooltip) is reachable. */
.grade-col-wide[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }

/* The ⋯ menu opens beside the column, over the paper. */
.grade-more { position: relative; }
.grade-more > .btn { justify-content: center; }
.grade-more > .btn i { font-size: 20px; }
.grade-more-menu {
  position: absolute;
  left: calc(100% + 6px);
  top: 0;
  z-index: 70;
  min-width: 270px;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px var(--shadow);
}
.grade-more-menu[hidden], .grade-more-menu [hidden] { display: none; }
.grade-more-menu [role="menuitem"] {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 32px;
  padding: 6px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.grade-more-menu [role="menuitem"]:hover,
.grade-more-menu [role="menuitem"]:focus-visible { background: var(--surface2); }
.grade-more-menu [role="menuitem"]:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--focus) inset; }
.grade-more-menu [role="menuitem"][aria-disabled="true"],
.grade-more-menu [role="menuitem"]:disabled { color: var(--text-dim); cursor: not-allowed; }
.grade-more-menu .grade-menu-why { margin-left: auto; font-size: 11px; color: var(--text-dim); }
.grade-more-menu [role="separator"] { height: 1px; margin: 4px 2px; background: var(--border); }

.pdf-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  background: var(--surface);
}

.pdf-filename {
  font-size: 13px;
  font-weight: 500;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pdf-page-info { font-size: 12px; color: var(--text-dim); }

.grade-sub-select {
  flex: 1;
  max-width: 420px;
  min-width: 0;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--text);
  padding: 5px 8px;
  font-family: inherit;
  font-size: 13px;
}

.pdf-canvas-wrap {
  flex: 1;
  overflow: auto;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 16px;
  background: var(--pdf-canvas-bg);
}
.pdf-canvas-wrap:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.pdf-canvas-wrap.pan-mode, .pdf-canvas-wrap.pan-mode #pdf-canvas { cursor: grab; }
.pdf-canvas-wrap.panning, .pdf-canvas-wrap.panning #pdf-canvas { cursor: grabbing; user-select: none; }

/* The ring of mark types, centred on the clicked spot: each choice always in
   the same direction (Praise above, Note right, Deduct below). Its middle is
   empty and lets a click through, so a second click can move the spot. */
.grade-mark-ring {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 60;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-rows: 1fr 28px 1fr;
  gap: 4px;
  pointer-events: none;
}
.grade-mark-ring[hidden] { display: none; }
.grade-ring-item {
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 24px;
  min-height: 28px;
  padding: 3px 10px;
  border: 2px solid rgba(255,255,255,0.35);
  border-radius: 14px;
  font: 600 12px/1 inherit;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0,0,0,0.55);
}
.grade-ring-n { grid-area: 1 / 2; align-self: end; }
.grade-ring-e { grid-area: 2 / 3; justify-self: start; }
.grade-ring-s { grid-area: 3 / 2; align-self: start; }

/* One mark's panel, dropped below the pending pin (placed by Floating UI);
   shown by showModal as a flex column. */
.grade-mark-panel {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 70;
  width: min(360px, calc(100vw - 16px));
  max-height: calc(var(--vh100) - 16px);
  overflow-y: auto;
  flex-direction: column;
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 28px var(--shadow);
}

/* The rubric line box stands for the select, which is never shown. */
#annot-rubric-pick-row { display: none; }
/* A mark made without a spot (the ⋯ menu): the panel is centred. */
.grade-mark-panel.grade-mark-unplaced {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
.annot-line-list {
  list-style: none;
  margin: 4px 0 0;
  padding: 2px;
  max-height: 180px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--surface);
}
.annot-line-list [role="option"] {
  min-height: 24px;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  cursor: pointer;
}
.annot-line-list [aria-selected="true"] { background: var(--accent); color: var(--accent-ink); }
.annot-line-new { font-weight: 600; }
.annot-newline { margin-top: 8px; }
.annot-newline[hidden], .annot-newline [hidden] { display: none; }

#pdf-canvas {
  max-width: 100%;
  box-shadow: 0 2px 20px var(--shadow);
}

.annot-panel {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-height: 0;
  overflow-y: auto;
  padding: 14px;
  background: var(--bg);
  border-left: 1px solid var(--border);
}

/* The score card: score, saved state, the decision (Approve score, or
   Approved + Re-open), then any status chips. */
.grade-score-card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.grade-score-card .score-number {
  font-size: 30px;
  font-weight: 700;
  line-height: 1;
  color: var(--accent-text-on-tint, var(--accent));
}
.grade-score-of { font-size: 15px; color: var(--text-dim); }
.grade-score-of[hidden] { display: none; }
.grade-draft-status { display: inline-flex; gap: 4px; align-items: center; }
.grade-score-go { margin-left: auto; display: inline-flex; gap: 6px; }
.grade-score-go:empty { display: none; }
.grade-score-chips { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 4px; }
.grade-score-chips:empty { display: none; }
.grade-hand-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  font-size: 13px;
  border: 1px solid var(--yellow);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--yellow) 10%, var(--surface));
}

/* The panel's sections. Narrated grading and Auto-grading open from their
   header (an accordion); Scoring summary is always open. The descriptor
   starts on its own line under the header and wraps there at any size. */
.grade-sec {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.grade-sec[hidden] { display: none; }
.grade-sec-h { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.grade-sec-h h3 { margin: 0; font-size: 14px; font-weight: 700; }
/* Its ▸/▾ marker is the shared .collapsible-details .disclosure-toggle rule. */
.grade-sec-tog {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 24px;
  padding: 2px 0;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.grade-sec-tog:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
.grade-sec-desc { padding-left: 16px; font-size: 12px; color: var(--text-dim); }
.grade-sec-b { display: flex; flex-direction: column; gap: 10px; }
.grade-sec-b[hidden] { display: none; }
/* The scoring summary's lists sit in the section itself, not a card in it. */
.grade-sec .grade-result-card { padding: 0; border: 0; background: none; }
.grade-auto-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.grade-auto-row #auto-grade-reason { flex-basis: 100%; margin: 0; }
.grade-auto-row #auto-grade-reason:empty { display: none; }

/* A collapsed row with a one-line preview: the solution process, the two
   notes to the AI. */
.grade-fold {
  padding: 8px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.grade-sec .grade-fold { background: var(--surface2); }
/* The ▸/▾ marker is the shared .collapsible-details rule. */
.grade-fold > summary { min-height: 24px; font-size: 13px; font-weight: 600; cursor: pointer; }
.grade-fold-pv {
  display: block;
  overflow: hidden;
  font-weight: 400;
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.grade-fold-pv:empty, .grade-fold[open] .grade-fold-pv { display: none; }
.grade-fold textarea { width: 100%; box-sizing: border-box; margin-top: 8px; }

/* A scoring row's text and page are one button: the jump to its mark. */
.grade-row-jump {
  display: block;
  flex: 1;
  min-width: 0;
  min-height: 24px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.grade-row-jump:hover .grade-row-page { text-decoration: underline; }
.grade-row-jump:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
.grade-row-page { color: var(--accent-text-on-tint, var(--accent)); }
.grade-row-jump > span { display: block; }
.note-jump { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
.note-jump > span { display: inline; }

/* Annotation type-picker — column of three buttons on the left of the
   Add-Annotation card. Active state is colour-coded to match the pin
   each type produces (red / accent / green). */
.annot-add-row {
  display: flex;
  gap: 12px;
  align-items: stretch;
}
.annot-type-column {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 64px;
  flex-shrink: 0;
}
.annot-add-fields {
  flex: 1;
  min-width: 0;
}
.annot-type-btn {
  flex: 1;
  border: 2px solid var(--border);
  border-radius: 6px;
  padding: 6px 4px;
  background: rgba(255,255,255,0.04);
  color: var(--text-dim);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  font-family: inherit;
  transition: background .12s, border-color .12s, color .12s;
}
.annot-type-btn:hover { background: rgba(255,255,255,0.10); color: var(--text); }
.annot-type-btn:disabled { opacity: .45; cursor: not-allowed; }
.annot-type-btn .annot-type-icon  { font-size: 18px; font-weight: 700; line-height: 1; }
.annot-type-btn .annot-type-label { font-size: 10px; font-weight: 500; letter-spacing: .02em; }
.annot-type-btn.active.annot-type-deduction     { background: var(--red);    color: var(--red-ink); border-color: var(--red); }
.annot-type-btn.active.annot-type-informational { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.annot-type-btn.active.annot-type-positive      { background: var(--green);  color: var(--green-ink); border-color: var(--green); }

.annot-add.annot-locked {
  /* Container opacity composites ALL descendant text over the page bg, which
     capped contrast near 4:1 (label/hint landed at 2.8:1 dark, 2.6:1 light —
     the "grade approved" hint the user must read was below AA, and no text
     token survives a 0.55 dim). Raised to 0.85: the panel still reads as
     dimmed/locked (reinforced by disabled controls + the explanatory hint),
     while text-dim clears AA (~5.0:1 all themes) and the brighter tokens
     clear it comfortably. */
  opacity: 0.85;
}
.annot-add.annot-locked input,
.annot-add.annot-locked textarea,
.annot-add.annot-locked button {
  cursor: not-allowed;
}

.form-row {
  margin-bottom: 12px;
}

.form-row label {
  display: block;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .4px;
  color: var(--text-dim);
  margin-bottom: 5px;
}

.form-row input, .form-row textarea, .form-row select {
  width: 100%;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--text);
  padding: 8px 10px;
  font-family: inherit;
  font-size: 13px;
  transition: border-color .15s;
}

.form-row input:focus, .form-row textarea:focus {
  outline: none;
  border-color: var(--accent);
}

.form-row textarea { resize: vertical; min-height: 72px; }
/* A field's error message (#584 price dialog): text beside the field, which
   also carries aria-invalid — the colour is a second cue, never the only one. */
.field-error { color: var(--error); margin: 4px 0 0; }

/* The scoring summary's Notes (#855): the marks its lists do not show. */
.note-item {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px 0;
  font-size: 13px;
}
.note-text { flex: 1; min-width: 8em; }

.grade-result-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
}

.grade-result-card .feedback-text {
  font-size: 13px;
  color: var(--text-dim);
  line-height: 1.6;
  margin-top: 10px;
  white-space: pre-wrap;
}

.deduction-item {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  gap: 8px;
}
.deduction-item:last-child { border-bottom: none; }

.deduction-body  { flex: 1; min-width: 0; }
.deduction-desc  { color: var(--text); }
.deduction-pts   { color: var(--red); font-weight: 600; white-space: nowrap; padding-top: 2px; }

/* Action buttons on each deduction row */
.deduction-actions {
  display: flex;
  gap: 2px;
  opacity: 0;
  transition: opacity .15s;
  flex-shrink: 0;
}
.deduction-item:hover .deduction-actions,
.positive-item:hover  .deduction-actions,
.deduction-item:focus-within .deduction-actions,
.positive-item:focus-within .deduction-actions { opacity: 1; }

.btn-icon {
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-dim);
  cursor: pointer;
  font-size: 11px;
  line-height: 1;
  padding: 2px 5px;
}
.btn-icon:hover             { background: var(--surface2); color: var(--text); }
.btn-icon.btn-icon-danger:hover { background: var(--red); color: var(--red-ink); border-color: var(--red); }

/* Extra-small button variant */
.btn-xs { padding: 3px 8px; font-size: 11px; }

/* 2.5.8 Target Size (Minimum): 24px floor for small controls.
   Glyph size unchanged — the box grows, the font does not. */
.btn-icon, .seg-chip-x, .topbar-ids-copy, .alert-close,
.queue-remove-btn, .rubric-stale-dismiss, .btn-xs, .topbar-ids-btn,
.asg-tile-menu-btn, .asg-add-act {
  min-width: 24px;
  min-height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Same 24px floor for the drag-alternative family: the assignment tile
   menu's Move earlier / later pair, the page-chip "Move ▾", and the
   submission-card cluster "Move".

   Neither 2.5.8 exception rescues these. Spacing: the menu items stack
   flush. Equivalent control: the only other way to perform the action is the
   drag gesture these controls exist to replace under 2.5.7 — so a 2.5.7
   remedy that itself fails 2.5.8 is no remedy at all.

   `width: auto` is load-bearing, not tidy-up, for the page-chip "Move ▾":
   it is `.btn-unstyled`, which sets `width: 100%`. `min-width` cannot undo
   that, so without this the button would stay full-bleed and the widening
   would land on its container instead of on the target. The tile menu's
   items are full-width rows on purpose (`.asg-tile-menu-list
   [role="menuitem"]`, more specific, below). */
.asg-reorder-up, .asg-reorder-down, .seg-chip-move {
  min-width: 24px;
  min-height: 24px;
  width: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Inline deduction edit form */
.deduction-edit-form {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 0;
}
.deduction-edit-form input {
  padding: 4px 7px;
  font-size: 12px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
}
.deduction-edit-form input:focus { outline: none; border-color: var(--accent); }
/* Inline "Pts" / "Page" labels on the two number spinners in the
   deduction edit form — without them the two bare spinners are
   visually indistinguishable. inline-flex keeps label text + input
   on one line as a single flex child of the .flex.gap-8 row. */
.de-field-label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
}

/* ─── Collapsible cards: grade-view grading instructions, process trace,
   rubric-tab grading context, Setup's solution preview, the dashboard's
   assignment cards ─────────────────────────────────────────────────── */
.collapsible-details > summary { list-style: none; }
.collapsible-details > summary::-webkit-details-marker { display: none; }
/* The marker is decoration: the open/closed state is the <details>' own (or
   the toggle button's aria-expanded, for the dashboard's assignment cards,
   whose heading cannot sit in a <summary> — static/views/dashboard.js), so
   the alt text after "/" keeps the triangle out of the control's name. An
   engine without that syntax drops the second declaration and keeps the
   first. */
.collapsible-details[open] > summary::before,
.collapsible-details .disclosure-toggle[aria-expanded="true"]::before { content: "▾ "; content: "▾ " / ""; }
.collapsible-details:not([open]) > summary::before,
.collapsible-details .disclosure-toggle[aria-expanded="false"]::before { content: "▸ "; content: "▸ " / ""; }
.grade-instructions-details textarea {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
}

/* ─── Rubric-tab grading-scheme slider toggle ─────────────────────────
   Two-option pill with a sliding "thumb" highlight on the active side.
   The track wraps two <button>s; the active one carries .active and is
   painted with the accent (Old Gold) color. The inactive option is dim
   and gains foreground color on hover so it reads as actionable. */
.scheme-toggle {
  display: inline-flex;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px;
  align-items: center;
  position: relative;
  gap: 0;
}
.scheme-toggle-opt {
  padding: 4px 14px;
  font-size: 12px;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  border-radius: 999px;
  transition: color 0.15s ease, background-color 0.15s ease;
  font-family: inherit;
  letter-spacing: 0.02em;
}
.scheme-toggle-opt.active {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}
.scheme-toggle-opt:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}
.scheme-toggle-opt:not(.active):not(:disabled):hover {
  color: var(--text);
}

.process-trace-strategy {
  font-size: 13px;
  color: var(--text);
  margin-bottom: 10px;
  line-height: 1.5;
}

.process-trace-alt-note {
  background: rgba(255, 205, 0,.12);
  border: 1px solid rgba(255, 205, 0,.3);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 10px;
  font-size: 12px;
  color: var(--accent-text-on-tint);
}

.process-trace-steps {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 10px;
}

.process-trace-step {
  background: var(--surface2);
  border-left: 3px solid var(--border);
  border-radius: 4px;
  padding: 8px 10px;
  font-size: 12px;
}

.process-trace-step.match    { border-left-color: var(--green); }
.process-trace-step.mismatch { border-left-color: var(--yellow); }
.process-trace-step.error    { border-left-color: var(--red); }

.process-trace-step-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 4px;
}

.process-trace-step-num   { font-weight: 700; color: var(--accent); }
.process-trace-step-page  { font-size: 11px; color: var(--text-dim); }

.process-trace-step-match-badge {
  margin-left: auto;
  font-size: 10px;
  padding: 1px 6px;
  border-radius: 10px;
}
.process-trace-step-match-badge.yes { background: rgba(74, 198, 142,.12); color: var(--green-text-on-tint); }
.process-trace-step-match-badge.no  { background: rgba(232, 155, 43,.12); color: var(--yellow-text-on-tint); }

.process-trace-step-action { color: var(--text); margin-bottom: 4px; }

.process-trace-step-detail {
  color: var(--text-dim);
  margin-top: 3px;
  line-height: 1.5;
}

/* One rule for the trace's labels. No opacity: --text-dim on --surface2 is
   5.56:1 dark/ugraid, 6.42:1 light; the .75 dim took it to 3.79/3.64 (#626). */
.process-trace-step-detail .label,
.process-trace-final .label {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .4px;
  color: var(--text-dim);
  margin-right: 4px;
}

.process-trace-step-detail code {
  background: rgba(0,0,0,.25);
  border-radius: 3px;
  padding: 1px 5px;
  font-family: "Consolas", "Fira Code", monospace;
  font-size: 11px;
  color: var(--text);
}

/* --red on its own tint was 3.63:1 dark, 4.17:1 light; the on-tint red is
   the theme's readable red for exactly this pairing (#626). */
.process-trace-step-error {
  margin-top: 4px;
  padding: 4px 8px;
  background: rgba(226, 90, 77,.10);
  border-radius: 4px;
  color: var(--red-text-on-tint);
  font-size: 11px;
}

.process-trace-final {
  margin-top: 10px;
  padding: 8px 10px;
  background: var(--surface2);
  border-radius: var(--radius);
  font-size: 12px;
}

.process-trace-final .label { margin-right: 6px; }

.process-trace-empty {
  color: var(--text-dim);
  font-size: 12px;
  font-style: italic;
}

/* ─── Rubric view ───────────────────────────────────────────────────── */
.rubric-criterion {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 12px;
}

.rubric-criterion-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.rubric-criterion-name {
  font-weight: 600;
  font-size: 14px;
}

.rubric-points-badge {
  background: var(--accent-dim);
  color: #fff;
  padding: 3px 10px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
}

.rubric-criterion-desc {
  font-size: 13px;
  color: var(--text-dim);
  margin-bottom: 10px;
}

.error-pattern {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 10px;
  background: var(--surface2);
  border-radius: 5px;
  margin-bottom: 6px;
  font-size: 12px;
  gap: 10px;
}

.error-pattern-desc { flex: 1; }
.error-pattern-pts { color: var(--red-text-on-tint); font-weight: 600; white-space: nowrap; }

/* Pattern-usage affordance: "N applied ▸" + collapsed hyperlinked SID list. */
.usage-link {
  margin-left: 8px;
  font-size: 11px;
  color: var(--accent, #4a90d9);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: underline;
}
.usage-link:hover { text-decoration: underline; }
.usage-sid-list {
  margin-top: 5px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 10px;
  align-items: center;
}
.usage-sid {
  cursor: pointer;
  font-size: 11px;
  padding: 1px 6px;
  border-radius: 4px;
  border: 1px solid var(--border);
  text-decoration: underline;
}
.usage-sid:hover { text-decoration: underline; }
.usage-sid.approved { color: var(--green); border-color: var(--green); }
.usage-sid.pending  { color: var(--text-hint); }
.usage-pts { font-size: 10px; color: var(--text-hint); margin-left: -6px; }

/* Merge history: each live line that absorbed others, with the lines merged
   into it nested under it. The connectors are borders; every price and price
   change is in the item's text. */
.merge-history { margin-top: 12px; }
.merge-tree { list-style: none; margin: 4px 0 0 0; padding-left: 18px; font-size: 12px; }
.merge-tree-root { padding-left: 0; }
.merge-tree li { position: relative; padding: 3px 0 3px 4px; }
.merge-tree .merge-tree li { border-left: 1px solid var(--border); padding-left: 14px; }
.merge-tree .merge-tree li::before {
  content: ""; position: absolute; left: 0; top: 12px; width: 10px;
  border-top: 1px solid var(--border);
}
.merge-survivor > .merge-wording { font-weight: 600; }
.merge-arrow { color: var(--text-dim); margin-right: 2px; }
.merge-detail { color: var(--text-dim); }
.merge-price {
  color: var(--text); font-weight: 600; white-space: nowrap;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 10px; padding: 0 6px;
}

/* ─── Structured rubric editor ──────────────────────────────────────── */
.rubric-edit-header {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
  margin-bottom: 12px;
}
.rubric-edit-header-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.rubric-edit-label {
  font-size: 12px;
  color: var(--text-dim);
  font-weight: 600;
  margin: 0;
  min-width: 96px;
}
.rubric-edit-input {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  padding: 5px 8px;
  font-size: 13px;
  font-family: inherit;
  width: 100%;
  box-sizing: border-box;
}
.rubric-edit-input:focus {
  outline: none;
  border-color: var(--accent);
}
.rubric-edit-input-sm  { width: 88px;  flex: 0 0 auto; }
.rubric-edit-input-xs  { width: 68px;  flex: 0 0 auto; }
.rubric-edit-input-name { flex: 1; font-weight: 600; }
.rubric-edit-pts-suffix {
  color: var(--text-dim);
  font-size: 12px;
  margin-left: -4px;
}
.rubric-edit-spacer { flex: 1; }

.rubric-edit-criterion {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
  margin-bottom: 10px;
}
.rubric-edit-criterion-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.rubric-edit-criterion textarea {
  min-height: 44px;
  resize: vertical;
}
.rubric-edit-errors-label {
  font-size: 11px;
  color: var(--text-dim);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  margin: 10px 0 4px;
}
.rubric-edit-error-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 0;
}

/* ─── Export view ───────────────────────────────────────────────────── */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  margin-bottom: 24px;
}

.stat-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  text-align: center;
}

.stat-number {
  font-size: 32px;
  font-weight: 700;
  color: var(--accent);
  line-height: 1;
}

.stat-label {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 6px;
}

.submissions-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.submissions-table th {
  text-align: left;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  color: var(--text-dim);
  font-weight: 500;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .3px;
}

.submissions-table td {
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.submissions-table tr:last-child td { border-bottom: none; }

.submissions-table tr:hover td { background: var(--surface2); }

/* Row headers (`<th scope="row">`) give each row's controls their programmatic
   context (1.3.1). They are data cells visually, so cancel the UA's bold/
   centred <th> default rather than letting them read as column headings.
   `.stats-table` itself is deliberately unstyled (UA table defaults). */
.stats-table tbody th,
.submissions-table tbody th {
  font-weight: 400;
  text-align: left;
}

/* ─── Utilities ─────────────────────────────────────────────────────── */
.flex { display: flex; }
.flex-center { display: flex; align-items: center; }
.gap-8  { gap: 8px; }
.mt-8   { margin-top: 8px; }
.mt-12  { margin-top: 12px; }
.mb-4   { margin-bottom: 4px; }
.text-dim { color: var(--text-dim); }
.text-sm  { font-size: 12px; }
.text-green { color: var(--green); }
.text-red   { color: var(--red); }
.text-yellow { color: var(--yellow-text); }
.bold { font-weight: 600; }

.divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 16px 0;
}

.empty-state {
  text-align: center;
  padding: 48px 24px;
  color: var(--text-dim);
}

.empty-state .empty-icon { font-size: 40px; margin-bottom: 12px; }
.empty-state p { font-size: 13px; }

/* ─── Rubric-stale banner (top of app) ─────────────────────────────── */
.rubric-stale-banner {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  margin: 0 12px;
  border-radius: 999px;
  background: rgba(255, 205, 0, 0.12);
  border: 1px solid rgba(255, 205, 0, 0.35);
  color: var(--accent-text-on-tint);
  font-size: 12px;
  cursor: pointer;
  transition: background 0.15s ease, transform 0.08s ease;
}
.rubric-stale-banner:hover {
  background: rgba(255, 205, 0, 0.22);
  transform: translateY(-1px);
}
.rubric-stale-icon {
  font-size: 15px;
  line-height: 1;
}
.rubric-stale-cta {
  color: var(--accent, #8b5cf6);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* Undimmed, like .alert-close (#626): the strip's ink is 6.44:1 or better. */
.rubric-stale-dismiss {
  background: transparent;
  border: 0;
  color: inherit;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  padding: 0 2px;
  margin-left: 4px;
  border-radius: 3px;
}
.alert-close:hover,
.rubric-stale-dismiss:hover {
  background: rgba(0, 0, 0, 0.08);
}

/* ─── Personal grading-style memory (settings modal) ───────────────── */
#us-tendencies-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 180px;
  overflow-y: auto;
  padding: 6px 8px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 4px;
}
.us-tendency {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text);
}

/* ─── Sticky-note pins on PDF canvas ───────────────────────────────── */
#pdf-canvas {
  display: block;   /* remove inline gap */
  cursor: crosshair;
}

.annot-pin {
  position: absolute;
  min-width: 28px;
  min-height: 28px;
  border-radius: 14px;
  padding: 0 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  /* centre on the click point */
  transform: translate(-50%, -50%);
  z-index: 20;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
  border: 2px solid rgba(255,255,255,0.2);
  box-shadow: 0 2px 8px rgba(0,0,0,0.55);
  transition: transform .12s, box-shadow .12s;
  user-select: none;
}

.annot-pin:hover {
  transform: translate(-50%, -50%) scale(1.2);
  box-shadow: 0 4px 14px rgba(0,0,0,0.75);
  z-index: 30;
}

/* A mark being moved (#855): a dashed outline, not a colour (1.4.1). A press
   on a mark in Annotate mode drags it rather than scrolling the page. */
.annot-pin.annot-pin-moving {
  outline: 2px dashed var(--focus);
  outline-offset: 3px;
  cursor: grabbing;
  z-index: 40;
}
.pdf-canvas-wrap:not(.pan-mode) .mb-mark { touch-action: none; }
/* The focused mark is drawn above every bubble (35, a raised one 45), so a
   bubble never hides the focus (2.4.11). */
#annot-overlay .mb-mark:focus { z-index: 46; }

/* What this view puts in a bubble's slots: its buttons and its author and
   time (#855). */
.mb-act {
  min-height: 24px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.mb-foot { font-size: 0.75rem; color: var(--text-dim); }

/* Annotation pin palette — three semantic types, three colors, applied
   uniformly across manual and AI-emitted pins so the layer is
   scheme-neutral. The shape distinction between manual (circular) and
   AI (rounded-rect) is preserved below for source legibility, but the
   color is type-driven only. */
.annot-pin-major   { background: var(--red);    color: var(--red-ink); }
.annot-pin-minor   { background: var(--red);    color: var(--red-ink); }
.annot-pin-comment { background: var(--yellow); color: #111; }
.annot-pin-praise  { background: var(--green);  color: var(--green-ink); }

/* AI-generated deduction pins — same red as manual corrections; the
   rounded-rect shape + italic font is what marks them as AI-sourced. */
.annot-pin-ai {
  background: var(--red);
  color: var(--red-ink);
  border-radius: 7px;
  border-color: rgba(255,255,255,0.3);
  font-style: italic;
}

/* AI-generated positive observation pins — green, same rounded-rect family as .annot-pin-ai */
.annot-pin-positive {
  background: var(--green);
  color: var(--green-ink);
  border-radius: 7px;
  border-color: rgba(255,255,255,0.3);
  font-style: italic;
}

/* The keyboard placement cursor: a crosshair, decorative (the page view
   says its position in words). */
.annot-place-cursor {
  position: absolute;
  width: 28px;
  height: 28px;
  transform: translate(-50%, -50%);
  z-index: 41;
  pointer-events: none;
  border: 2px solid var(--accent);
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--surface);
  background:
    linear-gradient(var(--accent), var(--accent)) center / 2px 100% no-repeat,
    linear-gradient(var(--accent), var(--accent)) center / 100% 2px no-repeat;
}

.annot-pin-pending {
  background: var(--accent);
  color: var(--accent-ink);
  border-style: dashed;
  opacity: .85;
  pointer-events: none; /* pending pins don't intercept clicks */
}

/* Per-row type tag rendered in the Annotations list. Mirrors the
   pin colour palette so the list and the canvas stay legible together
   at a glance. Same set of three states as .annot-type-btn (deduction,
   informational, positive); shapes differ (chip vs button). */
.annot-type-tag {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 0 6px;
  min-height: 16px;
  border-radius: 8px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .02em;
  white-space: nowrap;
}
.annot-type-tag-deduction     { background: var(--red);    color: var(--red-ink); }
.annot-type-tag-informational { background: var(--yellow); color: #111; }
.annot-type-tag-positive      { background: var(--green);  color: var(--green-ink); }

/* Rubric-link badge on deduction rows */
.rubric-link-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 10px;
  color: var(--accent);
  background: rgba(255, 205, 0,.12);
  border-radius: 4px;
  padding: 1px 5px;
  margin-left: 6px;
  vertical-align: middle;
}

/* Grading instructions card */
.instructions-card textarea {
  width: 100%;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--text);
  padding: 10px 12px;
  font-family: inherit;
  font-size: 13px;
  line-height: 1.5;
  resize: vertical;
  min-height: 88px;
  transition: border-color .15s;
}

.instructions-card textarea:focus {
  outline: none;
  border-color: var(--accent);
}

/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-hint); }

/* ─── Topbar extras ─────────────────────────────────────────────────── */
.topbar-right {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: 16px;
}

.provider-badge {
  font-size: 12px;
  color: var(--text-dim);
  padding: 4px 10px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 20px;
  white-space: nowrap;
  font-weight: 500;
}
.provider-badge:empty { display: none; }

.topbar-problem {
  font-size: 13px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 260px;
}

/* ─── Diagnostic IDs popover (ⓘ next to breadcrumb) ─────────────────── */
.topbar-ids-btn {
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 2px 6px;
  margin-left: 4px;
  color: var(--text-dim);
  border-radius: 4px;
}
.topbar-ids-btn:hover {
  color: var(--text);
  /* Matches the sibling .topbar-course:hover convention. The old
     var(--panel-hover, …) referenced a token that's never defined,
     so it always took the hardcoded fallback. */
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.topbar-ids-popover {
  position: absolute;
  top: 48px;
  left: 50%;
  transform: translateX(-50%);
  /* --panel-bg was never defined as a theme token, so var(--panel-bg,
     #1c1d22) always resolved to the dark literal — the popover stayed
     dark under [data-theme="light"]. --surface2 is the raised-surface
     token and tracks the active theme. */
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 6px 24px var(--shadow);
  padding: 8px;
  z-index: 1000;
  min-width: 320px;
  font-size: 12px;
}
.topbar-ids-row {
  display: grid;
  grid-template-columns: 36px 1fr 28px;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
}
.topbar-ids-row + .topbar-ids-row {
  border-top: 1px dashed var(--border);
}
.topbar-ids-label {
  color: var(--text-dim);
  font-weight: 600;
  text-transform: lowercase;
}
.topbar-ids-value {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px;
  color: var(--text);
  /* --surface reads as a subtle inset chip against the --surface2
     popover on both themes (was var(--panel-hover, …), an undefined
     token that fell back to a fixed translucent grey). */
  background: var(--surface);
  padding: 2px 6px;
  border-radius: 3px;
  user-select: all;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topbar-ids-copy {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  cursor: pointer;
  border-radius: 3px;
  padding: 2px 4px;
  font-size: 11px;
  line-height: 1;
}
.topbar-ids-copy:hover:not(:disabled) {
  color: var(--text);
  border-color: var(--accent);
}
.topbar-ids-copy:disabled {
  opacity: 0.7;
  cursor: default;
}
.topbar-ids-empty {
  padding: 8px;
  color: var(--text-dim);
  font-style: italic;
}

/* ─── Sidebar divider ───────────────────────────────────────────────── */
.sidebar-divider {
  height: 1px;
  background: var(--border);
  margin: 8px 12px;
}

/* Pushes the admin cluster and the foot to the bottom of the flex-column
   sidebar; a short window scrolls the sidebar instead, so the foot is never
   clipped (the drawer below 768 px scrolls the same way). */
.sidebar-spacer { flex: 1; }

/* ─── Tooltip (static/core/tooltip.js) ─────────────────────────────────
   One shared bubble for every [data-tip] control, shown on hover and on
   keyboard focus. It can itself be hovered (1.4.13), so pointer-events stay
   on; it never takes focus. Fixed: it is placed in viewport coordinates. */
.app-tooltip {
  position: fixed;
  z-index: 10000;
  pointer-events: none;   /* never between the pointer and a control; hoverable by geometry (tooltip.js) */
  max-width: 260px;
  padding: 5px 9px;
  border-radius: 6px;
  background: var(--surface2);
  color: var(--text);
  border: 1px solid var(--border);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .3);
  font-size: 12px;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.app-tooltip[hidden] { display: none; }

/* AI-provider radio list in the Settings modal. Four-column grid:
   radio | name | key-status | test-button */
.provider-radio-grid {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  row-gap: 8px;
  column-gap: 10px;
  align-items: center;
  padding: 4px 0;
}
.provider-radio-grid > label {
  display: contents;   /* children participate in the parent grid directly */
  cursor: pointer;
  font-weight: 400;
}
.provider-radio-grid > label > input[type="radio"] {
  justify-self: start;
  margin: 0;
}
.provider-radio-grid .provider-radio-name {
  justify-self: start;
}

/* Dev-only sidebar buttons get a slightly quieter treatment so they read
   as tools rather than primary navigation. */
.nav-btn-dev {
  font-size: 12px;
  color: var(--text-dim);
  opacity: 0.85;
}
.nav-btn-dev:hover { opacity: 1; }
.nav-btn-dev .icon { font-size: 14px; }

/* ─── Modals ────────────────────────────────────────────────────────── */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  z-index: 1000;
  backdrop-filter: blur(3px);
  /* Allow the overlay itself to scroll. Belt-and-braces with each
     modal's own overflow-y: this catches modals that exceed the
     viewport at zoom levels >100%, where an uncorrected viewport height
     would otherwise push their bottom off-screen. */
  overflow-y: auto;
  padding: 24px 16px;
}

/* Confirm dialogs always sit above other modals so they can't be hidden
 * behind a parent modal that opened them. */
#modal-confirm { z-index: 1100; }

.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 32px 28px;
  width: 100%;
  max-width: 420px;
  /* Cap height so tall modals (e.g. Preferences) scroll within the viewport
   * instead of overflowing the page. --vh100 carries the display-scale
   * correction, so the cap tracks the actual visible viewport at any
   * text size. */
  max-height: calc(var(--vh100) - 48px);
  overflow-y: auto;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
}

/* ─── Sign-in splash ─────────────────────────────────────────────── */
/* The overlay's gradient and background image (.splash-overlay, .splash-bg)
   are static/shared/splash.css, shared with the loading splash (#893). */

.splash-content {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  padding: 24px;
  max-width: 620px;
  width: 100%;
}

.splash-logo {
  font-size: 88px;
  font-weight: 800;
  letter-spacing: -2.5px;
  color: var(--text);
  line-height: 1;
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.6);
}
.splash-logo span { color: var(--accent); }
.splash-logo img {
  display: block;
  max-height: 180px;
  width: auto;
  max-width: 100%;
}


.splash-intro {
  width: 100%;
  max-width: 560px;
  margin: 4px 0 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
.splash-intro > summary {
  cursor: pointer;
  list-style: none;
  padding: 12px 16px;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: .2px;
  color: var(--text);
  display: flex;
  justify-content: space-between;
  align-items: center;
  user-select: none;
}
.splash-intro > summary::-webkit-details-marker { display: none; }
.splash-intro > summary::after {
  content: "▸";
  color: var(--accent);
  transition: transform 0.18s ease;
  font-size: 12px;
  margin-left: 10px;
}
.splash-intro[open] > summary::after {
  transform: rotate(90deg);
}
.splash-intro > summary:hover {
  background: var(--surface2);
}
.splash-intro-body {
  padding: 4px 16px 16px;
  color: var(--text-dim);
  font-size: 13.5px;
  line-height: 1.65;
  text-align: left;
  border-top: 1px solid var(--border);
}
.splash-intro-body p {
  margin: 10px 0 0;
}
.splash-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 28px 26px;
  width: 100%;
  max-width: 420px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
}

.modal-title {
  font-size: 17px;
  font-weight: 600;
  margin-bottom: 4px;
}

/* ─── Dashboard ─────────────────────────────────────────────────────── */
.problems-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 14px;
}

.problem-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 16px 14px;
  cursor: pointer;
  transition: border-color .15s, background .15s;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.problem-card .text-red { color: var(--red-text-on-tint); }

.problem-card:hover {
  border-color: var(--accent);
  background: var(--surface2);
}

.problem-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}

.problem-card-name {
  font-weight: 600;
  font-size: 14px;
  line-height: 1.3;
  flex: 1;
}

.problem-card-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.problem-card-footer {
  margin-top: 2px;
}

/* ─── Rubric context panel ──────────────────────────────────────────── */
.context-section-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .4px;
  color: var(--text-dim);
  margin-bottom: 6px;
  font-weight: 600;
}

.context-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 8px;
  background: var(--surface2);
  border-radius: 5px;
  margin-bottom: 4px;
  font-size: 12px;
}

.context-row.positive { background: rgba(74, 198, 142,.08); }

.context-row-desc { flex: 1; }
.context-row-meta { color: var(--text-dim); white-space: nowrap; font-size: 11px; }

/* ─── Positive observation items in grade result ────────────────────── */
.positive-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: rgba(74, 198, 142,.08);
  border-radius: 5px;
  margin-bottom: 5px;
  font-size: 12px;
  color: var(--green);
}

.positive-icon {
  font-weight: 700;
  flex-shrink: 0;
}

/* ══════════════════════════════════════════════════════════════════════
   NEW STYLES — model routing / caching / PII / hand-graded / source tags
   ══════════════════════════════════════════════════════════════════════ */

/* ─── Submission card: PII state ─────────────────────────────────────── */
.submission-card.pii { border-left: 3px solid var(--red); }

/* ─── PII warning banner ─────────────────────────────────────────────── */
.pii-warning-banner {
  background: rgba(226, 90, 77,.12);
  border: 1px solid rgba(226, 90, 77,.4);
  border-radius: var(--radius);
  padding: 12px 16px;
  margin-bottom: 12px;
  font-size: 13px;
  color: var(--red-text-on-tint);
}

.pii-warning-banner code {
  background: rgba(226, 90, 77,.15);
  padding: 1px 5px;
  border-radius: 4px;
  font-size: 11px;
  font-family: monospace;
}

.pii-warning-actions {
  margin-top: 6px;
}

/* ─── Rubric line badges (kept-line note, editor origin) ──────────────── */
.source-badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 500;
  padding: 1px 6px;
  border-radius: 10px;
  margin-left: 6px;
  vertical-align: middle;
  white-space: nowrap;
}

.source-badge-manual {
  background: var(--surface2);
  color: var(--text-dim);
  border: 1px solid var(--border);
}

/* #700: a rubric line's origin on the main Rubric view, shown only while the
   line is pointed at or focused (owner ruling 2026-10-17). 1.4.13: the reveal
   sits inside the line, flush below it, so the pointer can move onto it
   (hoverable); Escape adds .origin-dismissed (dismissible); it stays until
   the pointer or focus leaves the line (persistent). */
.rubric-entry { position: relative; }
.rubric-origin {
  display: none;
  position: absolute;
  top: 100%;
  left: 10px;
  z-index: 5;
  padding: 3px 8px;
  font-size: 11px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  box-shadow: 0 2px 8px var(--shadow);
  white-space: nowrap;
}
.rubric-entry:hover > .rubric-origin,
.rubric-entry:focus-within > .rubric-origin { display: block; }
.rubric-entry.origin-dismissed > .rubric-origin { display: none; }

.flex-badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 600;
  padding: 1px 6px;
  border-radius: 10px;
  margin-left: 6px;
  vertical-align: middle;
  /* Themed via the chart-manual blue token (defined for both themes)
     so the badge reads on dark and light surfaces. Text uses --text
     for guaranteed contrast since chart-manual is tuned for chart
     fills, not text legibility. */
  background: color-mix(in srgb, var(--chart-manual) 14%, var(--surface2));
  color: var(--text);
  border: 1px solid color-mix(in srgb, var(--chart-manual) 35%, var(--border));
}

/* ─── Novel-deductions panel ───────────────────────────────────────── */
.novel-panel-header {
  display: flex;
  align-items: center;
  gap: 12px;
}

.novel-group {
  margin-bottom: 14px;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}

.novel-group-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: var(--surface2);
  border-bottom: 1px solid var(--border);
  font-weight: 600;
}

.novel-group-count {
  font-size: 11px;
  color: var(--text-dim);
  font-weight: 400;
  margin-left: auto;
}

.novel-deduction-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}

.novel-deduction-row:last-child { border-bottom: none; }

.novel-deduction-desc {
  font-size: 13px;
  line-height: 1.4;
}

.novel-deduction-meta {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 2px;
}

.novel-deduction-pts {
  color: var(--red, #e07474);
  font-weight: 600;
  white-space: nowrap;
}

/* Bounds inputs in the rubric editor (flex common_patterns) */
.rubric-edit-bounds {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  color: var(--text-dim);
  margin-left: 6px;
}

.rubric-edit-bounds input {
  width: 48px;
  font-size: 11px;
  padding: 2px 4px;
}

/* ─── Hand-graded review panel ───────────────────────────────────────── */
#handgraded-review-panel {
  border: 1px solid rgba(232, 155, 43,.4);
  background: rgba(232, 155, 43,.06);
}

#handgraded-review-panel .card-title {
  color: var(--yellow-text);
}

/* ─── Upload batch progress ───────────────────────────────────────────── */

/* Outer track */
.upload-progress-track {
  height: 6px;
  background: var(--surface2);
  border-radius: 3px;
  overflow: hidden;
  margin: 10px 0 14px;
}

/* Animated fill */
.upload-progress-fill {
  height: 100%;
  /* Gold-to-bright-gold gradient: stays on-brand and reads on both
     themes. Earlier revision used a hardcoded blue endpoint that
     looked off against the cream light theme. */
  background: linear-gradient(90deg, var(--accent), var(--accent-hi));
  border-radius: 3px;
  transition: width 0.35s ease;
  min-width: 0;
}

/* Summary line above the bar */
.upload-progress-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 13px;
  color: var(--text-dim);
  margin-bottom: 4px;
}

.upload-progress-count {
  font-weight: 600;
  color: var(--text);
}

/* Per-file status rows */
.upload-file-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 6px;
  max-height: 280px;
  overflow-y: auto;
}

.upload-file-row {
  display: grid;
  grid-template-columns: 18px 1fr auto;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  padding: 5px 8px;
  border-radius: 4px;
  background: var(--surface2);
  transition: background 0.2s;
}

.upload-file-row.active  { background: rgba(255, 205, 0,.12); }
.upload-file-row.done    { background: rgba(74, 198, 142,.07); }
.upload-file-row.error   { background: rgba(226, 90, 77,.10); }

/* Status icon */
.upload-file-icon { font-size: 13px; text-align: center; }
.upload-file-row.active  .upload-file-icon::before { content: ""; display: inline-block; width: 12px; height: 12px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; vertical-align: middle; }
.upload-file-row.done    .upload-file-icon::before { content: "✓"; color: var(--green); }
.upload-file-row.error   .upload-file-icon::before { content: "✕"; color: var(--red); }
.upload-file-row.pending .upload-file-icon::before { content: "·"; color: var(--text-dim); }

/* Filename */
.upload-file-name {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text);
}

/* Stage label */
.upload-file-stage {
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 240px;
}

.upload-file-row.active .upload-file-stage { color: var(--accent); }
.upload-file-row.done   .upload-file-stage { color: var(--green); }
.upload-file-row.error  .upload-file-stage { color: var(--red); }

/* Per-row cancel button (X) — appears at the trailing edge of every
 * upload row. Subtle until hovered so it doesn't compete with the
 * stage label, but always discoverable. Hidden on .done rows since
 * there's nothing to cancel. */
.upload-file-cancel {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1;
  padding: 2px 6px;
  margin-left: 6px;
  border-radius: 3px;
  cursor: pointer;
  flex-shrink: 0;
  transition: background .12s, color .12s, border-color .12s;
}
.upload-file-cancel:hover {
  background: rgba(226, 90, 77, .12);
  color: var(--red);
  border-color: rgba(226, 90, 77, .35);
}
.upload-file-cancel:disabled {
  cursor: progress;
  opacity: .6;
}
.upload-file-row.done .upload-file-cancel { display: none; }

/* Inline hand-graded checkbox shown on pending rows */
.upload-hg-label {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  color: var(--text-dim);
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
}

.upload-hg-label input[type=checkbox] {
  accent-color: var(--accent);
  width: 12px;
  height: 12px;
  cursor: pointer;
}

/* ─── Upload queue panel ─────────────────────────────────────────────── */

.queue-card {
  margin-bottom: 12px;
}

/* File rows inside the queue panel reuse .upload-file-row but need 4 columns */
.queue-file-list {
  max-height: 220px;
  overflow-y: auto;
  margin-bottom: 12px;
}

.queue-file-row {
  display: grid;
  grid-template-columns: 18px 1fr auto auto;
  align-items: center;
  gap: 8px;
  padding: 5px 6px;
  border-radius: 6px;
  font-size: 12px;
}

/* Subtle zebra stripe — derived from --text so it stays visible against
   either theme's background (a fixed white-tint wash disappears on
   light surfaces). */
.queue-file-row:nth-child(odd) { background: color-mix(in srgb, var(--text) 4%, transparent); }

/* Remove button */
.queue-remove-btn {
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 12px;
  padding: 2px 4px;
  border-radius: 4px;
  line-height: 1;
}
.queue-remove-btn:hover { color: var(--red); background: rgba(226, 90, 77,.12); }

/* Queue action buttons row */
.queue-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}

.queue-submit-btn { margin-left: auto; }

/* ─── Server-offline overlay ─────────────────────────────────────────── */

/* Visibility is owned by showModal()/hideModal(), which set inline
   `display: flex` / `display: none` — no `.visible` class, which would
   fight the inline style the shared machinery writes. */
#offline-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: rgba(10, 11, 18, 0.92);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  align-items: center;
  justify-content: center;
}

.offline-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-top: 3px solid var(--red);
  border-radius: 10px;
  padding: 40px 48px;
  max-width: 420px;
  width: 90%;
  text-align: center;
  box-shadow: 0 24px 64px rgba(0,0,0,.6);
  animation: offline-in .25s ease;
}

@keyframes offline-in {
  from { opacity: 0; transform: scale(.95) translateY(12px); }
  to   { opacity: 1; transform: scale(1)  translateY(0); }
}

.offline-icon {
  font-size: 32px;
  margin-bottom: 16px;
  color: var(--red);
  animation: offline-pulse 2s ease-in-out infinite;
}

@keyframes offline-pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: .35; }
}

.offline-title {
  font-size: 20px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 10px;
}

.offline-body {
  font-size: 14px;
  color: var(--text-dim);
  line-height: 1.6;
  margin-bottom: 18px;
}

.offline-reconnect {
  font-size: 12px;
  /* No `opacity` here: --text-dim clears AA at full strength (5.56:1 on
     --surface) but a .7 alpha blend drops it to 3.83:1, which 12px text
     cannot spend. Use the token as-is. */
  color: var(--text-dim);
}

/* Logging button — amber tint when active */
.btn-logging-active {
  color: var(--yellow-text) !important;
  border-color: color-mix(in srgb, var(--yellow) 50%, transparent) !important;
  animation: log-pulse 2s ease-in-out infinite;
}
@keyframes log-pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: .6; }
}

/* ─── LTI grade posting ──────────────────────────────────────────────
   The log used to carry its colours as hard-coded hex literals in
   inline styles (a light-theme-only palette shipped into a dark-default
   app), so it failed 1.4.3 in every theme. The rules below are the
   single home for that styling; the JS emits class names only.

   Two token choices are load-bearing:
     • --red / --green are tuned as SOLID FILLS. As text on --surface2
       (the log background) --red is only 4.06:1 — a FAIL. The
       *-text-on-tint tokens are the text-weight variants (red 6.05:1)
       and are what these lines must use.
     • --text-dim (5.56:1) replaces every dim grey literal. */

/* Streaming post-grades log — one class per SSE event type. */
.post-log-line          { margin: 0 0 2px; }
.post-log-line.is-info,
.post-log-line.is-skip  { color: var(--text-dim); }
.post-log-line.is-ok    { color: var(--green-text-on-tint); }
.post-log-line.is-error { color: var(--red-text-on-tint); }
.post-log-line.is-summary {
  color: var(--text);
  font-weight: 600;
}

/* ─── Assignments ───────────────────────────────────────────────────── */
.asg-section {
  margin-bottom: 28px;
  padding: 12px 14px 14px;
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.asg-section-orphans {
  background: color-mix(in srgb, var(--surface) 40%, transparent);
  border-style: dashed;
}
.asg-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  gap: 10px;
}
.asg-section-title {
  font-weight: 600;
  font-size: 15px;
  color: var(--accent);
}
.asg-section-orphans .asg-section-title { color: var(--text-dim); }
/* An assignment is a collapsible card (#589): its h3 holds the toggle
   button, with the shared ▸/▾ marker before the name. */
.asg-section > .asg-section-title { margin: 0; }
.asg-section-body { margin-top: 10px; }
.asg-section-actions { margin-bottom: 10px; }

/* Assignment detail — the problems as tiles (ux wave 1, 2026-10-31). A tile
   is the dashboard's .problem-card; this adds the drag affordance, the
   per-tile menu, the discipline line and the Add tile. */
.asg-problem-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}
.asg-tile { position: relative; cursor: grab; }
.asg-tile:active { cursor: grabbing; }
.asg-tile.dragging { opacity: .4; }
.asg-tile.drag-over {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.asg-drag-handle {
  position: absolute;
  top: 6px;
  left: 6px;
  font-size: 12px;
  color: var(--text-hint);
  font-weight: 700;
  letter-spacing: -1px;
  user-select: none;
  opacity: 0;
  transition: opacity .15s;
}
.asg-tile:hover .asg-drag-handle,
.asg-tile:focus-within .asg-drag-handle { opacity: .8; }
.asg-tile-order {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .3px;
  text-transform: uppercase;
  color: var(--text-dim);
}
.asg-problem-name-link { cursor: pointer; text-align: left; overflow-wrap: anywhere; }
.asg-problem-name-link:hover { color: var(--accent); text-decoration: underline; }
/* The tile's menu: a button and the list it opens, right-aligned under it. */
.asg-tile-menu { position: relative; }
.asg-tile-menu-btn, .asg-add-act { padding: 2px 7px; }
.asg-tile-menu-list {
  position: absolute;
  right: 0;
  top: 100%;
  margin-top: 4px;
  z-index: 20;
  min-width: 200px;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px var(--shadow);
}
.asg-tile-menu-list [role="menuitem"] {
  display: block;
  width: 100%;
  text-align: left;
  padding: 6px 10px;
  border-radius: 6px;
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
}
.asg-tile-menu-list [role="menuitem"]:hover:not([aria-disabled="true"]),
.asg-tile-menu-list [role="menuitem"]:focus-visible { background: var(--surface2); }
.asg-tile-menu-list [role="menuitem"][aria-disabled="true"] { opacity: .4; cursor: default; }
.asg-tile-menu-list .asg-tile-danger { color: var(--red-text-on-tint); }
/* The discipline line: what the tile inherits or overrides, and the
   override's editor, shown by the menu's "Discipline override…". */
.asg-tile-disc {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 22px;
  font-size: 12px;
  color: var(--text-dim);
}
.asg-discipline-input {
  flex: 1;
  min-width: 0;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 4px 8px;
  font-size: 12.5px;
  font-family: inherit;
  cursor: text;
}
.asg-discipline-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.asg-discipline-input::placeholder { color: var(--text-hint); font-style: italic; }
/* The Add tile: dashed, last in the grid. */
.asg-tile-add {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 120px;
  border: 2px dashed var(--border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-dim);
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  transition: border-color .15s, color .15s;
}
.asg-tile-add:hover,
.asg-tile-add:focus-visible { border-color: var(--accent); color: var(--accent); }
.asg-tile-add-plus { font-size: 28px; line-height: 1; }
/* The settings card under the tiles. */
.asg-settings { margin-top: 20px; }
.asg-settings > summary { cursor: pointer; }
/* The add-problems dialog's rows and its "or" rule. */
.asg-add-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  margin-bottom: 6px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.asg-add-row:has(input:checked) { border-color: var(--accent); }
.asg-add-name { flex: 1; min-width: 0; overflow-wrap: anywhere; font-weight: 500; cursor: pointer; }
.asg-add-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0 8px;
  font-size: 12px;
  color: var(--text-dim);
}
.asg-add-divider::before,
.asg-add-divider::after { content: ""; flex: 1; border-top: 1px solid var(--border); }

/* ─── Prompt inspector (advanced settings) ──────────────────────────── */
.prompt-inspector-modal {
  max-width: 1100px;
  width: 95vw;
  padding: 22px 0 0;
  display: flex;
  flex-direction: column;
  max-height: calc(var(--vh100) * 0.88);
  height: calc(var(--vh100) * 0.88);
}
.prompt-inspector-modal > .modal-title {
  padding: 0 22px 12px;
}
.prompt-inspector-body {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: 0;
  min-height: 0;
  flex: 1;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  overflow: hidden;
}
.prompt-tabs-sidebar {
  background: var(--bg);
  border-right: 1px solid var(--border);
  overflow-y: auto;
  padding: 8px 0;
}
.pi-stage-item {
  padding: 8px 14px;
  cursor: pointer;
  color: var(--text-dim);
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: 8px;
  border-left: 3px solid transparent;
  user-select: none;
}
.pi-stage-item:hover {
  color: var(--text);
  background: var(--surface);
}
.pi-stage-item.active {
  color: var(--text);
  background: var(--surface2);
  border-left-color: var(--accent);
}
.pi-stage-name { flex: 1; }
.pi-stage-badge {
  background: var(--accent-dim);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 10px;
  min-width: 18px;
  text-align: center;
}
.prompt-inspector-main {
  overflow-y: auto;
  padding: 14px 22px 22px;
}
.prompt-fragment-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  margin: 0 0 12px;
  padding: 10px 12px;
}
.prompt-fragment-card.is-edited {
  border-color: var(--accent-dim);
  box-shadow: 0 0 0 1px var(--accent-dim) inset;
}
.prompt-fragment-card.is-security {
  background: var(--surface2);
  border-left: 3px solid var(--red);
}
.prompt-fragment-card.is-dynamic {
  background: var(--surface2);
  border-left: 3px solid var(--text-hint);
}
.pi-frag-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.pi-frag-label {
  font-weight: 600;
  font-size: 13px;
  color: var(--text);
  flex: 1;
}
.pi-frag-id {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  color: var(--text-hint);
}
.pi-frag-badge {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 3px;
}
.pi-frag-badge.kind-editable  { background: var(--accent-dim); color: #fff; }
.pi-frag-badge.kind-dynamic   { background: var(--text-hint); color: var(--bg); }
.pi-frag-badge.kind-security  { background: var(--red); color: var(--red-ink); }
.pi-frag-badge.role-system    { background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); }
.pi-frag-description {
  color: var(--text-dim);
  font-size: 12px;
  margin: 0 0 6px;
  font-style: italic;
}
.pi-frag-textarea {
  width: 100%;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 8px 10px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  line-height: 1.5;
  resize: vertical;
  min-height: 60px;
}
.pi-frag-textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.pi-frag-textarea.readonly {
  background: var(--bg);
  color: var(--text-dim);
  cursor: default;
}
.pi-frag-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  font-size: 11px;
  color: var(--text-hint);
}
.pi-frag-revert {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--accent);
  padding: 2px 8px;
  border-radius: 3px;
  font-size: 11px;
  cursor: pointer;
}
.pi-frag-revert:hover {
  background: var(--accent);
  color: var(--accent-ink, #111);
  border-color: var(--accent);
}
.pi-frag-saved {
  color: var(--green);
  font-size: 11px;
}
.prompt-inspector-footer {
  display: flex;
  gap: 8px;
  padding: 12px 22px 16px;
  align-items: center;
}

/* ─── Segment reconciliation modal ──────────────────────────────────── */
.seg-reconcile-grid {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr) minmax(0, 1fr);
  gap: 16px;
  margin-top: 10px;
  max-height: calc(var(--vh100) * 0.68);
}
.seg-pages-col,
.seg-lanes-col,
.seg-preview-col {
  min-height: 0;
  overflow-y: auto;
  padding-right: 4px;
}
.seg-col-title {
  font-weight: 600;
  margin-bottom: 6px;
  color: var(--text-dim);
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: space-between;
}
.seg-page-strip {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Lanes */
.seg-lane {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: 8px;
  margin-bottom: 10px;
  background: color-mix(in srgb, var(--surface) 60%, transparent);
  transition: border-color 120ms, background 120ms;
}
.seg-lane.drag-over {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}
.seg-lane-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
  font-size: 13px;
}
.seg-lane-title { font-weight: 600; }
/* A segmented scan's lane: its paper's state, and the split's finding while
   the lane holds the pages the split proposed (#729). */
.seg-lane-state { margin-left: auto; }
.seg-lane-finding { margin: -2px 0 6px; }
.seg-lane-body {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  min-height: 56px;
}
.seg-lane-empty {
  padding: 12px;
  width: 100%;
  text-align: center;
  font-style: italic;
}

/* Confidence badges */
.seg-conf {
  font-size: 10px;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid var(--border);
  text-transform: uppercase;
  letter-spacing: .4px;
}
.seg-conf-high   { color: var(--green-text-on-tint);  border-color: color-mix(in srgb, var(--green) 50%, transparent); }
.seg-conf-medium { color: var(--yellow-text-on-tint); border-color: color-mix(in srgb, var(--yellow) 50%, transparent); }
.seg-conf-low    { color: var(--red-text-on-tint);    border-color: color-mix(in srgb, var(--red) 50%, transparent); }

/* Page chips */
.seg-page-chip {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 72px;
  padding: 4px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: grab;
  user-select: none;
}
.seg-page-chip:active { cursor: grabbing; }
/* The page shown in the preview: a thicker border and a bold, underlined
   label, so the selection does not rest on colour alone (WCAG 1.4.1). */
.seg-page-chip[aria-current="true"] {
  border: 2px solid var(--text);
  padding: 3px;
}
.seg-page-chip[aria-current="true"] .seg-chip-label {
  font-weight: 700;
  text-decoration: underline;
  color: var(--text);
}
.seg-page-chip.dragging { opacity: .4; }
.seg-chip-thumb {
  width: 64px;
  height: 80px;
  background: var(--pdf-canvas-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 3px;
}
.seg-chip-thumb img { max-width: 100%; max-height: 100%; display: block; }
.seg-chip-placeholder {
  color: var(--text-dim);
  font-size: 11px;
}
.seg-chip-label {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 2px;
}
.seg-chip-x {
  position: absolute;
  top: -9px;
  right: -9px;
  background: var(--surface2);
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 999px;
  width: 24px;
  height: 24px;
  line-height: normal;
  padding: 0;
  font-size: 12px;
  cursor: pointer;
  /* display/flex-centering come from the shared 2.5.8 min-size rule near
     the button family; the glyph stays centered in the grown 24px box. */
  opacity: 0;
  transition: opacity .15s;
}
.seg-page-chip:hover .seg-chip-x,
.seg-page-chip:focus-within .seg-chip-x,
.seg-chip-x:focus-visible { opacity: 1; }

/* Hover/focus-revealed "Move ▾" drag-alternative control. */
.seg-chip-move {
  margin-top: 2px;
  padding: 0 2px;
  font-size: 11px;
  line-height: 16px;
  color: var(--text-dim);
  cursor: pointer;
  opacity: 0;
  transition: opacity .15s;
}
.seg-page-chip:hover .seg-chip-move,
.seg-page-chip:focus-within .seg-chip-move,
.seg-chip-move:focus-visible { opacity: 1; }
.seg-move-menu {
  position: absolute;
  z-index: 1000;
  top: 100%;
  left: 0;
  min-width: 170px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 6px 24px var(--shadow);
  padding: 4px;
  display: flex;
  flex-direction: column;
}
.seg-move-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 5px 8px;
  font-size: 12px;
  color: var(--text);
  border-radius: 4px;
  cursor: pointer;
}
.seg-move-item:hover,
.seg-move-item:focus-visible { background: var(--surface); }

/* Preview pane (#810) */
.seg-preview-col .seg-col-title { margin-top: 0; font-size: inherit; }
.seg-preview-caption { margin: 0 0 6px; font-size: 13px; }
.seg-preview-img {
  display: block;
  max-width: 100%;
  max-height: calc(var(--vh100) * 0.6);
  height: auto;
  background: var(--pdf-canvas-bg);
  border: 1px solid var(--border);
}
.seg-preview-img[hidden] { display: none; }

/* Horizontal page strip variant inside a lane looks fine; the left "all
   pages" column keeps one-per-row via its flex-direction:column container. */
.seg-pages-col .seg-page-chip { width: auto; flex-direction: row; gap: 8px; }
.seg-pages-col .seg-chip-thumb { width: 48px; height: 60px; }
.seg-pages-col .seg-chip-label { margin: 0; }

/* The Upload screens' list (core/upload_list.js): a paper split from a bulk
   scan is nested under the scan's name. */
.upload-list { list-style: none; margin: 0; padding: 0; }
.upload-stack { margin: 8px 0; }
.upload-stack-name { font-size: .95rem; margin: 6px 0 4px; overflow-wrap: anywhere; }
.upload-stack-papers { padding-left: 16px; border-left: 2px solid var(--border); }
/* A paper's name, opening its PDF: link-styled text, a 24 px target (2.5.8). */
.upload-pdf-btn {
  background: none; border: 0; padding: 0; min-height: 24px; cursor: pointer;
  font: inherit; color: inherit; text-decoration: underline;
  text-align: left; overflow-wrap: anywhere;
}
.upload-pdf-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Assignment-scope Submissions — upload rows */
.asg-upl-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 8px;
}
/* The finished row reads as secondary through the dim token, not opacity:
   the 0.7 dim took its details to 3.67/3.29 (#626). --text-dim on this
   background is 5.56:1 or better in every theme; the badges keep their ink. */
.asg-upl-row.committed {
  color: var(--text-dim);
  background: color-mix(in srgb, var(--surface2) 70%, transparent);
}
.asg-upl-main {
  flex: 1;
  min-width: 0;
}
.asg-upl-filename {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.asg-upl-meta {
  margin-top: 2px;
}
/* Upload student work — the Individual / Bulk scans selector (UX Edit 5).
   Real radios, visually hidden but focusable; the whole label is the target,
   and keyboard focus rings the label it is in. */
.upl-kind { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 6px 0 14px; }
.upl-kind[hidden] { display: none; }
.upl-kind-label { font-weight: 600; font-size: 14px; }
.upl-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--border); border-radius: 10px;
           overflow: hidden; background: var(--surface); }
.upl-seg label { position: relative; display: flex; flex-direction: column; gap: 2px; padding: 10px 18px;
                 cursor: pointer; min-width: 170px; border-right: 1px solid var(--border); }
.upl-seg label:last-child { border-right: 0; }
.upl-seg input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.upl-seg label:has(input:checked) { background: var(--accent); color: var(--accent-ink); }
.upl-seg label:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: -3px; }
.upl-seg .k-name { font-weight: 700; font-size: 14px; display: flex; gap: 6px; align-items: center; }
.upl-seg .k-help { font-size: 12px; }

/* Submissions overview (UX Edit 6). A status is a dot AND its word, never
   colour alone. */
.ov-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.ov-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ov-actions[hidden] { display: none; }
.ov-filter { display: flex; align-items: center; gap: 6px; }
.ov-summary { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 4px 0 12px; }
.ov-chip { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px;
           font-size: 12px; font-weight: 600; border: 1px solid var(--border); background: var(--surface2); }
.ov-chip.ok, .ov-state.ok { color: var(--green-text-on-tint); }
.ov-chip.miss, .ov-state.miss { color: var(--red-text-on-tint); }
.ov-chip.unc, .ov-state.unc { color: var(--yellow-text-on-tint); }
.ov-chip.multi, .ov-state.multi { color: var(--accent-text-on-tint); }
.ov-chip.ok { border-color: var(--green); }
.ov-chip.miss { border-color: var(--red); }
.ov-chip.unc { border-color: var(--yellow); }
.ov-chip.multi { border-color: var(--accent); }
.ov-table-wrap { overflow-x: auto; }
.ov-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.ov-table th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .4px;
               color: var(--text-dim); padding: 6px 8px; border-bottom: 1px solid var(--border); }
.ov-table td { padding: 8px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.ov-table tbody th { text-transform: none; letter-spacing: normal; font-size: 13px; color: var(--text);
                      padding: 8px; border-bottom: 1px solid var(--border); }
.ov-table .name { font-weight: 500; min-width: 160px; overflow-wrap: anywhere; }
.ov-table .name .login { display: block; font-size: 11px; color: var(--text-dim); font-weight: 400; }
.ov-table th.p, .ov-table td.p { text-align: center; }
.ov-table th.seg { text-align: right; }
.ov-table tr.ov-missing .name { color: var(--text-dim); }
.ov-state { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600;
            white-space: nowrap; }
.ov-state .dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
.ov-seg { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.ov-seg + .ov-seg { margin-top: 4px; }
.ov-seg .lbl { font-size: 12px; color: var(--text-dim); }
.ov-seg .btn { min-height: 24px; min-width: 24px; }
.ov-unmatched { margin-top: 18px; }
/* The active paper (UX wave 4): its dropdown, the notes said in words, and
   the "Set active for everyone" menu (core/menu.js, the ⋯ menu's styling). */
.ov-active { min-height: 28px; max-width: 100%; font-size: 12px; }
.ov-active-note { margin: 4px 0 0; font-size: 12px; color: var(--yellow-text-on-tint); text-align: right; }
.ov-menu { position: relative; }
.ov-menu[hidden] { display: none; }
/* The list is the Grade view's ⋯ menu (.grade-more-menu), opened below its
   button instead of beside a column. */
.ov-menu > .grade-more-menu { left: auto; right: 0; top: calc(100% + 4px); min-width: 220px; }
.ov-active-lost { color: var(--yellow-text-on-tint); }
/* Narrow widths: one stacked block per student; the problem statuses and
   the upload actions wrap under the name. */
@media (max-width: 720px) {
  .ov-table thead { display: none; }
  .ov-table tr { display: grid; gap: 4px; padding: 8px 0; border-bottom: 1px solid var(--border); }
  .ov-table td, .ov-table tbody th { display: block; border: 0; padding: 2px 4px; }
  .ov-table td.p { display: inline-flex; gap: 6px; text-align: left; }
  .ov-table td.p::before { content: attr(data-col) ": "; color: var(--text-dim); font-size: 11px; }
  .ov-seg { justify-content: flex-start; }
  .ov-active-note { text-align: left; }
}

/* ──────────────────────────────────────────────────────────────
   Approvals view (approval queue + redaction-box editor)
   ────────────────────────────────────────────────────────────── */

/* .nav-badge (the #nav-approvals-badge pill) is declared once, with the rest
   of the sidebar nav chrome — see the sidebar step-states block. It used to be
   re-declared here, which silently repainted every nav badge in the app. */

/* Courses picker — the post-login landing screen. Same feel as the
   problems grid but without cluster/status chrome, so picking a course
   is a one-click obvious action. */
.courses-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 14px;
  margin-top: 12px;
}
.course-card {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 18px 20px;
  background: var(--surface);
  cursor: pointer;
  transition: transform 0.08s, border-color 0.1s, background 0.1s;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.course-card:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
  transform: translateY(-1px);
}
.course-card-name {
  font-weight: 700;
  font-size: 16px;
  color: var(--text);
}
.course-card-desc {
  color: var(--text-dim, #7d8590);
  font-size: 12px;
  line-height: 1.4;
}
.course-card-enter {
  align-self: flex-end;
  margin-top: auto;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
}

/* Topbar course breadcrumb — click to switch courses */
.topbar-course {
  padding: 3px 8px;
  border-radius: 5px;
  cursor: pointer;
  color: var(--text-dim, #7d8590);
  font-size: 12px;
  transition: background 0.1s, color 0.1s;
}
.topbar-course:hover {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--text);
}

/* Topbar anonymizer queue indicator — visible whenever any problem has
   pending-approval or pending-analysis submissions. Shared across users. */
.anon-queue-indicator {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  margin-right: 8px;
  border: 1px solid color-mix(in srgb, var(--yellow, #bf8700) 40%, var(--border));
  background: color-mix(in srgb, var(--yellow, #bf8700) 10%, transparent);
  border-radius: 999px;
  font-size: 12px;
  font-weight: 500;
  color: var(--yellow-text-on-tint);
  cursor: pointer;
  user-select: none;
  transition: background 0.15s;
}
.anon-queue-indicator:hover {
  background: color-mix(in srgb, var(--yellow, #bf8700) 18%, transparent);
}
.anon-queue-indicator .anon-queue-icon {
  display: inline-block;
  animation: anon-queue-pulse 1.4s ease-in-out infinite;
}
@keyframes anon-queue-pulse {
  0%, 100% { opacity: 0.6; }
  50%      { opacity: 1.0; }
}

/* "Approve Redactions · N waiting", with Refresh at the right while
   automatic refresh is paused (UX wave 3). */
.approval-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 10px;
}
.approval-head .section-title { margin-bottom: 0; }
.approval-head .spacer { flex: 1; }
.approval-layout {
  display: grid;
  grid-template-columns: 280px 1fr 330px;
  gap: 14px;
  height: calc(var(--vh100) - 125px);
  min-height: 500px;
}
.approval-queue {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  overflow-y: auto;
  padding: 4px;
}
/* The queue's own controls above its list: Approve all ready, and the
   attention filter on a mixed queue (pressed: the accent fill). */
.approval-queue-top {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 6px 6px 8px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.approval-queue-top:not(:has(button:not([hidden]))) { display: none; }
.approval-queue-top .btn { flex: 1 1 auto; }
.approval-queue-top [aria-pressed="true"] {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
}
.approval-queue-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  margin-bottom: 2px;
  border-radius: 6px;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 0.1s;
}
.approval-queue-item:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.approval-queue-item.active {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.approval-queue-item .fn {
  font-weight: 600;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* "No student yet": the row's title, dimmed and in italics (said in words). */
.approval-queue-item .fn.none { color: var(--text-dim); font-style: italic; font-weight: 500; }
.approval-queue-item .sub {
  font-size: 11px;
  color: var(--text-dim, #7d8590);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
:is(.approval-queue-item, .approval-ocr) .pill {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  border: 1px solid;
}
:is(.approval-queue-item, .approval-ocr) .pill.needs-review {
  color: var(--yellow-text-on-tint);
  border-color: var(--yellow, #bf8700);
  background: color-mix(in srgb, var(--yellow, #bf8700) 12%, transparent);
}
:is(.approval-queue-item, .approval-ocr) .pill.confident {
  color: var(--green-text-on-tint);
  border-color: var(--green, #2da44e);
  background: color-mix(in srgb, var(--green, #2da44e) 12%, transparent);
}
.approval-queue-item .pill.id {
  color: var(--accent-text-on-tint);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
:is(.approval-queue-item, .approval-ocr) .pill.degraded {
  color: var(--red-text-on-tint);
  border-color: var(--red, #cf222e);
  background: color-mix(in srgb, var(--red, #cf222e) 12%, transparent);
}
/* Confidently name-free: a benign "nothing to redact" state, grouped with
   READY (not flagged for review). Neutral tone — it is neither a problem
   (yellow/red) nor a positive match (green). */
:is(.approval-queue-item, .approval-ocr) .pill.no-name {
  color: var(--text-dim, #6e7781);
  border-color: var(--border, #8c959f);
  background: color-mix(in srgb, var(--fg, #6e7781) 8%, transparent);
}

.approval-preview {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* Three columns: the file name and its page mark take the free space, so
   the page buttons and the view tools never move with the mark (#785). */
.approval-preview-toolbar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--surface2);
}
.approval-toolbar-file {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.approval-toolbar-file .pdf-filename { flex: 0 1 auto; min-width: 0; }
.approval-page-mark { flex: none; }
.approval-toolbar-pages,
.approval-toolbar-tools { display: inline-flex; align-items: center; gap: 4px; }
.approval-toolbar-tools .btn-secondary { margin-left: 4px; }
/* flex-start with the page's own auto margins: centred while it fits, and a
   zoomed page scrolls from its left edge instead of being cut off there. */
.approval-preview-wrap {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 12px;
  display: flex;
  justify-content: flex-start;
  background: var(--approval-preview-bg);
}
/* The preview takes keyboard focus so it can be scrolled (#318). Its parent
   clips overflow, so the global ring's outward offset would be cut off; draw
   it inside the edge instead. --focus on --approval-preview-bg: 12.60:1
   dark/ugraid, 3.94:1 light. */
.approval-preview-wrap:focus-visible {
  outline-offset: -2px;
}
/* Page container — the canvas and the SVG overlay sit in the same
   positioned block so zone coords map 1:1 onto the rendered bitmap. */
.approval-page {
  position: relative;
  margin: 0 auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  background: #fff;
}
.approval-page canvas {
  display: block;
}
/* A page turned on screen (display only): the box takes the turned size and
   the page, canvas and boxes together, is turned inside it from its corner. */
.approval-page-turn {
  position: relative;
  flex: none;
  margin: 0 auto;
}
.approval-page-turn > .approval-page {
  position: absolute;
  top: 0;
  left: 0;
  margin: 0;
  transform-origin: 0 0;
}
/* What the scanner read from the page on screen, under it (UX wave 3). */
.approval-ocr {
  border-top: 1px solid var(--border);
  background: var(--surface);
}
.approval-ocr > summary {
  cursor: pointer;
  padding: 8px 12px;
  font-size: 13px;
  display: flex;
  gap: 8px;
  align-items: center;
  min-height: 24px;
}
.approval-ocr > summary .pill { margin-left: auto; }
.approval-ocr-body { max-height: 220px; overflow: auto; padding: 0 12px 10px; }
.approval-ocr pre {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font: 12.5px/1.5 var(--mono, ui-monospace, monospace);
  color: var(--text);
  margin: 0;
  padding: 10px 12px;
  background: var(--surface2);
  border-radius: 8px;
}
/* The note where the read stopped: a bar and the words, never colour alone. */
.approval-ocr-cut {
  margin: 8px 0 0;
  padding: 6px 10px;
  border-left: 3px solid var(--yellow, #bf8700);
  background: color-mix(in srgb, var(--yellow, #bf8700) 12%, transparent);
  font-size: 12.5px;
  color: var(--text);
}
.approval-page svg.zones {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: all;
  /* Draw-a-box affordance for the whole overlay. The zone rects and handles
     declare their own cursor, which beats this inherited one — not !important. */
  cursor: crosshair;
}
.approval-zone-rect {
  fill: color-mix(in srgb, var(--accent) 25%, transparent);
  stroke: var(--accent);
  stroke-width: 2;
  cursor: move;
}
.approval-zone-rect.muted {
  fill: color-mix(in srgb, var(--text-dim) 15%, transparent);
  stroke: var(--text-dim, #7d8590);
  stroke-dasharray: 4 3;
}
.approval-zone-rect.selected {
  stroke: var(--yellow, #f0b849);
  stroke-width: 3;
}
.approval-zone-handle {
  fill: var(--yellow, #f0b849);
  cursor: nwse-resize;
}
.approval-zone-handle.ne, .approval-zone-handle.sw { cursor: nesw-resize; }

/* Unit IDENTITY: the students a provisional paper may belong to, one
   confirm button each (WCAG 2.5.8: the buttons keep the btn-sm hit area). */
.approval-candidates { list-style: none; margin: 6px 0 0; padding: 0;
                       display: flex; flex-direction: column; gap: 6px; }
.approval-candidates .btn { width: 100%; text-align: left; white-space: normal; }
/* #899 / UX wave 3: one block per student (or, on a group paper, per name
   read from the paper) -- who, why, then its buttons beneath. A confirmed
   student's block is tinted and says "confirmed" in words. */
.approval-name { display: flex; flex-direction: column; gap: 4px;
                 padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; }
.approval-name.confirmed {
  border-color: color-mix(in srgb, var(--green, #2da44e) 45%, var(--border));
  background: color-mix(in srgb, var(--green, #2da44e) 6%, transparent);
}
.approval-name-why { font-size: 12px; color: var(--text-dim); overflow-wrap: anywhere; }
.approval-name-read { font-weight: 600; overflow-wrap: anywhere; }
.approval-name-students { list-style: none; margin: 0; padding: 0 0 0 12px;
                          display: flex; flex-direction: column; gap: 4px; }
.approval-name-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.approval-name-student { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.approval-name > .approval-name-student { font-weight: 600; }
.approval-candidates .approval-name .btn { width: auto; }

.approval-meta {
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow-y: auto;
}
/* The decision at the top of the pane (owner 2026-11-01): Approve, the
   quiet Reject link, then the lines that come and go below them. */
.approval-decide { display: flex; flex-direction: column; gap: 6px; }
.approval-decide #approve-current-btn { width: 100%; }
.approval-reject {
  align-self: flex-end;
  display: inline-block;
  width: auto;
  min-height: 24px;
  padding: 4px 2px;
  font-size: 13px;
  color: var(--text-dim);
  text-decoration: underline;
}
.approval-reject:hover:not(:disabled) { color: var(--red-text-on-tint); }
.approval-reject:disabled { opacity: .45; cursor: not-allowed; }
/* Approve held for the student, and the field's Confirm before a student is
   typed: aria-disabled, so each keeps focus and its description. */
#approve-current-btn[aria-disabled="true"],
#approval-save-match[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
.approval-recover { display: flex; flex-wrap: wrap; gap: 6px; }
.approval-recover:not(:has(button:not([hidden]))) { display: none; }
#approval-hold-reason { margin: 0; }
.approval-other { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.approval-other[hidden] { display: none; }
.approval-other input { width: 100%; min-width: 0; }
.approval-other .btn { align-self: flex-start; }
/* The re-reads, quiet, at the foot of the pane; their notes above them. */
.approval-after {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  padding-top: 8px;
}
.approval-after p, .approval-after #rescan-status { align-self: stretch; margin: 0; }
.approval-zone-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 280px;
  overflow-y: auto;
  font-size: 12px;
}
.approval-zone-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border-radius: 4px;
  cursor: pointer;
}
.approval-zone-row:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.approval-zone-row.selected { background: color-mix(in srgb, var(--yellow, #f0b849) 14%, transparent); }
.approval-zone-row input[type=checkbox] {
  margin: 0;
  /* 2.5.8 Target Size: a native ~13px checkbox otherwise fails the 24px
     floor. Hit area only — the rendered control size is unchanged. */
  min-width: 24px;
  min-height: 24px;
}
.approval-zone-row .zone-text {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.approval-zone-row .zone-meta {
  font-size: 10px;
  color: var(--text-dim, #7d8590);
}

/* ─── Calibration wizard ("Grade with uGraid") ─────────────────────── */

#view-calibration { padding: 0; }

.wiz-shell {
  max-width: 880px;
  margin: 24px auto;
  padding: 28px 32px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
  position: relative;
}

.wiz-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}
.wiz-title {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.3px;
}
.wiz-title .wiz-spark { color: var(--accent); margin-right: 6px; }

.wiz-progress {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.wiz-phase-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 12px;
  background: var(--surface2);
  color: var(--text-dim);
  border: 1px solid var(--border);
  white-space: nowrap;
}
.wiz-phase-chip.active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent-text-on-tint);
  border-color: var(--accent-dim);
  font-weight: 600;
}
.wiz-phase-chip.done {
  color: var(--green);
  border-color: color-mix(in srgb, var(--green) 60%, var(--border));
}

.wiz-stage {
  animation: wiz-slide-in 0.35s ease;
}
.wiz-stage.leaving { animation: wiz-slide-out 0.25s ease forwards; }

@keyframes wiz-slide-in {
  from { opacity: 0; transform: translateX(28px); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes wiz-slide-out {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(-28px); }
}

.wiz-intro-card {
  padding: 22px 24px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 12px;
  margin-bottom: 18px;
}
.wiz-intro-card h3 { margin-bottom: 10px; font-size: 16px; }
.wiz-intro-card p { color: var(--text-dim); margin-bottom: 8px; }
.wiz-intro-card ol { margin: 8px 0 0 22px; color: var(--text-dim); }
.wiz-intro-card ol li { margin: 4px 0; }
.wiz-intro-card ol li strong { color: var(--text); }

.wiz-pick-card {
  padding: 14px 16px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 10px;
  margin-bottom: 12px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 6px 14px;
  align-items: center;
}
.wiz-pick-card .wiz-pick-name { font-weight: 600; }
.wiz-pick-card .wiz-pick-meta {
  font-size: 12px;
  color: var(--text-dim);
  grid-column: 1 / -1;
}
.wiz-pick-card .wiz-pick-reason {
  font-size: 11px;
  color: var(--accent);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Round 2's papers: a list of pick cards, each with its progress (as text)
   and its controls on a row of their own. */
.wiz-review-list { list-style: none; margin: 0; padding: 0; }
.wiz-pick-card .wiz-pick-actions {
  grid-column: 1 / -1;
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}
.wiz-review-state { flex: 1; font-size: 13px; }

.wiz-actions {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-top: 18px;
  flex-wrap: wrap;
}
.wiz-actions .spacer { flex: 1; }

.wiz-summary-stat {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 10px 0;
  border-bottom: 1px dashed var(--border);
}
.wiz-summary-stat:last-child { border-bottom: 0; }
.wiz-summary-stat .num {
  font-size: 24px;
  font-weight: 700;
  color: var(--accent);
  min-width: 48px;
}
.wiz-summary-stat .lbl { color: var(--text-dim); }

.wiz-loading {
  text-align: center;
  padding: 40px;
  color: var(--text-dim);
}
.wiz-loading .spinner { display: inline-block; margin-right: 8px; }

/* Inter-phase rubric refresh banner. Visual family matches .alert-info
   (gold-tinted bg + accent-text-on-tint text) but inline-flex with the
   spinner so it reads as in-flight work, not a static announcement. */
.wiz-refresh-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 0;
  padding: 8px 14px;
  background: rgba(255, 205, 0, .12);
  border: 1px solid rgba(255, 205, 0, .3);
  color: var(--accent-text-on-tint);
  border-radius: var(--radius);
  font-size: 13px;
}
.wiz-refresh-banner strong { color: var(--text); }


/* ─── Diagnostic inspect modals (descriptors + cluster report) ─────────
 * Used by openSubmissionInspect() / openClusterReport() in submissions.js.
 * Wider than the default .modal (which caps at 420px) because the body
 * has tabular score data + tag chips that need horizontal room.
 */

.modal-inspect-overlay { z-index: 1050; }

.modal-inspect {
  max-width: 920px;
  width: calc(100vw - 64px);
  padding: 0;
  display: flex;
  flex-direction: column;
}

.modal-inspect-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 22px 12px;
  border-bottom: 1px solid var(--border);
}

.modal-inspect-header .modal-title {
  font-size: 16px;
  font-weight: 600;
}

.modal-inspect-close {
  background: transparent;
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 6px;
  width: 28px; height: 28px;
  font-size: 14px;
  cursor: pointer;
}
.modal-inspect-close:hover { color: var(--text); background: var(--surface2); }

.modal-inspect-body {
  padding: 16px 22px 20px;
  overflow-y: auto;
}

.inspect-loading {
  padding: 40px;
  text-align: center;
  color: var(--text-dim);
}

.inspect-title-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
}

.inspect-section {
  margin: 14px 0;
}

.inspect-section-title {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  margin-bottom: 6px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.inspect-jump {
  margin-left: auto;
  font-size: 11px;
}

.inspect-problem-header {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 10px;
  margin-bottom: 12px;
}

.inspect-problem-strip,
.inspect-aggregate-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}

.inspect-topic-tags {
  margin-top: 6px;
  font-size: 12px;
  color: var(--text-dim);
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}

.inspect-cluster-desc {
  color: var(--text-dim);
  font-size: 13px;
  font-style: italic;
  margin-bottom: 8px;
}

.inspect-approach-label {
  font-weight: 600;
  margin-bottom: 2px;
}

.inspect-approach-desc {
  color: var(--text-dim);
  font-size: 13px;
}

/* Feature-flag rows (matched / submission-only / cluster-only) */
.inspect-feature-groups,
.inspect-occurrence-grid {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.inspect-feature-row {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: 10px;
  align-items: start;
  padding: 4px 0;
}

.inspect-feature-key {
  font-size: 12px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.inspect-feature-vals {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.inspect-occurrence-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Tag chips — small rectangular pills used in feature-vector renders. */
.tag-chip {
  display: inline-block;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--surface2);
  border: 1px solid var(--border);
  font-size: 11px;
  font-family: ui-monospace, monospace;
  color: var(--text);
}

.tag-match       { background: color-mix(in srgb, var(--green) 15%, var(--surface2));
                   border-color: color-mix(in srgb, var(--green) 30%, var(--border));
                   color: var(--green-text-on-tint); }
.tag-sub-only    { background: color-mix(in srgb, var(--accent) 15%, var(--surface2));
                   border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
                   color: var(--accent-text-on-tint); }
.tag-cluster-only{ background: color-mix(in srgb, var(--yellow, #bf8700) 15%, var(--surface2));
                   border-color: color-mix(in srgb, var(--yellow, #bf8700) 30%, var(--border));
                   color: var(--yellow-text-on-tint); }
/* The neutral/info tag pills (tag-section / topic-tag) take their text
   colour from var(--text), never a literal: a hardcoded light blue/teal
   reads as invisible on a light theme. The tinted background still
   distinguishes them from the
   semantic match/cluster-only/sub-only variants above. */
.tag-section     { background: color-mix(in srgb, #6b88ff 12%, var(--surface2));
                   border-color: color-mix(in srgb, #6b88ff 30%, var(--border));
                   color: var(--text); }
.topic-tag       { background: color-mix(in srgb, #8aa 18%, var(--surface2));
                   color: var(--text); }

/* Numeric axis bars (one row per score axis). */
.axis-grid {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.axis-row {
  display: grid;
  grid-template-columns: 130px 1fr 200px;
  align-items: center;
  gap: 10px;
  padding: 4px 0;
}

.axis-label {
  font-size: 12px;
  color: var(--text-dim);
}

.axis-bar-wrap {
  position: relative;
}

.axis-bar-track {
  position: relative;
  height: 14px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 7px;
  overflow: hidden;
}

/* Bands sit on var(--surface2). Mixing with that (instead of transparent)
   gives a more solid colour block, which reads on both dark and light
   themes — transparent mixes wash out badly against the light cream.
   Colour comes from --axis-band (purple) to keep the score bars
   visually distinct from the gold accent and the semantic chips. */
.axis-stddev-band {
  position: absolute;
  top: 0; bottom: 0;
  background: color-mix(in srgb, var(--axis-band) 30%, var(--surface2));
}

.axis-iqr-band {
  position: absolute;
  top: 2px; bottom: 2px;
  background: color-mix(in srgb, var(--axis-band) 60%, var(--surface2));
  border-radius: 4px;
}

.axis-minmax-band {
  position: absolute;
  top: 5px; bottom: 5px;
  background: color-mix(in srgb, var(--axis-band) 16%, var(--surface2));
}

.axis-median-tick {
  position: absolute;
  top: -1px; bottom: -1px;
  width: 2px;
  background: var(--axis-band);
  transform: translateX(-1px);
}

.axis-value-marker {
  position: absolute;
  top: -4px; bottom: -4px;
  width: 4px;
  background: var(--text);
  border-radius: 2px;
  transform: translateX(-2px);
  box-shadow: 0 0 6px rgba(255, 255, 255, 0.4);
}

.axis-numbers {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-family: ui-monospace, monospace;
}

.axis-value  { color: var(--text); font-weight: 600; }
.axis-median { color: var(--text-dim); }

.axis-z-chip {
  font-size: 10px;
  padding: 1px 4px;
  border-radius: 3px;
  font-family: ui-monospace, monospace;
}
.axis-z-near { background: color-mix(in srgb, var(--green) 18%, var(--surface2));  color: var(--green-text-on-tint); }
.axis-z-mid  { background: color-mix(in srgb, var(--yellow, #bf8700) 18%, var(--surface2)); color: var(--yellow-text-on-tint); }
.axis-z-far  { background: color-mix(in srgb, var(--red) 18%, var(--surface2));    color: var(--red-text-on-tint); }

/* Footer for tokens / cost. */
.inspect-footer {
  margin-top: 12px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  font-family: ui-monospace, monospace;
  font-size: 11px;
}

/* Cluster-report members table */
.inspect-members-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}

.inspect-members-table th {
  text-align: left;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-dim);
  padding: 6px 8px;
  border-bottom: 1px solid var(--border);
}

.inspect-members-table td {
  padding: 6px 8px;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
  vertical-align: top;
}

.inspect-members-table tr:hover td { background: var(--surface2); }

/* Submission-card Inspect button — same visual family as retry/kickback. */
.sub-inspect-btn {
  position: absolute;
  top: 6px;
  right: 78px;
  width: 22px; height: 22px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 22px;
  text-align: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity .15s, background .15s, color .15s;
}
.submission-card:hover .sub-inspect-btn,
.submission-card:focus-within .sub-inspect-btn,
.sub-inspect-btn:focus-visible { opacity: 1; }
.sub-inspect-btn:hover {
  background: color-mix(in srgb, var(--accent) 18%, var(--surface2));
  color: var(--accent);
}

/* Cluster-reassignment drag-alternative — same hover-revealed family. */
.sub-move-btn {
  position: absolute;
  top: 6px;
  right: 102px;
  /* 24px, not 22px: a drag-alternative cannot claim
     2.5.8's equivalent-control exception. Set here rather than in the shared
     floor block because these explicit width/height declarations come later
     in the cascade and would otherwise win. */
  width: 24px; height: 24px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 24px;
  text-align: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity .15s, background .15s, color .15s;
}
.submission-card:hover .sub-move-btn,
.submission-card:focus-within .sub-move-btn,
.sub-move-btn:focus-visible { opacity: 1; }
.sub-move-btn:hover {
  background: color-mix(in srgb, var(--accent) 18%, var(--surface2));
  color: var(--accent);
}
.sub-move-menu {
  position: absolute;
  z-index: 1000;
  top: 30px;
  right: 6px;
  min-width: 180px;
  max-height: 220px;
  overflow-y: auto;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 6px 24px var(--shadow);
  padding: 4px;
  display: flex;
  flex-direction: column;
}
.sub-move-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 5px 8px;
  font-size: 12px;
  color: var(--text);
  border-radius: 4px;
  cursor: pointer;
}
.sub-move-item:hover,
.sub-move-item:focus-visible { background: var(--surface); }

.cluster-report-btn {
  background: var(--surface2);
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  width: 24px; height: 24px;
  font-size: 12px;
  line-height: 1;
  margin-left: 4px;
}
.cluster-report-btn:hover { color: var(--text); background: var(--surface); }

/* OCR section in the submission Inspect modal */
.inspect-ocr-toggle {
  margin-left: auto;
  font-size: 11px;
}

.inspect-ocr-body {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 480px;
  overflow-y: auto;
  padding: 8px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.inspect-ocr-meta {
  display: flex;
  gap: 16px;
  padding: 4px 6px;
  border-bottom: 1px solid var(--border);
  font-family: ui-monospace, monospace;
}

.inspect-ocr-page {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.inspect-ocr-page-head {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.inspect-ocr-page-text {
  margin: 0;
  padding: 6px 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: ui-monospace, monospace;
  font-size: 11px;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 240px;
  overflow-y: auto;
}

/* ── Admin audit inspector ────────────────────────────────────────────── */
.admin-audit-grid {
  display: grid;
  grid-template-columns: minmax(240px, 320px) 1fr;
  gap: 16px;
  align-items: start;
}
.admin-audit-side {
  position: sticky;
  top: 12px;
  align-self: start;
  max-height: calc(var(--vh100) - 24px);
  overflow-y: auto;
}
.admin-audit-main { min-width: 0; }
.audit-tree-course      { font-weight: 600; margin-top: 8px; }
.audit-tree-courselog   { padding-left: 12px; font-size: 12px; }
.audit-tree-asg         { padding-left: 12px; margin-top: 4px; }
.audit-tree-prob        { padding-left: 24px; font-size: 13px; }
.audit-tree-orphans-label { padding-left: 12px; margin-top: 4px; color: var(--text-dim); font-style: italic; }
/* One rule for all four scopes the tree renders (course, course-level
   events, assignment, problem). An <a> with no box of its own is only as
   tall as its line (~15-16px), which fails the 24px pointer-target floor;
   inline-flex gives it a box that min-height can raise without widening the
   click area past the label. */
a.audit-tree-link       { display: inline-flex; align-items: center;
                          min-height: 24px;
                          text-decoration: none; color: var(--text); cursor: pointer; }
a.audit-tree-link:hover { text-decoration: underline; }

/* The credits dialog's per-dependency project links. They carried no rule at
   all, so they rendered in the browser's default link blue — 1.83:1 on the
   dark dialog surface. Same shape as the tree link above, and for the same
   two reasons: a themed colour that is calibrated against the surface, and
   inline-flex so min-height can raise the pointer target to the 24px floor
   (2.5.8) without widening the click area past the label. Underlined at rest
   rather than on hover only, so the link is not distinguished from the rest
   of its table row by colour alone (1.4.1). The model price dialog's
   pricing-page link (#584) is the same kind of external link on the same
   dialog surface, so it shares the rule. */
.credits-section a, #model-price-link { display: inline-flex; align-items: center;
                     min-height: 24px;
                     text-decoration: underline; color: var(--text); }

.audit-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
/* Earlier revisions referenced undefined vars (--bg-card, --hover,
   --bg-subtle, --font-mono) with light-mode-tuned rgba fallbacks.
   The fallbacks fired in dark mode and produced white-on-dark sticky
   headers + invisible hover states. Switch to the canonical theme
   tokens so the table reads on both themes. */
.audit-table thead th {
  position: sticky;
  top: 0;
  background: var(--surface);
  text-align: left;
  border-bottom: 1px solid var(--border);
  padding: 8px 6px;
  font-weight: 600;
}
/* Chrome-free fieldsets. A fieldset is what gives a set of controls a
   collective identity; these rules only take away the browser's default
   chrome so the group still reads as the flat strip it always was. Removing
   the border is a PRESENTATIONAL choice — the grouping it used to draw is
   carried by the element and its legend, which is what assistive technology
   reads, so nothing is lost by not painting it.

   Used by the History filter row and by the Settings panel's per-service
   model blocks; one rule rather than a near-identical copy per surface. */
.audit-filter-group,
.provider-model-group {
  border: 0;
  margin: 0;
  padding: 0;
  min-inline-size: 0;   /* fieldsets do not shrink below their content without this */
}
.provider-model-group + .provider-model-group { margin-top: 14px; }
.audit-filter-legend,
.provider-model-legend {
  padding: 0;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .4px;
  color: var(--text-dim);
  margin-bottom: 5px;
}

.audit-table tr.audit-row { cursor: pointer; }
.audit-table tr.audit-row:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
.audit-table td { padding: 6px; border-bottom: 1px solid var(--border); vertical-align: top; }
.audit-table td.audit-ts { font-family: ui-monospace, monospace; white-space: nowrap; }
.audit-detail-row td { background: color-mix(in srgb, var(--text) 4%, transparent); }
.audit-detail {
  font-family: ui-monospace, monospace;
  font-size: 11px;
  margin: 0;
  padding: 8px;
  white-space: pre-wrap;
  word-break: break-word;
}

/* ── Process Tree view ──────────────────────────────────────────────── */
/* Cluster edge palette (cycled by color_index) + node colors. Resolved at
   render time in process_tree.js so they track the active theme. */
:root {
  --pt-c0: #4e9af1; --pt-c1: #1abc9c; --pt-c2: #e67e22; --pt-c3: #9b59b6;
  --pt-c4: #e74c3c; --pt-c5: #2ecc71; --pt-c6: #f1c40f; --pt-c7: #5dade2;
  --pt-c8: #af7ac5; --pt-c9: #e08e0b;
  --pt-node: #2b2f37; --pt-node-ref: #243024; --pt-node-border: #3a3f4a;
  --pt-node-text: #f2eddc; --pt-node-err: #3a2a2a;
}
[data-theme="light"] {
  --pt-node: #eef2f7; --pt-node-ref: #eaf5ea; --pt-node-border: #c8cdd6;
  --pt-node-text: #1a1814; --pt-node-err: #fbeaea;
}

.pt-toolbar { display: flex; align-items: center; gap: 12px; margin: 8px 0; flex-wrap: wrap; }
.pt-meta { color: var(--text-dim); font-size: 12px; }
.pt-legend { display: flex; flex-wrap: wrap; gap: 14px; margin: 8px 0 12px; }
.pt-legend-item { display: flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }
.pt-swatch { width: 14px; height: 14px; border-radius: 3px; display: inline-block; flex: 0 0 auto; }
.pt-count { color: var(--text-dim); }

#pt-stage { position: relative; }
#pt-cy {
  width: 100%; height: calc(var(--vh100) * 0.7); min-height: 420px;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
}

.pt-detail {
  position: absolute; top: 12px; right: 12px; width: 300px;
  max-height: calc(var(--vh100) * 0.7 - 24px); overflow: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 12px; box-shadow: 0 4px 16px rgba(0, 0, 0, .3);
}
.pt-detail-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
.pt-detail-cluster { font-size: 11px; color: var(--text-dim); }
.pt-detail-steps { margin: 0; padding-left: 0; list-style: none; }
.pt-detail-steps li { font-size: 12px; margin: 4px 0; display: flex; gap: 6px; }
.pt-step-n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; min-height: 18px; border-radius: 9px; background: var(--surface2);
  color: var(--text-dim); font-size: 10px; flex: 0 0 auto;
}

/* ─── Keyboard focus visibility (WCAG 2.4.7 / 2.4.13) ─────────────────
   :focus-visible only: mouse clicks on buttons/cards do not paint the
   ring; keyboard focus (and focus in text fields) does. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Supplement the outline-suppressed text-control focus styles: keyboard
   focus still gets the ring. Selector list mirrors every outline-cleared
   :focus rule left in place. Equal specificity, later in source, so it
   wins for keyboard focus. */
.cluster-label-edit:focus-visible,
.cluster-desc-edit:focus-visible,
.form-row input:focus-visible,
.form-row textarea:focus-visible,
.deduction-edit-form input:focus-visible,
.rubric-edit-input:focus-visible,
.instructions-card textarea:focus-visible,
.asg-discipline-input:focus-visible,
.pi-frag-textarea:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* ─── Shared utility classes (consumed by later WPs) ──────────────────── */

/* NOTE: `.btn-unstyled` / `.btn-unstyled--inline` are defined near the TOP of
   this file (before the component sections) on purpose — see the comment
   there. Placing them here (after the components) let their resets clobber
   component styling and broke pixel-parity. Only `.visually-hidden` lives
   here; its properties are `!important`/positioning, so cascade order is
   irrelevant. */

/* Visually hidden, still in the accessibility tree and focus order. */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Off-screen until keyboard focus brings it on-screen, top-left. */
.skip-link {
  position: absolute;
  left: 8px;
  top: -40px;
  z-index: 1000;
  padding: 8px 12px;
  background: var(--surface2);
  color: var(--text);
  border-radius: 6px;
  transition: top .15s ease;   /* collapsed by P7 under reduced motion */
}
.skip-link:focus {
  top: 8px;
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* ─── Reduced motion (WCAG 2.3.3) ─────────────────────────────────────
   Collapse all animation/transition to a single imperceptible frame.
   Spinners freeze — their accompanying status text still conveys
   activity. Covers the wizard slide (wiz-slide-in/out), the offline/log/
   anon-queue pulses, hover transforms, and modal entrances. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ─── Reflow (WCAG 1.4.10 / 2.4.11) ──────────────────────────────────
   Hamburger drawer + grid stacking below 768px. Everything visible-facing
   lives inside the @media (max-width: 768px) block so the desktop (≥769px)
   layout stays pixel-identical. The two rules outside the media query are:
   (1) the topbar menu button and overlay default to display:none (they only
   appear inside the query), and (2) scroll-padding-top on .main-content,
   a keyboard-focus fix that applies at all widths (2.4.11). */

.topbar-menu-btn { display: none; }
.sidebar-overlay { display: none; }

/* 2.4.11: a keyboard-focused audit row lands below the sticky audit thead,
   which sticks inside the app's real scroll container (.main-content). */
.main-content { scroll-padding-top: 48px; }

@media (max-width: 768px) {
  /* Single-column shell; topbar spans, sidebar goes off-canvas. */
  .app-shell {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
  }
  .topbar {
    flex-wrap: wrap;
    height: auto;
    min-height: 56px;
    padding: 8px 12px;
  }
  /* The badges and buttons on the right wrap with the topbar instead of
     pushing the page sideways at 320 px (1.4.10), now that an administrator
     sees the provider badge (#439). */
  .topbar-right {
    flex-wrap: wrap;
    min-width: 0;
  }
  .topbar-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    min-height: 40px;
    font-size: 18px;
    width: auto;           /* override .btn-unstyled block width:100% */
  }

  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 400;
    width: 260px;
    max-width: 85vw;
    transform: translateX(-100%);
    /* visibility flips after the slide-out so off-canvas nav items leave the
       tab order once closed (2.4.3); the delay is collapsed by P7. */
    visibility: hidden;
    transition: transform 0.2s ease, visibility 0s linear 0.2s;
    box-shadow: 4px 0 24px rgba(0, 0, 0, 0.35);
  }
  .sidebar.open {
    transform: translateX(0);
    visibility: visible;
    transition: transform 0.2s ease, visibility 0s linear 0s;
  }
  .sidebar-overlay {
    position: fixed;
    inset: 0;
    z-index: 399;
    background: rgba(0, 0, 0, 0.45);
  }
  .sidebar.open + .sidebar-overlay,
  .sidebar-overlay.open { display: block; }

  /* Stack every fixed multi-column grid into one column. */
  .grade-layout,            /* was 1fr 380px */
  .approval-layout,         /* was 280px 1fr 300px */
  .stats-grid,              /* was repeat(4, 1fr) */
  .prompt-inspector-body,   /* was 240px 1fr */
  .seg-reconcile-grid,      /* was 220px 1fr 1fr */
  .admin-audit-grid         /* was minmax(240px,320px) 1fr */
  { grid-template-columns: 1fr; }

  /* Relax the viewport-locked fixed heights so the single column flows and
     scrolls inside .main-content instead of being clipped. */
  .grade-layout    { height: auto; min-height: 0; }   /* was the full height of the work area */
  /* The grader stacks: the tool column becomes a wrapping row above the
     paper, the paper keeps most of a screen's height, and the page scrolls. */
  .main-content:has(> #view-grade.active) { overflow-y: auto; }
  #view-grade.active { height: auto; }
  .grade-col { flex-direction: row; flex-wrap: wrap; align-items: flex-end;
               border-right: 0; border-bottom: 1px solid var(--border); }
  .grade-col-rule { display: none; }
  .grade-col-stack { flex-direction: row; }
  .grade-col-wide { width: auto; }
  .grade-more-menu { left: auto; right: 0; top: calc(100% + 6px); }
  .pdf-panel { height: calc(var(--vh100) * 0.7); }
  .annot-panel { border-left: 0; overflow-y: visible; }
  .grade-mark-panel.grade-mark-unplaced { transform: none; left: 0; }
  /* The mark panel docks as a full-width sheet at the bottom (#929). */
  .grade-mark-panel {
    top: auto;
    right: 0;
    bottom: 0;
    width: auto;
    max-height: calc(var(--vh100) * 0.7);
    border-radius: var(--radius) var(--radius) 0 0;
  }
  .approval-layout { height: auto; min-height: 0; }   /* was calc(var(--vh100) - 125px), min-height 500px */
  /* The page header wraps: the view tools take a line of their own. */
  .approval-preview-toolbar { grid-template-columns: minmax(0, 1fr) auto; }
  .approval-toolbar-tools { grid-column: 1 / -1; flex-wrap: wrap; }
  .seg-reconcile-grid { max-height: none; }           /* was calc(var(--vh100) * 0.68) */
  .admin-audit-side { position: static; max-height: none; } /* was sticky rail */

  /* 4 stat cards → 2×2; still no horizontal scroll at 320px. */
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ── Voice narration panels (Setup + Grade) ─────────────────────────────────
   Shared chrome for the two narration surfaces. Colour is never the sole
   carrier of state (WCAG 1.4.1): the record button's label flips
   Record → Stop and the adjacent role="status" text spells the phase out, so
   the red dot is redundant reinforcement rather than the signal itself.
   The pulse is decorative and inherits the global prefers-reduced-motion
   clamp near the top of this file. */

.narration-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.narration-status { min-height: 1.2em; }

/* Recording indicator. Hidden until the button reports aria-pressed="true",
   so the idle button carries no stray dot. */
.narration-dot {
  display: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--red);
  margin-right: 6px;
  flex: 0 0 auto;
}

.narration-controls [aria-pressed="true"] .narration-dot {
  display: inline-block;
  background: currentColor;   /* on the red fill below, the ink colour */
  animation: narration-pulse 1.4s ease-in-out infinite;
}

/* While recording, the toggle IS the Stop button, the action the instructor
   reaches for, so it takes the solid recording fill (the --red/--red-ink pair
   .btn-danger uses, calibrated per theme). Discard stays a ghost button
   beside it, so the control that loses the take is the quieter one (#576). */
.narration-controls [aria-pressed="true"] {
  background: var(--red);
  color: var(--red-ink);
  font-weight: 600;
}
.narration-mic { margin-right: 6px; }
.narration-controls [aria-pressed="true"] .narration-mic { display: none; }

/* The grader's Edit pages and the Segment dialog's Save pages on an approved
   paper are aria-disabled, not disabled, so they keep focus (#729); they are
   dimmed the way :disabled is. */
#btn-edit-pages[aria-disabled="true"],
#seg-commit-btn[aria-disabled="true"] { opacity: .4; cursor: not-allowed; }

/* ─── Set up your problem (UX wave 3, 08-setup) ───────────────────────────
   One column; from 1100px the PDF viewer is a drawer beside it, below that
   the viewer sits under the solution card with its own bounded scroll area.
   Nothing is covered or made inert at any width. min-width:0 lets every
   column narrow, so nothing scrolls sideways at 320px (WCAG 1.4.10). */
:root { --su-drawer-w: min(560px, 44vw); }
.su-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.su-head .section-title { margin-bottom: 0; flex: 1; }
.su-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "sol" "tab" "pdf" "list";
  column-gap: 20px;
}
.su-sol { grid-area: sol; min-width: 0; }
.su-tab { grid-area: tab; }
.su-drawer { grid-area: pdf; }
.su-list { grid-area: list; }
.su-keep { margin: 8px 0 16px; }
.su-sol > .card, .su-sol > .upload-zone { margin-bottom: 16px; }

/* The analysed solution's card. */
.su-sol-top { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
.su-sol-ic { font-size: 28px; color: var(--text-dim); }
.su-sol-name { flex: 1; min-width: 200px; }
.su-sol-title { font-weight: 700; font-size: 15px; overflow-wrap: anywhere; }
#view-setup .spacer { flex: 1; }
.su-file {
  display: inline-block; font-size: 12px; color: var(--text-dim); overflow-wrap: anywhere;
  text-align: left; text-decoration: underline; text-underline-offset: 2px; min-height: 24px;
}
.su-file:hover { color: var(--text); }
.su-file.big { font-size: 14px; font-weight: 700; color: var(--text); min-width: 0; }
.su-pts { font-size: 13px; color: var(--text-dim); white-space: nowrap; align-self: center; }
.su-pts b { font-size: 20px; color: var(--text); }
.su-sol-actions { display: flex; gap: 6px; align-self: center; flex-wrap: wrap; }
.su-summary { margin: 10px 0 12px; font-size: 13.5px; }
.su-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px 22px; margin: 0; }
.su-grid dt { font-size: 11px; text-transform: uppercase; letter-spacing: .4px; color: var(--text-dim); font-weight: 600; }
.su-grid dd { margin: 2px 0 8px; font-size: 13px; }
.su-grid ol, .su-grid ul { margin: 0; padding-left: 18px; }
.su-hint { font-weight: 400; }
.su-edit-foot {
  display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; padding-top: 10px;
  position: sticky; bottom: 0; background: var(--surface); border-top: 1px solid var(--border);
}

/* Uploaded, not analysed: the file, the instructions and Analyze. */
.su-file-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
.su-file-ic { font-size: 24px; }
.su-notes-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.su-notes-label { font-weight: 600; font-size: 14px; flex: 1; }
.su-notes #sol-narration-text { min-height: 300px; }
.su-go { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }

/* The settings rows. */
.su-list { padding: 0; }
.su-row { border-top: 1px solid var(--border); }
.su-row:first-child { border-top: 0; }
/* The ▸/▾ marker is the shared .collapsible-details rule. */
.su-row > summary, .su-row.plain {
  display: flex; align-items: center; gap: 12px; padding: 12px 16px; min-height: 44px;
}
.su-row > summary { cursor: pointer; }
.su-row.plain { flex-wrap: wrap; }
.su-row > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
/* Candidates waiting: a bar and a tint, and the preview in full ink — never
   colour alone, the preview says "N waiting for you". */
.su-row.su-attn { box-shadow: inset 4px 0 0 var(--purple); background: var(--surface2); }
.su-row.su-attn .su-prev { color: var(--text); font-weight: 600; }
.su-label { font-weight: 600; font-size: 14px; min-width: 190px; }
.su-prev { flex: 1; min-width: 0; color: var(--text-dim); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.su-body { padding: 0 16px 14px 38px; }
.su-row textarea {
  width: 100%; background: var(--surface2); border: 1px solid var(--border); border-radius: 5px;
  color: var(--text); padding: 10px 12px; font-family: inherit; font-size: 13px; line-height: 1.5;
  resize: vertical; min-height: 88px;
}
.su-check { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 13.5px; cursor: pointer; }
.su-alt { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-top: 10px; background: var(--surface); }
.su-alt-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.su-alt-summary { margin: 6px 0 0; font-size: 13px; }
.su-alt-detail { margin-top: 6px; }
.su-alt-detail ul { margin: 4px 0 0 18px; }

/* Discipline: an ARIA combobox (typeahead + caret). */
.su-combo { position: relative; display: flex; width: 340px; max-width: 100%; }
.su-combo input {
  flex: 1; min-width: 0; background: var(--surface2); border: 1px solid var(--border); border-right: 0;
  border-radius: 5px 0 0 5px; color: var(--text); font-family: inherit; font-size: 13px; padding: 7px 10px;
}
.su-combo input:focus-visible { outline: 2px solid var(--focus); outline-offset: -1px; }
.su-combo-btn {
  background: var(--surface2); border: 1px solid var(--border); border-radius: 0 5px 5px 0;
  color: var(--text); min-width: 32px; cursor: pointer;
}
.su-combo ul {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 50; margin: 4px 0 0; padding: 4px;
  list-style: none; max-height: 240px; overflow-y: auto; background: var(--surface);
  border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 6px 18px var(--shadow);
}
.su-combo li { padding: 6px 10px; border-radius: 4px; font-size: 13px; cursor: pointer; min-height: 24px; }
.su-combo li:hover { background: var(--surface2); }
.su-combo li[aria-selected="true"] { background: var(--surface2); outline: 2px solid var(--focus); outline-offset: -2px; }
.su-combo li.su-inherit { color: var(--text-dim); font-style: italic; }

/* The PDF viewer. Narrow: a full-width toggle row, then the pages in the
   page with their own scroll area capped at 60% of the window height. */
.su-tab {
  display: inline-flex; align-items: center; gap: 6px; width: 100%; padding: 10px 14px;
  margin-bottom: 8px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: var(--text); font: inherit; font-size: 13px;
  font-weight: 600; cursor: pointer;
}
.su-tab:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.su-tab[hidden], .su-drawer[hidden] { display: none; }
.su-caret { transition: transform .15s; }
.su-tab[aria-expanded="true"] .su-caret { transform: rotate(90deg); }
.su-drawer {
  max-height: calc(var(--vh100) * .6); overflow-y: auto; overscroll-behavior: contain; margin-bottom: 16px;
  background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius);
}
.su-dhead {
  position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; background: var(--surface); border-bottom: 1px solid var(--border);
}
.su-dname { flex: 1; min-width: 0; font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.su-pages { padding: 16px; display: flex; flex-direction: column; gap: 16px; }
/* Each page is a pdf.js canvas: the paper is page content, white in every
   theme; the box holds the page's shape until it is drawn. */
.su-pg { background: #fff; box-shadow: 0 1px 4px var(--shadow); aspect-ratio: var(--su-page-ratio, 8.5 / 11); }
.su-pg.drawn, .su-pg.failed { aspect-ratio: auto; }
.su-pg.failed { background: var(--surface); padding: 16px; }
.su-pg canvas { display: block; width: 100%; height: auto; }
.su-pdf-msg { padding: 8px 0; }
@media (min-width: 1100px) {
  /* Wide: the tab is a vertical strip beside the column; the viewer, when
     open, is a drawer beside it that stays in view while the page scrolls. */
  .su-layout {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "sol tab" "list tab";
    align-items: start;
  }
  .su-layout.with-drawer {
    grid-template-columns: minmax(0, 1fr) auto var(--su-drawer-w);
    grid-template-areas: "sol tab pdf" "list tab pdf";
  }
  .su-tab {
    writing-mode: vertical-rl; width: auto; padding: 12px 7px; margin: 0; position: sticky; top: 0;
    border-radius: 8px;
  }
  .su-tab i { writing-mode: horizontal-tb; }
  .su-tab .su-caret { display: none; }
  .su-drawer {
    position: sticky; top: 0; max-height: calc(var(--vh100) - 120px); margin: 0;
  }
  .su-layout.with-drawer .su-label { min-width: 0; }
  .su-layout.with-drawer .su-body { padding-left: 16px; }
}
@media (max-width: 1099px) {
  .su-label { min-width: 0; }
  .su-body { padding-left: 16px; }
  /* So the PDF pane starts on screen below the instructions. */
  .su-notes #sol-narration-text { min-height: 160px; }
}

@keyframes narration-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .35; }
}

.narration-hint {
  margin-top: 8px;
  padding: 8px 10px;
  border-left: 3px solid var(--border);
  background: var(--surface2);
  border-radius: 4px;
}

/* Per-page split returned by the transcriber, shown so the instructor can
   confirm the boundaries landed where they meant them to before generating
   a draft. */
.narration-pages {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.narration-pages .narration-page-row {
  padding: 6px 8px;
  background: var(--surface2);
  border-radius: 4px;
}

.narration-pages .narration-page-num {
  color: var(--text);
  font-weight: 600;
  margin-right: 6px;
}

/* Narration the model could not place. Surfaced rather than dropped —
   amber border marks it as "needs your attention", and the leading label
   states that in words for anyone who can't see the border. */
.narration-unhandled {
  margin-top: 12px;
  padding: 8px 10px;
  border-left: 3px solid var(--yellow);
  background: var(--surface2);
  border-radius: 4px;
  color: var(--text);
}

/* Marks a draft item that came from narration, with the quoted source. */
.annot-narration-span {
  display: block;
  margin-top: 4px;
  padding-left: 8px;
  border-left: 2px solid var(--border);
  color: var(--text-dim);
  font-style: italic;
}

/* ── Bulk scan splitter ───────────────────────────────────────────────────
   The load-bearing rules: the review grid's `.bulk-boundary` controls are
   ALWAYS in the DOM and focusable; hover and focus change only their opacity
   — never their presence — so every split and merge is reachable by keyboard,
   and on a touch screen, where there is no hover at all, they are simply
   shown. */

.bulk-panel-title { margin: 0 0 8px; font-size: 1.05rem; }
.bulk-panel-title:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.bulk-skip-link {
  position: absolute; left: -9999px;
}
.bulk-skip-link:focus {
  position: static; display: inline-block; margin-bottom: 8px;
  padding: 4px 8px; border: 1px solid var(--border); border-radius: 4px;
}

.bulk-cover-zone { margin-top: 8px; padding: 14px; }
.bulk-or-divider { display: flex; align-items: center; gap: 10px; color: var(--text-dim);
                   font-size: 12px; margin: 14px 0 8px; }
.bulk-or-divider::before, .bulk-or-divider::after { content: ""; flex: 1; border-top: 1px solid var(--border); }

.bulk-file-list { list-style: none; margin: 10px 0 0; padding: 0; }
.bulk-file-row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 4px 0; border-bottom: 1px solid var(--border);
}

.bulk-thumb-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 10px; max-height: calc(var(--vh100) * 0.52); overflow-y: auto; padding: 4px;
}
.bulk-thumb {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 6px; background: var(--surface2); cursor: pointer;
  /* 2px transparent, replaced (not added) when selected, so the tile never
     changes size on selection and the grid does not reflow. */
  border: 2px solid transparent; border-radius: 6px;
}
.bulk-thumb canvas { width: 150px; height: auto; background: #fff; }
/* Selection is signalled three ways — border, checkmark glyph and
   aria-checked — never by colour alone (WCAG 1.4.1). */
.bulk-thumb.is-selected { border-color: var(--accent); }
.bulk-check { font-weight: 700; }
.bulk-thumb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bulk-thumb-cap { font-size: .8rem; color: var(--text-dim); }

.bulk-destination { border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; }
.bulk-dest-option { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
/* An assignment name can be one unbroken token wider than a phone-width
   dialog: the label wraps anywhere rather than widening the dialog (1.4.10). */
.bulk-dest-option span { overflow-wrap: anywhere; }

.bulk-threshold { display: flex; align-items: center; gap: 10px; margin: 12px 0; }
.bulk-threshold input[type="range"] { flex: 1 1 220px; }
.bulk-threshold output { min-width: 15ch; font-variant-numeric: tabular-nums; }

/* The review grid (step 3): one tile per page in page order, and the first
   page of each submission starts a new row (#577). The boundary control sits
   in the column gap on a tile's left edge, so the gap is wide enough to hold
   a 24 px target and the container's left padding leaves room for the first
   tile of each row. */
.bulk-groups {
  --bulk-gap: 36px;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  column-gap: var(--bulk-gap); row-gap: 20px;
  margin: 0; padding: 6px 6px 6px 34px;
  max-height: calc(var(--vh100) * 0.46); overflow-y: auto;
}
/* Page counts (#909): the summary beside the threshold, Previous/Next unusual
   at the start of the action row, and a rail beside the grid's scroll area
   with one mark per unusual submission. The marks (amber, >= 3:1 on
   --surface2 in every theme) are a pointer shortcut; the buttons are the
   keyboard's, and the badge says "Unusual" in words. */
.bulk-unusual-next { margin-right: auto; }
.bulk-grid-wrap { display: flex; gap: 4px; }
.bulk-grid-wrap .bulk-groups { flex: 1 1 auto; min-width: 0; }
.bulk-rail {
  position: relative; flex: 0 0 12px;
  background: var(--surface2); border: 1px solid var(--border); border-radius: 4px;
}
.bulk-rail-mark {
  position: absolute; left: 1px; right: 1px; height: 8px;
  background: var(--yellow-text); border-radius: 2px; cursor: pointer;
}
.bulk-chip.bulk-chip-unusual {
  border-color: var(--yellow-text); border-radius: 4px; color: var(--text); font-weight: 600;
}
.bulk-tile {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 4px; padding: 6px; background: var(--surface2);
  border: 2px solid transparent; border-radius: 6px;
}
.bulk-tile canvas { width: 100%; max-width: 150px; height: auto; background: #fff; }
/* The split point: a bar in the gap before the first page of each submission
   (the accent clears 3:1 against the dialog in both themes — 1.4.11). A
   document boundary, a fixed cut with no control, is drawn double. That
   page sits in the first column, so each submission starts a new row. */
.bulk-tile.is-first { grid-column-start: 1; }
.bulk-tile.is-first::before {
  content: ""; position: absolute; top: 0; bottom: 0;
  left: calc(-1 * var(--bulk-gap) / 2 - 2px); width: 4px;
  background: var(--accent); border-radius: 2px;
}
.bulk-tile.is-doc::before { background: none; border-left: 4px double var(--accent); }
/* Over the destination's page limit: the label says so in words; the border
   is the second cue, never the only one (1.4.1). */
.bulk-tile.is-over { border-color: var(--danger, #d13); }
/* A file name can be one unbroken token longer than a phone-width tile: the
   label wraps anywhere rather than widening the grid (1.4.10). */
.bulk-tile-label {
  font-size: .8rem; font-weight: 600; text-align: center; overflow-wrap: anywhere;
}
.bulk-tile-cap {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: center;
  font-size: .8rem; color: var(--text-dim);
}
.bulk-page-conf { font-variant-numeric: tabular-nums; }
.bulk-chip { font-size: .8rem; border: 1px solid var(--border); border-radius: 10px; padding: 0 6px; }
.bulk-boundary {
  /* In the DOM and focusable at all times; only its opacity follows hover and
     focus. A round 24 px target (2.5.8) centred in the gap. */
  position: absolute; top: 50%; left: calc(-1 * var(--bulk-gap) / 2 - 12px);
  transform: translateY(-50%); z-index: 1;
  width: 24px; height: 24px; min-width: 24px; min-height: 24px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 50%;
  background: var(--surface); color: var(--text); cursor: pointer;
  font: inherit; font-size: .8rem; line-height: 1;
  opacity: 0; transition: opacity .15s;
}
.bulk-boundary.is-cut { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.bulk-tile:hover .bulk-boundary, .bulk-tile:hover + .bulk-tile .bulk-boundary,
.bulk-tile:focus-within .bulk-boundary, .bulk-boundary:focus-visible { opacity: 1; }
.bulk-boundary:focus-visible, .bulk-rotate:focus-visible, .bulk-keep:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.bulk-rotate, .bulk-keep {
  background: none; border: 1px solid var(--border); border-radius: 4px;
  color: inherit; cursor: pointer; font: inherit; font-size: .8rem;
  min-height: 24px; min-width: 24px; padding: 0 6px;
}
/* A page left out of its submission (owner design 2026-10-20): the chip says
   "left out" in words; the red tint and border are the second cue (1.4.1).
   Its leave-out control is shown at rest so the way back is visible; on any
   other page it is revealed by hover or focus, like the boundary control. */
.bulk-tile.is-out {
  background: color-mix(in srgb, var(--red) 14%, var(--surface2));
  border-color: var(--red);
}
.bulk-tile.is-out canvas { opacity: .55; }
.bulk-tile.is-out .bulk-tile-cap { color: var(--text); }
.bulk-chip-out { color: var(--red-text-on-tint); border-color: var(--red); font-weight: 600; }
.bulk-keep { opacity: 0; transition: opacity .15s; }
.bulk-keep.is-out, .bulk-tile:hover .bulk-keep,
.bulk-tile:focus-within .bulk-keep { opacity: 1; }

@media (hover: none) {
  /* No pointer hover on a touch screen: the controls are simply shown. */
  .bulk-boundary { opacity: 1; }
  .bulk-keep { opacity: 1; }
}

@media (max-width: 768px) {
  /* A phone: fewer, narrower columns; the controls keep their 24 px and the
     gap keeps room for them. The threshold row and the review panel's button
     rows wrap instead of widening the dialog (1.4.10). */
  .bulk-groups {
    --bulk-gap: 32px;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); padding-left: 30px;
  }
  .bulk-threshold { flex-wrap: wrap; }
  #bulk-panel-review .flex { flex-wrap: wrap; }
}

@media (prefers-reduced-motion: reduce) {
  /* The fade is the only motion here; removing it leaves the reveal
     instantaneous rather than removing the control. */
  .bulk-boundary { transition: none; }
  .bulk-keep { transition: none; }
}

@media (forced-colors: active) {
  /* Forced-colours mode drops author colours, so the selected/split states
     must be carried by a BORDER (a system-colour-aware property) rather than
     by the accent fill — otherwise selection becomes invisible. */
  .bulk-thumb.is-selected { border-color: Highlight; border-style: solid; }
  .bulk-boundary { opacity: 1; border-color: ButtonBorder; background: ButtonFace; color: ButtonText; }
  .bulk-tile.is-first::before { background: Highlight; }
  .bulk-tile.is-doc::before { background: none; border-left-color: Highlight; }
  .bulk-rail-mark { background: Highlight; }
  .bulk-chip { border-color: ButtonBorder; }
  .bulk-tile.is-over { border-color: Highlight; }
  .bulk-tile.is-out { border-color: Highlight; border-style: dashed; }
  .bulk-keep { opacity: 1; }
  .bulk-thumb:focus-visible, .bulk-boundary:focus-visible,
  .bulk-rotate:focus-visible, .bulk-keep:focus-visible { outline: 2px solid Highlight; }
}
