/* One stylesheet. No framework, no CDN (CLAUDE.md, Conventions). */
:root {
  /* ⚠⚠ `light`, NOT `light dark`, AND THAT IS A STATEMENT OF FACT RATHER THAN A PREFERENCE.
     Declaring `light dark` told the UA to paint a dark canvas and near-white default text in OS
     dark mode — but `body` sets neither `color` nor `background`, and five page-local stylesheets
     hard-code ~30 light backgrounds (`background:#fff`, `#eef1f6`, `#f5f6f8`) with no paired
     `color`. The result, measured in Chrome on 2026-09-17: `.notgrade` on result.html rendered
     rgb(255,255,255) on rgb(238,241,246) — WHITE ON NEAR-WHITE.
     ⚠ The block that disappeared is "This is feedback, not a grade … not usable for progression",
     which exists BECAUSE a reader must not mistake this output for a mark. Its own comment says
     "A statement in a document protects nobody who is reading a screen" — and on a dark-mode
     iPad nobody was reading it. Dark mode is ordinary on tablets; the bench machine is light,
     which is why this survived to here.
     ⚠⚠ OWNER RULING 2026-09-18: NO DARK MODE. Verbatim: "Sperate item...no dark mode." So this
     line is a DECISION and no longer the temporary mitigation it was written as. The app is
     light-only by intent, not merely in fact.
     ⚠ DO NOT "fix" this by restoring `light dark`, and do not pair the ~27 hard-coded
     backgrounds in the page-local stylesheets on its account — that task was measured as
     unnecessary (I-84) and is now permanently out of scope. Read I-84 before touching either.

     ⚠⚠ CORRECTION, 2026-09-17, MEASURED — THE SENTENCE THAT STOOD HERE IS NO LONGER TRUE.
     It read: "Restoring `light dark` requires pairing every hard-coded background with an explicit
     colour, file by file — until then, changing this back re-breaks the student result screen."
     That was true of the code it was written against and was falsified by the fix three
     declarations below it, in this same file, in the same sitting.

     Re-measured in Chrome under dark-scheme emulation with `color-scheme: light dark` forced back
     on, on /static/result.html: `.notgrade` renders rgb(22,24,29) on rgb(238,241,246) — DARK ON
     NEAR-WHITE, legible. So do `.item`, `.warn` and `.stop`. The white-on-near-white does not
     reproduce, because `body` below now declares `color` explicitly and the ~30 unpaired
     backgrounds inherit that ink no matter what scheme the UA picks. The unpaired backgrounds
     were never the cause; BODY DECLARING NO INK WAS, and that is fixed.

     ⚠ So DO NOT spend a session pairing those ~30 backgrounds on the strength of this comment.
     The count is 27 across admin/assign/result/review/validate and it buys nothing today. (Probed
     with class-only selectors; the table-context rules `table.f th`, `tr.credited td` were not
     individually measured, but they inherit the same body ink.)

     ⚠ WHAT RESTORING `light dark` ACTUALLY STILL COSTS, measured the same way — none of it a
     legibility failure today, all of it invisible while this says `light`:
       · `html` declares no background, so the canvas behind body's 3rem margin and max-width
         gutter inverts: a dark frame around a white sheet.
       · UA-painted text inputs and textareas compute rgb(59,59,59) — a dark box on a white page.
         Self-consistent and readable, but wrong, and this app has an ASSESSED form.
       · An unstyled checkbox computes border rgb(255,255,255) against a white body.
       · index.html does not load this stylesheet, still declares `light dark`, and is CORRECT on
         its own terms — so the landing page and every other page disagree in OS dark mode.
     Restoring it is a dark-palette decision about CONTROLS and CANVAS, not a background-pairing
     chore. Filed as I-84. */
  color-scheme: light;
  --line: color-mix(in srgb, currentColor 18%, transparent);
  /* ⚠⚠ A SECOND LINE COLOUR, FOR CONTROLS ONLY, AND THE SPLIT IS THE POINT.
     `--line` composites to 1.46:1 against white — measured, not estimated. WCAG 1.4.11 asks for
     3:1 on the boundary of anything you can operate, so every button and select on every screen
     was below it. MVP-SPEC §12 makes "keyboard operable, visible focus, labelled controls" a
     written requirement; a control whose edge you cannot see fails the spirit of it.
     ⚠ NOT applied to `.card` or `.log li`. 1.4.11 governs CONTROLS, not decorative separators,
     and darkening every card border to match would make the page heavier for no accessibility
     gain — a change that looks like compliance and is only noise. 46% composites to ~3.0:1. */
  --control-line: color-mix(in srgb, currentColor 46%, transparent);
  --warn: #b26a00;
  --error: #b3261e;
  --ok: #1b6b3a;
}

