/* ================================================================================
   grader-ui PRODUCT CSS (everything here is grader-ui's own, on grain's tokens).
   The GRAIN theme + every component class comes from theme.css (baked from
   @tjakoen/grain by scripts/bake-theme.mjs). Mirror of the product-owned block in
   src/build-dashboard.mjs. Two kinds of rules live here, nothing else:
   1. Layout/composition grain leaves to the parent (the wide page column, the stats
      grid, card spacing, the review drawer, the matrix's sticky column).
   2. The documented monochrome exceptions (docs/grain-for-ai.md): a grading matrix
      genuinely needs status hues, heat cells, and syntax tint — grain is monochrome
      by doctrine, so these stay product-owned and never go upstream.
   ================================================================================ */
:root{--wrap-max:1500px;
  --good:#3f6d3a;--warn:#8a6410;--bad:#a12f22;--held:#5a3fa0;--tc:#6a7a52;--ts:#9a5b2a;--tn:#4a7a4a;--tk:#3a5a8a;--tg:#5a7a6a}
@media(prefers-color-scheme:dark){:root:not([data-color-scheme=light]){--good:#7fae70;--warn:#d0a24a;--bad:#e08a7a;--held:#b79af0;--tc:#9ab07a;--ts:#d0a07a;--tn:#a0c090;--tk:#8aa0d0;--tg:#8ab0a0}}
:root[data-color-scheme=dark]{--good:#7fae70;--warn:#d0a24a;--bad:#e08a7a;--held:#b79af0;--tc:#9ab07a;--ts:#d0a07a;--tn:#a0c090;--tk:#8aa0d0;--tg:#8ab0a0}
/* page column — a review matrix needs the full window, not grain's 768px reading column */
.wrap{max-width:var(--wrap-max);margin:0 auto;padding:var(--space-5) var(--space-4)}
h1{font-size:var(--text-2xl);margin:0 0 var(--space-1)}
/* tab strips (grain tab/tab-bar) — parent chrome: a quiet band so the active tab's
   merge-into-the-page read works outside an app-shell topbar */
.wrap .tab-bar,.dp .tab-bar{flex:0 1 auto;background:var(--paper-2);border-bottom:1px solid var(--color-line);margin-top:var(--space-4)}
.tab-bar .tab{cursor:pointer}
.dp .tab-bar{display:inline-flex;margin:0 0 var(--space-3)}
/* the review drawer's Screenshots/Code switch renders its tabs as real <button>s */
button.tab{background:none;border:0;border-right:1px solid var(--color-line)}
button.tab:disabled{opacity:.4;cursor:not-allowed}
/* stats strip — parent layout for grain stat tiles */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));gap:var(--space-3);margin:var(--space-4) 0}
/* cards — page-level spacing (parent-owned) */
.wrap>.card,#matrixcard{margin:var(--space-4) 0}
.card>.table-scroll:last-child,.card>.table-scroll:last-child .table{margin-bottom:0}
/* matrix — sticky student column + dense numeric cells */
.matrix th,.matrix td{white-space:nowrap}
.matrix th:first-child,.matrix td:first-child{padding-left:var(--space-2)}
.matrix td.stu,.matrix th.stu{position:sticky;left:0;background:var(--paper);z-index:1;max-width:230px;overflow:hidden;text-overflow:ellipsis}
.matrix th.stu{z-index:3;background:var(--paper-2)}
.cell{cursor:pointer;text-align:center;font-variant-numeric:tabular-nums}
.cell:hover,.cell:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}
/* heat cell: hue from the score fraction (--pct, 0..1), computed in CSS so an OS
   light/dark flip recolors without a repaint (documented monochrome exception) */
.cell[style*="--pct"]{background:hsl(calc(var(--pct)*120) 55% 90%)}
@media(prefers-color-scheme:dark){:root:not([data-color-scheme=light]) .cell[style*="--pct"]{background:hsl(calc(var(--pct)*120) 30% 24%)}}
:root[data-color-scheme=dark] .cell[style*="--pct"]{background:hsl(calc(var(--pct)*120) 30% 24%)}
/* attendance presence cells are display-only — do not look clickable */
.attcell{text-align:center;font-variant-numeric:tabular-nums}
/* sortable column headers (Students) — keyboard-operable, so show the affordance */
.matrix th.sortable{cursor:pointer;user-select:none}
.matrix th.sortable:hover{color:var(--color-accent)}
.matrix th.sortable:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}
tr[data-s]{cursor:pointer}
/* status hues on grain's badge — tone is a data-attribute, hue is the documented exception */
.badge[data-tone]{white-space:nowrap}
.badge[data-tone=good]{color:var(--good);border-color:var(--good)}
.badge[data-tone=warn]{color:var(--warn);border-color:var(--warn)}
.badge[data-tone=bad]{color:var(--bad);border-color:var(--bad)}
.badge[data-tone=held]{color:var(--held);border-color:var(--held)}
.badge[data-tone=quiz]{color:var(--tk);border-color:var(--tk)}
.badge[data-tone=muted]{color:var(--color-muted)}
/* override decision — accent hue, distinct from the AI-lane "held" purple */
.badge[data-tone=ov]{color:var(--color-accent);border-color:var(--color-accent)}
/* review meter decision-mix segments (grain ships only ok/bad/pending; the
   grading status hues are the documented monochrome exception) */
.meter__seg[data-tone=good]{background:var(--good)}
.meter__seg[data-tone=ov]{background:var(--color-accent)}
.meter__seg[data-tone=warn]{background:var(--warn)}
.mut{color:var(--color-muted)}.rt{text-align:right}.center{text-align:center}
.pill{font-size:var(--text-xs);color:var(--color-muted);font-weight:var(--font-weight-regular)}
.tot{font-weight:var(--font-weight-bold);font-variant-numeric:tabular-nums}
.legend{display:flex;gap:var(--space-4);flex-wrap:wrap;color:var(--color-muted);font-size:var(--text-xs);margin:var(--space-2) 0}
/* control row widths — the field atoms are flex:1 by default; pin the loose ones */
input.search{width:15rem;flex:none}
input.num{width:4.5rem;flex:none}
.ctl{display:flex;gap:var(--space-2);align-items:center;flex-wrap:wrap;margin-top:var(--space-2)}
.hdr-actions{float:right;display:flex;gap:var(--space-2);align-items:center}
.meter{margin-top:var(--space-3)}
/* review drawer — product-owned (grain has no side-sheet organism) */
.drawer{position:fixed;inset:0;background:color-mix(in srgb,var(--ink) 45%,transparent);display:none;z-index:50}
.drawer.on{display:block}
.dp{position:absolute;right:0;top:0;height:100%;width:min(720px,94vw);background:var(--paper);border-left:1px solid var(--color-line);overflow:auto;padding:var(--space-5)}
/* the same side-sheet as a native <dialog> (showModal traps focus, Escape fires
   cancel, ::backdrop dims): the dialog element IS the right sheet, so the inner
   .dp drops its absolute positioning and just carries the padding. */