body {
  font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  max-width: 46rem;
  margin: 3rem auto;
  padding: 0 1.25rem;
  /* ⚠⚠ EXPLICIT, BOTH OF THEM. `color-scheme: light` alone was NOT enough and I shipped it
     believing it was — html and body both computed to rgba(0,0,0,0), so the page never painted
     its own canvas and simply inherited whatever was behind it. In dark mode that left BLACK
     TEXT ON A DARK BACKGROUND: strictly worse than the white-on-white it replaced, because the
     white-on-white at least only hit the cards while this hit every heading and every word of
     body copy.
     ⚠ Caught by LOOKING AT THE RENDER after the change, not by reasoning about the change.
     A page that declares a text colour and no background is making a bet on its container. */
  background: #fff;
  color: #16181d;
}

/* ⚠ `<details>` is the fold used for developer output that must stay reachable but must not
   compete with the page's real content — currently the player's self-check block, which sits
   under the Submit button. `cursor: pointer` because a summary that does not look clickable is
   not discovered; the heading goes inline so the fold reads as one line when shut. Native
   `<summary>` is already keyboard-operable and exposes its own expanded state, so nothing here
   adds ARIA — adding `role`/`aria-expanded` by hand would override correct behaviour with a
   copy that has to be kept in sync. */
summary { cursor: pointer; }
summary > h2 { display: inline; margin: 0; }

/* ⚠⚠ `.options` WAS APPLIED IN FOUR PLACES AND DEFINED IN NONE. `rubric-form.js` sets it on the
   wrapper built by `radioGroup`, `checkboxGroup`, `numberWithEscapes` and `scale`; `grep -n
   options static/app.css` returned nothing. So every option group rendered as bare inline
   `<label>`s and wrapped like prose — at narrow widths a long label's text broke onto the next
   line while its own radio stayed on the previous one, which reads as a radio belonging to the
   wrong option. THIS IS THE ASSESSED FORM; a mis-click here is a wrong answer recorded.
   ⚠ Flex, not `display: block` per option: a scale of 1–5 must stay on one row, while a group of
   prose-length labels must not be forced into one. `flex-wrap` gives both from one rule — short
   options sit side by side, long ones take a row each, and either way the break happens BETWEEN
   labels rather than inside one. */
.options { display: flex; flex-wrap: wrap; gap: .5rem 1.15rem; }

/* ⚠ WCAG 2.5.8 wants a 24×24 CSS-pixel target and the UA default checkbox is 13×13. The target
   is not the control — the `<label>` carries `htmlFor`, so the whole label accepts the click, and
   giving it `min-height: 24px` makes the target compliant without a 24px box that would look
   wrong next to 15px text. The control is enlarged to 1.15rem as well, because a 13px box is hard
   to AIM at even when the region around it is live.
   ⚠ `align-items: flex-start` + a hair of top margin, not `center`: a label that wraps to two
   lines would otherwise float its control to the vertical middle, beside no text at all.
   ⚠ `flex: none` so the control never shrinks when a long label competes for the row. */
.options > label {
  display: inline-flex; align-items: flex-start; gap: .45rem;
  min-height: 24px; padding: .15rem 0; cursor: pointer;
}
.options input[type=radio],
.options input[type=checkbox] {
  width: 1.15rem; height: 1.15rem; margin: .1rem 0 0; flex: none;
}

h1 { font-size: 1.45rem; margin-bottom: .25rem; }
h2 { font-size: 1rem; margin: 0 0 .6rem; letter-spacing: .01em; }
.sub { opacity: .7; margin-top: 0; }
.muted { opacity: .72; }

.card {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 1rem 1.1rem;
  margin: 1.1rem 0;
}