dialog.drawer-modal{position:fixed;margin:0;inset:0 0 0 auto;height:100dvh;max-height:100dvh;width:min(720px,94vw);max-width:96vw;border:0;border-left:1px solid var(--color-line);background:var(--paper);color:inherit;padding:0;overflow:auto;z-index:50}
dialog.drawer-modal::backdrop{background:color-mix(in srgb,var(--ink) 45%,transparent)}
dialog.drawer-modal .dp{position:static;right:auto;top:auto;width:auto;height:auto;min-height:100%;border-left:0;box-sizing:border-box}
.dp h3{margin:0 0 var(--space-1)}
.dp .x{float:right;cursor:pointer;color:var(--color-muted);font-size:var(--text-xl);border:0;background:none}
.dp.wide{width:min(1600px,90vw)}
.rvhead{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-1)}
.rvnav{margin-left:auto;display:flex;align-items:center;gap:var(--space-2)}
.rvnav .cnt{color:var(--color-muted);font-size:var(--text-xs);font-variant-numeric:tabular-nums;min-width:52px;text-align:center}
.rev2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:var(--space-4);margin-top:var(--space-3)}
@media(max-width:900px){.rev2{grid-template-columns:1fr}}
.rvcol{min-width:0}
.decision{display:flex;gap:var(--space-2);flex-wrap:wrap;align-items:center}
/* screenshot panes */
.shots{display:flex;flex-direction:column;gap:var(--space-3)}
.shot{border:1px solid var(--color-line);border-radius:var(--radius-md);overflow:hidden;background:var(--paper-2)}
.shot .cap{font-size:var(--text-xs);color:var(--color-muted);padding:var(--space-1) var(--space-2);border-bottom:1px solid var(--color-line);text-transform:uppercase;letter-spacing:.04em}
.shot img{display:block;width:100%;height:auto;cursor:zoom-in}
.noshot{border:1px dashed var(--color-line);border-radius:var(--radius-md);padding:var(--space-6);text-align:center;color:var(--color-muted);font-size:var(--text-sm)}
/* prompt/note blocks ride grain's code-block; prompts are prose-ish, so they wrap */
.prompt{white-space:pre-wrap;word-wrap:break-word;font-size:var(--text-xs)}
/* source viewer — code-block + a cap height and the syntax tint (documented exception) */
.codepre{white-space:pre;max-height:74vh;font-size:var(--text-xs);tab-size:2}
.codepre .tc{color:var(--tc);font-style:italic}.codepre .ts{color:var(--ts)}.codepre .tn{color:var(--tn)}.codepre .tk{color:var(--tk)}.codepre .tg{color:var(--tg)}
select.cfile{margin-bottom:var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs)}
/* feedback editors — grain fields; product owns only sizing and the mono variant */
.dp .field{margin-top:var(--space-3)}
textarea.fta{resize:vertical;line-height:var(--leading-relaxed)}
textarea.fta.mono{font-family:var(--font-mono);font-size:var(--text-xs);line-height:1.5}

/* ---- hosted-shell additions (boot + settings, product-owned to the Pages build) ---- */
.boot{padding:var(--space-6);text-align:center;color:var(--color-muted);font-size:var(--text-base)}
#bootmsg a{color:var(--color-accent)}

/* ---- Scan tab (absorbed attendance scanner) ---- */
/* GRAIN collapses success/danger to ink by design; a door scanner needs the fast
   green/red read at a glance, so we re-point those token slots (the supported way
   to theme GRAIN) - the rest of the palette stays untouched. */
.scanview{--scan-ok:#137333;--scan-ok-soft:color-mix(in srgb,#137333 16%,transparent);
  --scan-bad:#b00020;--scan-bad-soft:color-mix(in srgb,#b00020 16%,transparent);
  display:flex;flex-direction:column;gap:var(--space-4);max-width:640px}
[data-color-scheme="dark"] .scanview{--scan-ok:#5cd07a;--scan-bad:#ff6b6b}
/* OS dark mode with no explicit override: a first-run phone in the dark would
   otherwise show the light (dark-green) status color on dark paper (~2.8:1),
   defeating the at-a-glance read. Mirrors the not([data-color-scheme=light])
   guard used for the palette twins above. */
@media(prefers-color-scheme:dark){:root:not([data-color-scheme=light]) .scanview{--scan-ok:#5cd07a;--scan-bad:#ff6b6b}}
/* Camera: a size-capped SQUARE viewport (not a full-width video), centered,
   with a reticle overlay - the framing teachers scan against. object-fit:cover
   only crops the DISPLAY; scan.mjs still decodes the full camera frame. */
.scan-cam{position:relative;width:100%;max-width:20rem;aspect-ratio:1/1;margin:0 auto;
  border:1px solid var(--color-line);border-radius:var(--radius-md);overflow:hidden;background:var(--paper-2)}
.scan-cam .scan-video{width:100%;height:100%;object-fit:cover;display:block}
/* Centered reticle; flashes to the status color on each scan (green ok / red
   bad / muted dup) then settles back to a neutral frame. The big spread shadow
   dims everything outside the square (clipped by the container's overflow). */
.scan-reticle{position:absolute;inset:16%;border:3px solid color-mix(in srgb,#fff 78%,transparent);
  border-radius:var(--radius-md);box-shadow:0 0 0 100vmax color-mix(in srgb,#000 26%,transparent);
  transition:border-color .15s ease,box-shadow .15s ease;pointer-events:none}
.scan-cam.ok .scan-reticle{border-color:var(--scan-ok);box-shadow:0 0 0 100vmax color-mix(in srgb,var(--scan-ok) 24%,transparent)}
.scan-cam.bad .scan-reticle{border-color:var(--scan-bad);box-shadow:0 0 0 100vmax color-mix(in srgb,var(--scan-bad) 22%,transparent)}
.scan-cam.dup .scan-reticle{border-color:var(--color-muted)}
.scan-status{padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-weight:var(--font-weight-semibold)}
.scan-status.ok{background:var(--scan-ok-soft);color:var(--scan-ok)}
.scan-status.dup{background:var(--paper-2);color:var(--color-muted)}
.scan-status.bad{background:var(--scan-bad-soft);color:var(--scan-bad)}
/* layout/composition only (grain owns the components): tokens, no raw values */
.scan-stack{display:flex;flex-direction:column;gap:var(--space-3)}
.scan-stack p,.scan-stack .status-list{margin:0}
.scan-stack[hidden]{display:none}
.scan-status[hidden]{display:none}
.scan-row{display:flex;gap:var(--space-2);align-items:flex-end;flex-wrap:wrap}
.scan-row>.field{flex:1;min-width:0}
.scan-row>.field .field__input,.scan-row>.field .field__select{width:100%;min-width:0}
.scan-row>.field__input{flex:1;min-width:0}
.scan-fixed{flex:0 0 auto}
.scan-row--center{align-items:center}

/* ---- app-shell era (LMS layout) ---- */
/* skip-to-content link: off-screen until focused, then pinned top-left */
.skip-link{position:fixed;left:var(--space-2);top:-4rem;z-index:100;background:var(--paper);color:var(--color-accent);border:1px solid var(--color-accent);border-radius:var(--radius-sm);padding:var(--space-2) var(--space-3);text-decoration:none;transition:top .12s ease}
.skip-link:focus{top:var(--space-2)}
/* the Tour nav-item is a real <button>; reset it to look like the sibling links */
.nav-item--btn{border:0;background:none;width:100%;cursor:pointer;font:inherit;text-align:left}
.app-shell__main{overflow:auto}
/* the SPA route-focus target must not draw a focus ring on the scroll container */
.app-shell__main:focus{outline:none}
.app-shell__main .wrap{margin:0;max-width:var(--wrap-max)}
.app-shell__topbar{display:flex;align-items:center;gap:var(--space-3)}
.app-shell__topbar .tab-bar{flex:1 1 auto;min-width:0;margin:0;border-bottom:0;background:transparent}
.topbar-ctl{display:flex;align-items:center;gap:var(--space-2);margin-left:auto}
.window-bar__search .kbd{margin-left:var(--space-2)}
#statusBar{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-muted)}
.side-rail__group{display:flex;flex-direction:column;border-top:1px solid var(--color-line);margin-top:var(--space-2);padding-top:var(--space-2)}
.side-rail .navheld{margin-left:auto}
/* dashboard cards */
.dashgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--space-3);margin-top:var(--space-4)}
.dashcard{text-decoration:none;color:inherit;display:flex;flex-direction:column;gap:var(--space-2)}
.dashcard h2{margin:0;font-size:var(--text-lg)}
.stats--mini{display:flex;gap:var(--space-3)}
.stats--mini .stat__value{font-size:var(--text-lg)}
/* scanner standalone page chrome */
.scanner-page{max-width:640px;margin:0 auto;padding:0 var(--space-4) var(--space-6)}
.scanner-page .appbar{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;padding:var(--space-4) 0;border-bottom:1px solid var(--color-line);margin-bottom:var(--space-4)}
.scanner-page .appbar__title{font-size:var(--text-lg);font-weight:var(--font-weight-semibold)}
.scanner-page .appbar .spacer{flex:1}
.rowlink{cursor:pointer}
.rowlink:hover td{background:var(--paper-2)}
.ctl .chips{display:inline-flex;gap:var(--space-2);align-items:center}
/* ops + confirm dialogs + docked feed */
.opgroup{margin:var(--space-5) 0 var(--space-2)}
.opcard{margin-bottom:var(--space-3)}
.ophead{display:flex;gap:var(--space-2);align-items:baseline;flex-wrap:wrap}
.ophead .oprun{margin-left:auto}
.opdesc{margin:var(--space-1) 0}
.opform{display:flex;gap:var(--space-2);align-items:flex-end;flex-wrap:wrap}
/* cap field widths so a lone text/choice input does not flex to an absurd width */
.opform .opf{margin:0;min-width:140px;max-width:24rem}
.opform .opf[style*="width:100%"]{flex:1 1 100%;max-width:none}
/* the bool gate rides a switch; align it to the field baselines */
.opform .opsw{align-self:center}
/* pin RUN to the end of its line instead of letting it wander after the inputs */
.opform .oprunbtn{margin-left:auto;align-self:flex-end}
.confirm-dialog{border:1px solid var(--color-line);border-radius:var(--radius-md);background:var(--paper);color:inherit;padding:var(--space-4);max-width:min(640px,92vw);max-height:85vh;display:flex;flex-direction:column}
.confirm-dialog::backdrop{background:color-mix(in srgb,var(--ink) 35%,transparent)}
.confirm-dialog h3{margin:0 0 var(--space-2)}
/* the diff/pre is the only growable part: it scrolls inside the dialog so the
   title, summary, and Commit/Cancel buttons stay on screen at any file size */
.confirm-dialog pre{overflow:auto;min-height:0}
#opConsole .console__feed{max-height:180px;overflow:auto;font-family:var(--font-mono);font-size:var(--text-xs);padding:var(--space-2) var(--space-3)}
#opConsole .console__bar{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-3);border-top:1px solid var(--color-line)}
.opline{padding:2px 0}
/* grouped class rail */
/* subject group label: starts past the rail's icon-gutter hairline, like grain's
   own sub items, so nothing straddles the line */
.side-rail__grouplabel{font-size:var(--text-xs);letter-spacing:.06em;text-transform:uppercase;color:var(--color-muted);padding:var(--space-2) var(--space-2) 2px calc(var(--rail-icon-col,2.5rem) + var(--space-2));white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
[data-rail-collapsed="true"] .side-rail__grouplabel{display:none}
/* mill-rendered flag bodies */
.flagbody .note{padding:0}

/* ===== Phase D: pipeline legibility ===== */
/* AI Review stage header: Generate -> Review -> Apply -> Deliver stepper */
.stepper{display:flex;flex-wrap:wrap;gap:var(--space-1);list-style:none;margin:0 0 var(--space-3);padding:0;font-size:var(--text-xs)}
.stepper__step{display:flex;align-items:center;gap:var(--space-1);color:var(--color-muted)}
.stepper__step:not(:last-child)::after{content:"";width:1.5rem;height:1px;background:var(--color-line);margin:0 var(--space-1)}
.stepper__dot{display:inline-flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;border-radius:50%;border:1px solid var(--color-line);font-weight:var(--font-weight-semibold);font-size:var(--text-xs)}
.stepper__step[data-state=done]{color:var(--good)}
.stepper__step[data-state=done] .stepper__dot{border-color:var(--good);color:var(--good)}
.stepper__step[data-state=active]{color:var(--color-fg)}
.stepper__step[data-state=active] .stepper__dot{border-color:var(--color-accent);color:var(--color-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-accent) 22%,transparent)}
.stepper__label{font-weight:var(--font-weight-medium)}
.revbar{display:flex;justify-content:space-between;align-items:center;gap:var(--space-3);flex-wrap:wrap}
.revbar__info{min-width:0}
.revbar__act{display:flex;gap:var(--space-2);align-items:center;flex-shrink:0}
/* overflow menu (native <details>, no extra JS for open/close) */
.ovmenu{position:relative;display:inline-block}
.ovmenu>summary{list-style:none;cursor:pointer}
.ovmenu>summary::-webkit-details-marker{display:none}
.ovmenu__list{position:absolute;right:0;top:calc(100% + 4px);z-index:20;min-width:15rem;display:flex;flex-direction:column;background:var(--paper);border:1px solid var(--color-line);border-radius:var(--radius-md);box-shadow:0 6px 20px rgb(0 0 0/.14);padding:var(--space-1)}
.ovmenu__item{text-align:left;background:none;border:0;color:inherit;font:inherit;padding:var(--space-2) var(--space-2);border-radius:var(--radius-sm);cursor:pointer}
.ovmenu__item:hover,.ovmenu__item:focus-visible{background:var(--paper-2)}
.ovmenu__item--danger{color:var(--bad)}
/* one-line consequence + warn banner + post-Send next-step card in the drawers */
.consequence{margin:var(--space-2) 0 0;border-left:2px solid var(--color-line);padding-left:var(--space-2)}
.warnline{margin:var(--space-2) 0 0;padding:var(--space-2);border-left:3px solid var(--warn);background:color-mix(in srgb,var(--warn) 10%,transparent);border-radius:var(--radius-sm);font-size:var(--text-sm)}
.nextstep{border-left:3px solid var(--good)}
.nextstep__steps{margin-top:var(--space-1)}
.mono{font-family:var(--font-mono)}
/* pending-intents strip mark */
.pibody .status-list__meta{white-space:nowrap}
/* review detail: keyboard legend + collapse the empty media pane */
.kbdlegend{margin:var(--space-2) 0 0;display:flex;gap:var(--space-1);align-items:center;flex-wrap:wrap}
.kbdlegend kbd{font-family:var(--font-mono);font-size:var(--text-xs);border:1px solid var(--color-line);border-bottom-width:2px;border-radius:var(--radius-sm);padding:0 .35em;background:var(--paper-2)}
.rev2[data-solo=true]{grid-template-columns:minmax(0,1fr)}
/* transient toast (approve-and-advance confirmation) */
#toast{position:fixed;left:50%;bottom:calc(var(--space-5) + 24px);transform:translateX(-50%) translateY(8px);z-index:80;background:var(--ink);color:var(--paper);font-size:var(--text-sm);padding:var(--space-2) var(--space-4);border-radius:var(--radius-md);box-shadow:0 6px 20px rgb(0 0 0/.2);opacity:0;pointer-events:none;transition:opacity .18s ease,transform .18s ease}
#toast[data-on=true]{opacity:1;transform:translateX(-50%) translateY(0)}
@media(prefers-reduced-motion:reduce){#toast{transition:none}}
/* openNote drawer: split, wrapping halves */
.notewrap{display:flex;flex-direction:column;gap:var(--space-3);margin-top:var(--space-2)}
.notehalf h4{margin:0 0 var(--space-1);font-size:var(--text-sm)}
.notehalf--instr{border-top:1px solid var(--color-line);padding-top:var(--space-3)}
.noteprose{white-space:pre-wrap;word-wrap:break-word;font-size:var(--text-sm);line-height:1.5}
.notehalf--instr .noteprose{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-muted)}
/* ---- demo mode ---- */
/* Loud but not shouty: the offer card at the front door, the banner on the
   dashboard, and a rail tag so the mode is legible from any view. */
.demoOffer{margin-top:var(--space-4);padding:var(--space-3);border:1px solid var(--color-line);border-left:3px solid var(--color-accent);border-radius:var(--radius-md);background:var(--paper-2);font-size:var(--text-sm);line-height:1.55}
.demoOffer .btn{margin-top:var(--space-2)}
.demoBanner{border-left:3px solid var(--color-accent)}
.demoBanner .ctl{margin-top:var(--space-2)}
.side-rail__brand .demoTag{margin-left:auto;text-transform:uppercase;letter-spacing:.04em}
[data-demo] #exitDemo{color:var(--bad)}