button {
  font: inherit;
  padding: .45rem .9rem;
  border-radius: 7px;
  border: 1px solid var(--control-line);
  background: transparent;
  color: inherit;
  cursor: pointer;
}
button:disabled { opacity: .45; cursor: not-allowed; }
button:focus-visible, select:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

select {
  font: inherit;
  width: 100%;
  padding: .3rem;
  border-radius: 7px;
  border: 1px solid var(--control-line);
  background: transparent;
  color: inherit;
}

.badge {
  display: inline-block;
  padding: .3rem .7rem;
  border-radius: 999px;
  border: 1px solid currentColor;
  font-size: .85rem;
  font-weight: 600;
}
.badge.unverified { color: var(--warn); }
.badge.lost { color: var(--error); }
.badge.verified { color: var(--ok); }

.log {
  list-style: none;
  padding: 0;
  margin: .8rem 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82rem;
  max-height: 18rem;
  overflow-y: auto;
}
.log li { padding: .18rem 0; border-bottom: 1px solid var(--line); }
.log li.ok { color: var(--ok); }
.log li.warn { color: var(--warn); }
.log li.error { color: var(--error); font-weight: 600; }

code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; }

dt { font-weight: 600; margin-top: .75rem; }
dd { margin: 0 0 0 1rem; opacity: .85; }

/* --- field errors: reserve the line ALWAYS (I-42) -------------------------------------
 *
 * ⚠ THE OPERATOR HIT THIS TWICE ON THE BENCH: "the screen jumped back to first response
 * when filling in second response." `.field-error` had no rule at all and relied on the
 * `hidden` attribute's `display: none`, so every time an item autosaved, got a 422 — the
 * normal state of a form somebody is still filling in — and painted its errors, that item
 * GREW. Everything below shifted down, the browser kept its scroll offset, and the student
 * was left typing into a field that had moved off screen.
 *
 * Anchoring the scroll after the fact was not enough: the save-state label repaints after
 * the errors do, so a correction lands and is then undone a few milliseconds later.
 *
 * So the space is reserved instead — the element keeps its line whether or not it has text,
 * and `visibility` rather than `display` does the hiding. Nothing changes size, so there is
 * nothing to correct. ⚠ A long message that wraps to two lines can still add one line; the
 * messages are short by design and that is a far smaller movement than a whole card.
 */
.field-error {
  color: var(--error);
  font-size: .9em;
  margin: .15rem 0 0;
  min-height: 1.35em;
}

/* Beat the UA stylesheet's `[hidden] { display: none }` — the element must keep occupying
 * its line. `visibility` hides it without taking the space back. */
.field-error[hidden] {
  display: block;
  visibility: hidden;
}

/* --- error, warning and hint text that is actually styled (review finding, 2026-09-19) -------
 *
 * ⚠⚠ `.bad`, `.help` and `.meta` MATCHED NO RULE ANYWHERE IN THIS FILE, and modules had been
 * setting them for weeks. Counted: `.bad` 0 rules, `.help` 0, `.meta` 0 — while `.stop` had one
 * and `.warn` two. The only `bad` rule in the project was `.hint.bad`, scoped to the assign
 * screen, so `el.className = 'card bad'` on the hub, `'meta error'` on the people screen and the
 * `.stop` refusal `gateSession` writes on nine pages all rendered as ORDINARY PROSE.
 *
 * ⚠ An error that is not styled as an error is not read as one. On `password.html` — the page a
 * user reaches holding a temporary password, where every message is a refusal they must act on —
 * NOT ONE of them was distinguishable from body text.
 *
 * ⚠ Nothing ties a className used in JS to a rule in this file, so this will drift again. The
 * honest guard is a test that greps the modules for `className`/`classList` literals and fails
 * on any that this stylesheet does not define. It is NOT built, and is recorded as such.
 */
.bad,
.error { color: var(--error); font-weight: 600; }

.warn.meta,
.meta.warn { color: var(--warn); }

/* Secondary explanatory text. Set by four modules and by several pages' markup. */
.meta,
.help { font-size: .88rem; opacity: .8; }

/* ⚠ `.meta` and `.help` must not swallow the colour of an error placed on the same element:
 * `class="meta error"` and `class="help bad"` are both used, and the colour has to win. */
.meta.error,
.meta.bad,
.help.error,
.help.bad { color: var(--error); opacity: 1; font-weight: 600; }
