@import url("tokens.css");

/* One visual family everywhere — same palette, same shapes, same type.
   Corners are not set here any more. This block used to give the student
   surface 18/12/11px and teacher mode 14/10/8px, which is exactly what Uri's
   Work Plan item 113 names: "Teacher cards and student cards still use
   different corner radii." Both surfaces now take the one scale in tokens.css
   (--r-card, --r-sm, --r-btn, --r-xs, --r-pill). --r stays as an alias for
   --r-card so its existing call sites keep working. Buttons keep their own
   radius rather than borrowing the pill: fully round controls read as
   toy-like, and pills survive where the shape means something — tags, status
   chips, meter tracks.

   Teacher mode still tightens the rhythm, just from the same spacing scale
   (item 114: "8, 16, 24, 32 and 48px"): a smaller step for card padding and
   row gaps, and no celebratory flourish. It should read as the pragmatic
   surface of the same app, not a different product. Monospace survives only
   for diagnostic readouts. */
body {
  --r: var(--r-card);
  --font-body: var(--font-play);
  --font-head: var(--font-play);
  --head-track: -0.01em;
  --card-pad: var(--sp-24);
  --row-gap: var(--sp-16);
}
body[data-mode="teacher"] {
  --card-pad: var(--sp-16);
  --row-gap: var(--sp-8);
}

/* ── Base ──────────────────────────────────────────────────────────────── */

* { box-sizing: border-box; }

/* ── Scrollbars ─────────────────────────────────────────────────────────────
   The last piece of chrome still borrowed from the OS. On Windows in dark mode
   the native bar renders as a near-black slab against surfaces that are nowhere
   near black, which reads as a rendering fault rather than as a control.

   Both syntaxes on purpose, and they do not fight: a browser that supports the
   standard properties ignores the ::-webkit- rules entirely, and one that does
   not falls back to them. Colours come from the same border/foreground tokens
   as everything else, so dark mode needs no separate rule.

   On * rather than on the two known scrollers, because the ones worth styling
   are the ones nobody predicted — a long class list, an overflowing dialog. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
/* Keep content width constant as an editor, list or dialog starts overflowing.
   On mobile the document scrolls; desktop teacher panels own their scroll. */
html, .t-scroll, .t-folders, .modal { scrollbar-gutter: stable; }
@media (min-width: 821px) {
  html:has(body[data-mode="teacher"]) { scrollbar-gutter: auto; }
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  /* Transparent border plus content-box clipping is what insets the thumb from
     the channel. Padding does not exist on this pseudo-element. */
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: var(--r-pill);
}
::-webkit-scrollbar-thumb:hover {
  background: var(--fg-muted);
  background-clip: content-box;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--fs-16);
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
}

h1, h2, h3 {
  font-family: var(--font-head);
  letter-spacing: var(--head-track);
  margin: 0;
}
h1 { font-weight: 700; }
h2, h3 { font-weight: 650; }

[hidden] { display: none !important; }

.shell {
  width: 100%;
  max-width: 620px;
  margin: 0 auto;
  padding: var(--sp-24) var(--sp-16) calc(var(--sp-24) + env(safe-area-inset-bottom, 0px));
}
.shell-narrow { max-width: 760px; }

/* One focus cue for the whole app — a soft ring, never a stacked outline.
   The outline is suppressed for EVERYTHING, so anything missing from the list
   below has no visible focus at all rather than a browser default: links,
   text inputs and <details> summaries were all in that gap. Between them that
   is "See a demo", "Send feedback", every "How this was checked" disclosure,
   and the confirm field on deleting a student's data.

   Kept as a flat list rather than :is(), which would raise the specificity
   above the per-field rings (.q-text, .kp-input, .rv-mark-input) that
   deliberately draw their own. */
:focus { outline: none; }
:focus-visible { outline: none; }
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
summary:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}
/* The ring needs a corner to follow on the two that are not already boxes. */
a:focus-visible, summary:focus-visible { border-radius: var(--r-btn); }

/* ── Top bar ───────────────────────────────────────────────────────────── */

/* Session chrome only. With the wordmark gone the bar has no resting state:
   it exists while a student session or the demo needs announcing, and is not
   in the layout at all otherwise — display, not visibility, so the gate and
   teacher views get the full height back. */
.topbar {
  display: none;
  align-items: center;
  gap: var(--sp-16);
  padding: var(--sp-8) var(--sp-16);
  border-bottom: 1px solid var(--border);
  background: var(--card);
}
body[data-student="1"] .topbar,
body[data-demo="1"] .topbar,
body[data-preview="1"] .topbar { display: flex; }

/* ── Cards ─────────────────────────────────────────────────────────────── */

.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--card-pad);
  box-shadow: var(--shadow-card);
}

.card-hero { text-align: center; padding: var(--sp-32) var(--sp-24); }

.hero-emoji {
  font-size: clamp(3rem, 12vw, 4.5rem);
  line-height: 1;
  margin-bottom: var(--sp-8);
}
/* Still. It used to bob forever on every hero card, the one piece of motion in
   the product that answered no event — the Work Plan's "Random animations".
   The settling screen had already switched it off because an endlessly bobbing
   sprig worked against lowering the temperature before a child speaks; the
   same reasoning holds on every screen, so the float is gone rather than
   switched off one card at a time. */

.eyebrow {
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-12);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-teal);
  margin: 0 0 var(--sp-8);
}
.hero-note { color: var(--fg-muted); margin: var(--sp-8) 0 var(--sp-24); }

/* Screens rise in; distance scales with the viewport, not a fixed px. */
.screen { animation: rise 0.5s var(--ease-out) both; }
@keyframes rise {
  from { opacity: 0; transform: translateY(clamp(8px, 2vh, 20px)); }
  to   { opacity: 1; transform: none; }
}

/* ── Buttons ───────────────────────────────────────────────────────────── */

/* The signature press: a solid face over a darker underside that compresses. */
.btn {
  --btn-face: var(--card);
  --btn-under: var(--under-surface);
  --btn-ink: var(--fg);
  --btn-lift: 4px;
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-16);
  letter-spacing: 0.01em;
  color: var(--btn-ink);
  background: var(--btn-face);
  border: 0;
  border-radius: var(--r-btn);
  padding: var(--sp-16) var(--sp-24);
  cursor: pointer;
  box-shadow: 0 var(--btn-lift) 0 var(--btn-under);
  transition: transform 0.11s var(--ease-out), box-shadow 0.11s var(--ease-out),
              filter 0.15s ease;
}
.btn:hover:not(:disabled) { filter: brightness(1.04); }
.btn:active:not(:disabled) {
  transform: translateY(var(--btn-lift));
  box-shadow: 0 0 0 var(--btn-under);
}
.btn:focus-visible { box-shadow: 0 var(--btn-lift) 0 var(--btn-under), 0 0 0 var(--ring-w) var(--ring); }
.btn:active:focus-visible { box-shadow: 0 0 0 var(--btn-under), 0 0 0 var(--ring-w) var(--ring); }
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  --btn-face: var(--bg-soft);
  --btn-under: var(--under-off);
  --btn-ink: var(--fg-muted);
}

/* Same reasoning as .btn-solid-quiet: the accent fills are light in both
   themes, so white text on them never reaches a readable ratio. */
/* Teal, not grass. This was the loudest of the leftover greens — "Try the demo",
   "Start", "I'm ready", "Next phrase" are the buttons a person is most likely to
   press in this product, and they were all wearing a colour the brand sheets do
   not contain. Green survives in this file only where it is paired against berry
   to mean right-versus-wrong; an action is not an answer.

   It lands on the same pair as .btn-primary, and that is the point rather than a
   collision: there is one primary action colour now instead of two competing for
   the same job. Both names stay because the markup distinguishes a playful
   student action from a modal's confirm, and that distinction may earn a shape
   or a size later even though it no longer earns a hue. */
/* .btn-go and .btn-primary resolve to the same three values today, and that is
   deliberate rather than an oversight waiting to be tidied.

   They mean different things. .btn-go is forward motion inside a task — Start,
   I'm ready, Next phrase, Try the demo. .btn-primary is the committing action of
   a form or a dialog — Join, Email me a link, I understand. Those diverged once
   (go was grass, primary was teal) and could again; collapsing them now would
   throw away the distinction and make the next person invent it back under a
   worse name.

   What was actually wrong was that the sameness was silent. It is not now. */
.btn-go     { --btn-face: var(--c-teal);  --btn-under: var(--under-teal);  --btn-ink: var(--ink-on-bright); }
.btn-danger { --btn-face: var(--c-berry); --btn-under: var(--under-berry); --btn-ink: var(--ink-on-deep); }
/* The modal had only a destructive primary. Signing in is not destructive, and
   a red "Email me a link" reads as a warning about the thing you just chose. */
/* Navy ink, for the reason stated two rules up and now measured: white on the
   brand teal is 2.56:1 in light and 2.04:1 in dark, which fails even the 3:1
   asked of a graphical control. Navy on the same fill is 6.55 and 8.24. Berry
   above keeps white because it is the one fill dark enough to carry it. */
.btn-primary { --btn-face: var(--c-teal); --btn-under: var(--under-teal); --btn-ink: var(--ink-on-bright); }
.btn-ghost  { --btn-face: transparent; --btn-under: transparent; --btn-ink: var(--fg-muted);
              box-shadow: inset 0 0 0 2px var(--border); }
/* .btn:active clears box-shadow to drop the underside, which on a ghost button
   takes its ONLY edge with it — the control vanishes for the length of the
   press. It has no underside to drop, so it keeps the inset outline. */
.btn-ghost:active:not(:disabled) { transform: translateY(2px); box-shadow: inset 0 0 0 2px var(--border); }
.btn-ghost:active:focus-visible { box-shadow: inset 0 0 0 2px var(--border), 0 0 0 var(--ring-w) var(--ring); }
.btn-ghost:focus-visible { box-shadow: inset 0 0 0 2px var(--border), 0 0 0 var(--ring-w) var(--ring); }

/* Sticky action bar: sits in flow, so it reserves its own room. One rule owns
   the bottom gutter + safe-area inset so nothing ever sits flush to the edge. */
.actionbar {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: var(--sp-8);
  margin-top: var(--sp-16);
  padding: var(--sp-16) 0 calc(var(--sp-16) + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to top, var(--bg) 55%, transparent);
}
.actionbar .btn { flex: 1; }
.actionbar .btn-ghost { flex: 0 1 32%; }

/* ── Stats strip ───────────────────────────────────────────────────────── */

.stats {
  display: flex;
  align-items: center;
  gap: var(--sp-16);
  max-width: 620px;
  margin: 0 auto;
  padding: var(--sp-16) var(--sp-16) 0;
}
.stat {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-16);
  flex: none;
}
.stat-ico { font-size: var(--fs-16); line-height: 1; }
#p-stat-streak { color: var(--c-flame); }
.stat-num { font-variant-numeric: tabular-nums; }
.stat.bump { animation: bump 0.5s var(--ease-spring); }
@keyframes bump {
  0% { transform: scale(1); }
  40% { transform: scale(1.35) rotate(-4deg); }
  100% { transform: scale(1); }
}

/* Segmented progress — each cell is flex:1, so it fits any width. */
.progress { display: flex; flex: 1; gap: var(--sp-4); min-width: 0; }
.seg {
  flex: 1;
  height: 11px;
  border-radius: var(--r-pill);
  background: var(--bg-soft);
  box-shadow: inset 0 0 0 1px var(--border);
  overflow: hidden;
}
.seg-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  /* Same reasoning as .ring-fill: progress, not correctness. .is-weak below still
     goes gold, which is the part that carries a judgement. */
  background: var(--c-teal);
  transition: width 0.6s var(--ease-out);
}
.seg.is-weak .seg-fill { background: var(--c-gold); }
.seg.is-skip .seg-fill { background: var(--border-strong); }
/* Sent, not scored. Teal because it is the brand's "voice" colour and this is
   the only thing the strip is allowed to say about an assessment answer: it
   went. Deliberately not gold or grass — either would be a grade. */
.seg.is-sent .seg-fill { background: var(--c-teal); }

/* Back control: a bordered chip at tap height with a real icon — never an
   underlined arrow-glyph text link. Every screen that isn't a tab has one. */
.backchip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  font: inherit;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-13);
  color: var(--fg-muted);
  background: var(--card);
  border: 0;
  border-radius: var(--r-pill);
  padding: var(--sp-8) var(--sp-16) var(--sp-8) 0.75rem;
  margin-bottom: var(--sp-16);
  min-height: 38px;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--under-surface);
  transition: transform 0.11s var(--ease-out), box-shadow 0.11s var(--ease-out), color 0.15s ease;
}
/* In the topbar it is session chrome sitting beside the Demo or class tag, not a
   control belonging to the page below it: no bottom margin to push the bar out
   of shape, and sized to the bar's own rhythm. */
.topbar-back {
  margin-bottom: 0;
  flex: none;
  min-height: 32px;
  padding: var(--sp-4) 0.75rem var(--sp-4) var(--sp-8);
  font-size: var(--fs-13);
  background: var(--bg-soft);
  box-shadow: none;
  border: 1.5px solid var(--border);
}
.topbar-back:active { transform: none; box-shadow: none; }
.topbar-back:focus-visible { box-shadow: 0 0 0 var(--ring-w) var(--ring); }
.backchip:hover { color: var(--fg); }
.backchip:active { transform: translateY(2px); box-shadow: 0 0 0 var(--under-surface); }
.backchip:focus-visible { box-shadow: 0 2px 0 var(--under-surface), 0 0 0 var(--ring-w) var(--ring); }
.backchip svg {
  width: 16px; height: 16px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.5;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ── Speech card ───────────────────────────────────────────────────────── */

.mic-zone { display: flex; flex-direction: column; align-items: center; gap: var(--sp-8); }

.mic {
  position: relative;
  width: clamp(86px, 24vw, 108px);
  height: clamp(86px, 24vw, 108px);
  border: 0;
  border-radius: 50%; /* deliberate circle */
  background: var(--c-teal);
  /* The glyph is drawn in currentColor, so this is the contrast of the single
     most important control in the product. White on the brand teal is 2.04:1 in
     dark mode — below the 3:1 a graphical control needs, on the one thing a
     child has to find. Navy is 8.24. */
  color: var(--ink-on-bright);
  cursor: pointer;
  box-shadow: 0 5px 0 var(--under-teal);
  transition: transform 0.12s var(--ease-out), box-shadow 0.12s var(--ease-out),
              background 0.2s ease;
}
.mic:active { transform: translateY(5px); box-shadow: 0 0 0 var(--under-teal); }

/* Waiting on the browser's mic prompt. That await can sit for seconds, and an
   unresponsive-looking button reads as broken. A ring outside the circle so it
   never crowds the glyph — and it moves, because a static hint on an already
   coloured control is invisible. */
.mic.is-waiting { cursor: progress; }
.mic.is-waiting::after {
  content: "";
  position: absolute;
  inset: -9px;
  border-radius: 50%;
  border: 3px solid color-mix(in oklch, var(--c-teal-deep) 22%, transparent);
  border-top-color: var(--c-teal-deep);
  animation: mic-wait 0.85s linear infinite;
}
@keyframes mic-wait { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .mic.is-waiting::after {
    animation: none;
    border-color: color-mix(in oklch, var(--c-teal-deep) 45%, transparent);
  }
}
.mic:focus-visible { box-shadow: 0 5px 0 var(--under-teal), 0 0 0 var(--ring-w) var(--ring); }
.mic svg {
  position: absolute; inset: 0; margin: auto;
  width: 42%; height: 42%;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.mic .stop-glyph { fill: currentColor; stroke: none; opacity: 0; }
.mic.is-live { background: var(--c-berry); box-shadow: 0 5px 0 var(--under-berry); }
.mic.is-live .mic-glyph { opacity: 0; }
.mic.is-live .stop-glyph { opacity: 1; }
.mic:disabled { background: var(--bg-soft); color: var(--fg-muted); box-shadow: 0 5px 0 var(--under-off); cursor: not-allowed; }

/* Pulse anchors to the button itself, so it scales with it at every width. */
.mic-pulse {
  position: absolute;
  /* Not `inset: -6px`. The button carries `box-shadow: 0 5px 0 var(--under-teal)`
     — an underside that belongs to its visual mass but not to its box — so a
     ring centred on the box sits half that underside too high. Same height,
     shifted down 2.5px, which centres it on what the eye actually reads as the
     button. Most obvious with reduced motion on, where the ring holds still and
     the error has nowhere to hide. */
  top: -3.5px;
  bottom: -8.5px;
  left: -6px;
  right: -6px;
  border-radius: 50%;
  border: 3px solid var(--c-berry);
  opacity: 0;
  pointer-events: none;
}
.mic.is-live .mic-pulse { animation: pulse-ring 1.7s ease-out infinite; }
@keyframes pulse-ring {
  0%   { transform: scale(0.94); opacity: 0.75; }
  70%  { transform: scale(1.28); opacity: 0; }
  100% { transform: scale(1.28); opacity: 0; }
}
/* The ring's resting opacity is 0 — it only exists while pulse-ring is running,
   and the keyframe ends at 0 too. So the global 0.001ms clamp fast-forwards it
   to invisible and a reduced-motion recording session gets no halo at all.
   Hold it on the opening frame instead: a still ring, at rest size. */
@media (prefers-reduced-motion: reduce) {
  .mic.is-live .mic-pulse {
    animation: none;
    transform: none;
    opacity: 1;
  }
}

.mic-label {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-14);
  color: var(--fg-muted);
  margin: 0;
}
/* Recording changed the button and left the word underneath it in the same
   muted grey as "Tap to speak". A teacher scanning a room of twenty-five needs
   to see which screens are live from standing height. */
.mic.is-live + .mic-label { color: var(--c-alert); }

.meter-wrap {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  width: 100%;
  background: var(--bg-soft);
  border-radius: var(--r-sm);
  padding: var(--sp-8) var(--sp-16);
}
/* Layout owns this canvas's box, never the other way round. drawMeter() writes
   clientWidth * dpr into the width/height attributes every frame; without a
   CSS box those attributes ARE the layout size, so each frame multiplies the
   last by dpr and the canvas runs away to Chrome's 2^25-1 clamp. flex sets the
   width, this rule sets the height, and the feedback path is gone. */
#p-meter { flex: 1; min-width: 0; height: 34px; display: block; }
.meter-status {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  white-space: nowrap;
  margin: 0;
}
.meter-status.live { color: var(--c-alert); font-weight: 600; }

/* Before you start. Deliberately plain: this screen's job is to lower the
   temperature, and a card competing for attention would do the opposite. */
.ready-card { max-width: 32rem; }
.ready-line {
  margin: 0 0 var(--sp-8);
  font-size: var(--fs-16);
  line-height: 1.55;
  color: var(--fg);
}
.ready-quiet { color: var(--fg-muted); font-size: var(--fs-14); }
.ready-card .btn-go { margin-top: var(--sp-8); }

/* The instruction keeps the left, the audio controls take the right, and the
   question below stays a clean line down to the mic. */
.ask-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-8);
  flex-wrap: nowrap;
}
/* The instruction yields first: it can run to two lines without harm, whereas a
   wrapped control group leaves a stranded row above the question. */
.ask-row .p-instruction { margin: 0; flex: 1 1 11rem; min-width: 0; }
.ask-audio { display: flex; align-items: center; gap: var(--sp-8); flex-shrink: 0; }
/* Shorter than the replay bar's, which owns a whole row to itself. Narrow
   screens shrink the track rather than dropping the group to its own line: a
   lone strip of controls between the instruction and the question was the
   awkward region — it reads as a thing in its own right instead of as the
   question's controls. The instruction wraps to two lines instead, which costs
   nothing. */
.ask-audio .replay-vol { padding: 0 0.75rem; min-height: 40px; gap: var(--sp-8); }
.ask-audio .replay-vol input[type="range"] { width: clamp(56px, 15vw, 104px); }

/* The verdict and its speaker share a line, so reading it aloud never pushes the
   rest of the result down the card. */
.verdict-line { display: flex; align-items: center; gap: var(--sp-8); }
.verdict-line .verdict-head { margin: 0; }

/* Read aloud. Icon only: it sits beside text it must not compete with, and a
   labelled button in either slot was the thing that read as clutter. The
   speaking state is the only moment it raises its voice. */
.say-btn {
  display: inline-grid;
  place-items: center;
  width: 40px;                  /* a thumb on an iPad, not a mouse on a desktop */
  height: 40px;
  flex-shrink: 0;
  background: var(--bg-soft);
  border: 1.5px solid var(--border);
  border-radius: var(--r-btn);
  color: var(--fg-muted);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.say-btn svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.say-btn:hover { color: var(--fg); border-color: var(--border-strong); }
.say-btn.is-speaking {
  color: var(--c-accent-ink);
  border-color: var(--c-teal);
  background: color-mix(in oklch, var(--c-teal) 12%, transparent);
}
/* The waves pulse only while it is actually speaking, so "is it still going?"
   never needs asking. Static under reduced motion — the colour already says it. */
.say-btn.is-speaking svg { animation: say-pulse 1.1s ease-in-out infinite; }
@keyframes say-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}
@media (prefers-reduced-motion: reduce) {
  .say-btn.is-speaking svg { animation: none; }
}

/* Words as they land. Settled ones sit at full strength; the tail the recogniser
   is still revising stays dimmed, so a word changing under the reader looks like
   the machine thinking rather than the reader being corrected. Centred and
   capped so a long answer grows upward in a steady column instead of shoving the
   mic around. */
.live-heard {
  margin: var(--sp-8) auto 0;
  max-width: 34rem;
  min-height: 1.4em;
  text-align: center;
  font-size: var(--fs-16);
  line-height: 1.45;
  color: var(--fg);
  overflow-wrap: break-word;
}
.live-heard .interim { color: var(--fg-muted); }

.notice {
  font-size: var(--fs-13);
  color: var(--fg-muted);
  background: var(--bg-soft);
  border-radius: var(--r-sm);
  padding: var(--sp-8) var(--sp-16);
  margin: var(--sp-16) 0 0;
}
.notice.is-error { color: var(--c-alert); background: color-mix(in oklch, var(--c-berry) 8%, transparent); }

/* ── Grading busy state ────────────────────────────────────────────────── */
/* A small crisp bar that MOVES along the top edge, in currentColor so it
   reads on any surface. Static fallback under reduced motion. */

.grading { position: relative; overflow: hidden; padding: var(--sp-48) 0 var(--sp-24); text-align: center; color: var(--fg-muted); }
.grading-bar {
  position: absolute;
  top: 0; left: 0;
  height: 3px;
  width: 28%;
  border-radius: var(--r-pill);
  /* Every other progress indicator in the product names its colour; this one
     inherited --fg-muted from its parent and was the only one that could drift
     if that text colour ever changed for an unrelated reason. */
  background: var(--c-teal);
  animation: travel 1.2s cubic-bezier(0.55, 0, 0.45, 1) infinite;
}
@keyframes travel {
  from { transform: translateX(-100%); }
  to   { transform: translateX(357%); }
}
.grading-text { font-family: var(--font-head); font-weight: 600; font-size: var(--fs-14); margin: 0; }
.dots::after { content: ""; animation: dots 1.2s steps(4) infinite; }
@keyframes dots {
  0%   { content: ""; }
  25%  { content: "."; }
  50%  { content: ".."; }
  75%  { content: "..."; }
}

/* ── Feedback ──────────────────────────────────────────────────────────── */

.verdict-row { display: flex; align-items: center; gap: var(--sp-16); margin-bottom: var(--sp-16); }

.score-ring { position: relative; width: 104px; height: 104px; flex: none; }
.score-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-track, .ring-fill {
  fill: none;
  stroke-width: 11;
  stroke-linecap: round;
}
.ring-track { stroke: var(--track); }
/* Teal, with gold and berry still below it. This is a SCORE dial, not a verdict:
   it counts from 0 to 100 and has three bands, where grass-against-berry means
   right against wrong. Keeping the top band green made the brand accent absent
   from the one number a student looks at hardest, and made the hero's picture of
   this screen disagree with the screen. Gold and berry are untouched — the
   warning half of the scale is where the semantics actually live. */
.ring-fill {
  stroke: var(--c-teal);
  stroke-dasharray: 327;
  stroke-dashoffset: 327;
  transition: stroke-dashoffset 0.95s var(--ease-out), stroke 0.3s ease;
}
.score-ring.is-mid .ring-fill { stroke: var(--c-gold); }
.score-ring.is-low .ring-fill { stroke: var(--c-berry); }
.score-num {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-32);
  font-variant-numeric: tabular-nums;
}

.verdict-copy { min-width: 0; }
.verdict-head { font-family: var(--font-head); font-weight: 700; font-size: var(--fs-20); margin: 0; }
.verdict-sub { color: var(--fg-muted); font-size: var(--fs-14); margin: 0; }
/* Column count is driven by the number of meters, not by available width.
   auto-fit produced rows of three, which always leaves a dangling cell. */
.bars { display: grid; grid-template-columns: 1fr; gap: var(--sp-16); margin-bottom: var(--sp-24); }
.bars[data-count="2"], .bars[data-count="4"] { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 560px) {
  .bars[data-count="4"] { grid-template-columns: repeat(4, 1fr); }
}
.bar-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-8);              /* space-between alone let long names touch the number */
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-12);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin-bottom: var(--sp-4);
}
/* The name yields, the number never does — a clipped score is useless. */
.bar-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-label > span:last-child { flex: none; font-variant-numeric: tabular-nums; color: var(--fg); }
.bar-track {
  height: 9px;
  background: var(--track);
  border-radius: var(--r-pill);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--fg) 7%, transparent);
}
.bar-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: var(--c-teal);
  transition: width 0.75s var(--ease-out);
}

.list-head { font-family: var(--font-head); font-weight: 650; font-size: var(--fs-13); letter-spacing: 0.04em; margin: 0 0 var(--sp-8); }

/* Engine name on the set picker — diagnostic, so monospace earns its place.
   (The per-result badge moved into the collapsed .p-tech block.) */
.engine-line {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.6;
  color: var(--fg-muted);
  margin: var(--sp-24) 0 0;
}

/* ── Summary ───────────────────────────────────────────────────────────── */

.sum-tiles { display: flex; gap: var(--sp-8); margin: var(--sp-24) 0; }
.tile {
  flex: 1;
  min-width: 0;
  background: var(--bg-soft);
  border-radius: var(--r-sm);
  /* 0.5rem inline sat inside a 12px arc, so the number started against the
     curve. The house rule is padding >= radius + 6px on the axis that shows. */
  padding: var(--sp-16);
}
.tile-num {
  display: block;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-24);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.tile-cap { font-size: var(--fs-12); letter-spacing: 0.05em; text-transform: uppercase; color: var(--fg-muted); }

.sum-list { list-style: none; margin: 0 0 var(--sp-16); padding: 0; text-align: left; }
.sum-row {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-8) 0;
  border-top: 1px solid var(--border);
  font-size: var(--fs-14);
}
.sum-row .sum-q { flex: 1; min-width: 0; }
.sum-row .sum-score { font-family: var(--font-head); font-weight: 700; font-variant-numeric: tabular-nums; }
.sum-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--c-grass); }
.sum-dot.is-mid { background: var(--c-gold); }
.sum-dot.is-low { background: var(--c-berry); }
.sum-dot.is-skip { background: var(--border-strong); }
/* Sent, not scored. Hollow rather than a colour, because every filled colour on
   this row already means "how it went" and this one deliberately doesn't say. */
.sum-dot.is-sent {
  background: transparent;
  box-shadow: inset 0 0 0 2px var(--c-teal);
}

.teacher-note { font-size: var(--fs-13); color: var(--fg-muted); margin: 0 0 var(--sp-24); }
.sum-actions { display: grid; gap: var(--sp-8); }

/* ── Teacher view ──────────────────────────────────────────────────────── */

.t-head h1 { font-size: var(--fs-24); }

.t-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-16); margin-bottom: var(--sp-24); }
@media (max-width: 520px) { .t-row { grid-template-columns: 1fr; } }
.t-field { display: block; }
/* Micro-labels on the teacher surface are mono, uppercase and tracked. They are
   the cheapest way to make this side read as an authoring tool rather than as
   the student app with tighter corners — and they name a region without
   competing with the content inside it. */
/* One formula, five callers. There were four: .mono-label at 0.66/0.09 mono,
   .rv-label at 0.62/0.08, .rv-state at 0.62/0.06, and the assign table's headers
   at 0.68/0.06 in NUNITO — four near-identical micro-labels that read as an
   accident rather than a decision, and the reason Classes looks like a settings
   page that inherited the radii but not the vocabulary.

   Type only. Layout stays with the callers, because .rv-label sits in a dense
   review card and wants a tighter margin than a form-field caption does —
   sharing the type is the point, sharing the spacing would be overreach. */
.mono-label,
.t-label,
.kp-head,
.rv-label,
.cls-assign-table thead th {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--fs-12);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.mono-label,
.t-label,
.kp-head {
  display: block;
  margin: 0 0 var(--sp-4);
}

/* Keep the native select — it opens the platform picker on a phone — and
   restyle it. The chevron is a real element so it inherits currentColor.
   Built in JS by wrapSelect(), so every dropdown in the app lands here. */
.select-wrap { position: relative; display: block; }
.select-wrap select {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  font: inherit;
  font-family: var(--font-body);
  color: var(--fg);
  background: var(--card);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  /* Left padding clears the corner arc — text starting inside it reads as
     pressed against the edge however much room is really there. Right padding
     clears the chevron, which is the thing nearest that corner. */
  padding: var(--sp-8) 2.25rem var(--sp-8) var(--sp-16);
  min-height: 44px;
  cursor: pointer;
}
.select-wrap select:hover { border-color: var(--fg-muted); }
/* One focus cue, not two. The browser's own outline stacking on top of the ring
   reads as a doubled, clashing border. */
.select-wrap select:focus,
.select-wrap select:focus-visible {
  outline: none;
  border-color: var(--c-teal);
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}
.select-wrap select:disabled { opacity: 0.45; cursor: default; }
.select-chev {
  position: absolute;
  right: 0.7rem; top: 50%;
  transform: translateY(-50%);
  width: 16px; height: 16px;
  pointer-events: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  color: var(--fg-muted);
}

/* Compact variant: sits inline beside other controls rather than owning a row.
   Same skin, smaller — not a second dropdown design. */
.select-wrap.is-compact { display: inline-block; width: auto; }
.select-wrap.is-compact select {
  min-height: 34px;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-13);
  color: var(--fg-muted);
  border-color: var(--border);
  padding: var(--sp-4) 1.75rem var(--sp-4) 0.75rem;
}
.select-wrap.is-compact .select-chev { right: 0.55rem; width: 14px; height: 14px; }

.t-row-tight { grid-template-columns: 1fr 90px; margin-bottom: 0; }
.t-field-narrow { min-width: 0; }

.t-card {
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--card);
  padding: var(--card-pad);
  display: grid;
  gap: var(--sp-16);
}

.t-input, .t-textarea {
  width: 100%;
  font: inherit;
  font-family: var(--font-body);
  font-size: var(--fs-16);
  line-height: 1.5;
  color: var(--fg);
  background: var(--bg-soft);
  /* A visible edge at rest: a field defined only by fill is easy to miss on a
     washed-out school laptop screen. */
  border: 2px solid var(--border);
  border-radius: var(--r-sm);
  padding: var(--sp-8) 0.75rem;
  /* Height comes from here, not from the text, so a bigger glyph can't grow the
     box — that's what made the icon field taller than the name beside it. */
  min-height: 44px;
  transition: border-color 0.16s ease, background 0.16s ease;
}
/* Fixed, not min: emoji carry tall font metrics that inflate an input's
   internal line box regardless of line-height, which is what made the icon
   field 48px next to a 44px name field. */
.t-input { height: 44px; }
.t-textarea { resize: vertical; min-height: 8rem; }
.t-input:hover, .t-textarea:hover { border-color: var(--border-strong); }
.t-input:focus-visible, .t-textarea:focus-visible {
  background: var(--bg);
  border-color: var(--c-teal);
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}
.t-count {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--fg-muted);
  margin: 0;
}
.t-count.is-empty { color: var(--c-alert); }

/* No outside margin. This dates from when the footer flowed under the library;
   it is now a status bar pinned by the shell grid, and a margin above it can
   only expose the page colour in a full-width band between two lighter
   surfaces. Invisible in light mode — --bg and --bg-soft are 0.02 apart — and a
   black stripe in dark mode, where they are 0.14 apart. Breathing room belongs
   in the padding below, which sits inside the bar's own background. */
.t-foot {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--fg-muted);
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  flex-wrap: wrap;
}
/* A quiet confirmation that typing is being kept — there is no Save button, so
   something has to say so. */
/* .t-foot is mono and holds .t-pill and .save-flag, both of which were Nunito —
   two fonts in one status bar, sometimes in adjacent words. The bar is a
   diagnostic readout, which is what mono is for on this surface. */
.save-flag {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-12);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--c-grass);
  background: color-mix(in oklch, var(--c-grass) 15%, transparent);
  border-radius: var(--r-pill);
  padding: 0 var(--sp-8);
  opacity: 0;
  transition: opacity 0.25s var(--ease-out);
}
.save-flag.is-on { opacity: 1; }
/* Three states, because "saved" and "saving" and "didn't save" are three
   different things to a teacher deciding whether it is safe to close the tab.
   Amber while in flight, red when it failed — and the red one never fades. */
.save-flag.is-pending {
  color: var(--c-gold-deep);
  background: color-mix(in oklch, var(--c-gold) 18%, transparent);
}
.save-flag.is-error {
  color: var(--c-alert);
  background: color-mix(in oklch, var(--c-berry) 15%, transparent);
}
@media (prefers-reduced-motion: reduce) { .save-flag { transition: none; } }

/* New / Duplicate sit under the set picker, not in the destructive row. */
.t-set-actions { display: flex; gap: var(--sp-8); margin-top: var(--sp-8); flex-wrap: wrap; }
/* A modifier that never fired. .btn-quiet sets the same three properties, has
   equal specificity, and sits later in the file — so every "mini" control in
   the product has been rendering at full size. That is most of the density
   problem on the Classes surface: four of these in a review card's action row,
   plus the results rows.

   Specificity raised to match how it is always used (both classes together)
   rather than reordering the file, which would make the fix depend on an
   ordering nobody can see from the call site. */
.btn-quiet.btn-mini,
.btn-solid-quiet.btn-mini,
.btn-icon.btn-mini,
.btn-mini { font-size: var(--fs-13); padding: var(--sp-4) 0.75rem; min-height: 36px; }
.btn-quiet.btn-mini svg,
.btn-solid-quiet.btn-mini svg,
.btn-mini svg { width: 13px; height: 13px; }
/* The one exception, named rather than left to be rediscovered: the demo bar's
   sign-in is the product's only conversion control and stays at touch size. */
#demo-bar .btn-quiet.btn-mini { min-height: 44px; }

/* Compact buttons in the same family as the student ones — rounded and
   pressable, just smaller and with a shorter lift. Pragmatic, not toy-like. */
.btn-quiet, .btn-solid-quiet, .btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-8);
  /* These classes now sit on an anchor as well as on buttons — a control that
     navigates should stay a link — so the underline a browser adds by default
     has to come off here rather than in a one-off rule beside it. */
  text-decoration: none;
  font: inherit;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-14);
  cursor: pointer;
  border: 0;
  border-radius: var(--r-btn);
  padding: var(--sp-8) var(--sp-16);
  min-height: 44px;
  background: var(--bg-soft);
  color: var(--fg);
  box-shadow: 0 2px 0 var(--under-surface);
  transition: transform 0.11s var(--ease-out), box-shadow 0.11s var(--ease-out),
              filter 0.15s ease;
}
.btn-quiet:hover, .btn-icon:hover, .btn-solid-quiet:hover { filter: brightness(1.04); }
.btn-quiet:active, .btn-icon:active, .btn-solid-quiet:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 var(--under-surface);
}
.btn-quiet:focus-visible, .btn-icon:focus-visible {
  box-shadow: 0 2px 0 var(--under-surface), 0 0 0 var(--ring-w) var(--ring);
}
/* Teal, with .btn-go: this is the teacher surface's version of the same "do the
   thing" button, and the two sat one screen apart in two different greens'
   worth of the same idea. */
.btn-solid-quiet {
  background: var(--c-teal);
  /* White on this fill measured 2.32:1 on the green it replaced and 2.56:1 on
     the teal — unreadable at button size either way. The accents stay light in
     both themes, so dark ink is correct in both. */
  color: var(--ink-on-bright);
  box-shadow: 0 3px 0 var(--under-teal);
}
.btn-solid-quiet:active { box-shadow: 0 0 0 var(--under-teal); transform: translateY(3px); }
.btn-solid-quiet:focus-visible { box-shadow: 0 3px 0 var(--under-teal), 0 0 0 var(--ring-w) var(--ring); }
.btn-quiet svg, .btn-solid-quiet svg, .btn-icon svg {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.btn-icon { padding: var(--sp-8); min-width: 38px; }
/* Was scoped to .btn-icon only, so the reset confirmation — a .btn-quiet —
   never picked up the destructive styling it was asking for. */
/* The underside is --under-wash, not --c-berry-deep. This face is a 15% tint
   rather than a fill, so in dark it composites down to Y 0.024 on a card —
   darker than berry-deep, darker than any shade of berry there is — and the
   armed button was the one control whose bottom edge glowed red. */
.btn-quiet.is-armed, .btn-icon.is-armed, .btn-solid-quiet.is-armed {
  background: color-mix(in oklch, var(--c-berry) 15%, transparent);
  color: var(--c-alert);
  box-shadow: 0 2px 0 var(--under-wash);
}
.btn-quiet.is-armed:active, .btn-icon.is-armed:active {
  box-shadow: 0 0 0 var(--under-wash);
}
.btn-quiet:disabled, .btn-solid-quiet:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}

.t-actions { display: flex; gap: var(--sp-8); flex-wrap: wrap; margin-top: var(--sp-24); }
.t-actions .btn-solid-quiet { margin-left: auto; }

/* ── Practice mode ─────────────────────────────────────────────────────── */

#p-stat-reps { color: var(--c-grape); }

.set-grid { display: grid; gap: var(--sp-8); text-align: left; }
.set-card {
  display: flex;
  align-items: center;
  gap: var(--sp-16);
  width: 100%;
  font: inherit;
  font-family: var(--font-head);
  text-align: left;
  cursor: pointer;
  background: var(--bg-soft);
  color: var(--fg);
  border: 2px solid transparent;
  border-radius: var(--r-sm);
  padding: var(--sp-16);
  transition: border-color 0.16s ease, transform 0.12s var(--ease-out);
}
.set-card:hover { border-color: var(--c-teal); }
.set-card:active { transform: translateY(2px); }
.set-card:focus-visible { border-color: var(--c-teal); box-shadow: 0 0 0 var(--ring-w) var(--ring); }
.set-emoji { display: grid; place-items: center; flex: none; color: var(--c-accent-ink); }
.set-emoji svg { width: 24px; height: 24px; }
.set-copy { min-width: 0; }
.set-name { display: block; font-weight: 650; font-size: var(--fs-16); }
.set-meta { display: block; font-weight: 600; font-size: var(--fs-13); color: var(--fg-muted); }

.p-instruction {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-14);
  color: var(--fg-muted);
  margin: 0 0 var(--sp-8);
}

/* Target phrase as word chips — each one can be marked independently. */
.target {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8) var(--sp-4);
  margin-bottom: var(--sp-24);
}
.word {
  /* Positioned unconditionally: the playback sweep is an absolute ::after, and
     without this it escapes to the card and draws across the bottom of it. */
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  font-family: var(--font-head);
  font-weight: 650;
  font-size: clamp(1.1rem, 3.9vw, 1.4rem);
  line-height: 1.25;
  border-radius: var(--r-sm);
  padding: 0 var(--sp-8);
  background: transparent;
  transition: background 0.3s ease, color 0.3s ease;
}
/* Colour AND an underline, because these three chips are the only place the
   product tells a child which specific word slipped, and roughly one boy in
   twelve cannot separate the grass and berry tints. The underline thickness
   carries the same three-step scale the colour does. */
.word.is-hit  {
  background: color-mix(in oklab, var(--c-grass) 20%, transparent);
  box-shadow: inset 0 -2px 0 var(--c-grass-deep);
}
.word.is-near {
  background: color-mix(in oklab, var(--c-gold) 26%, transparent);
  box-shadow: inset 0 -3px 0 var(--c-gold-deep);
}
.word.is-miss {
  background: color-mix(in oklab, var(--c-berry) 16%, transparent);
  box-shadow: inset 0 -4px 0 var(--c-alert);
}
.word .heard {
  font-size: var(--fs-13);
  font-weight: 600;
  color: var(--fg-muted);
  font-style: italic;
}

/* ── Tap a word to hear yourself say it ─────────────────────────────────────
   A number tells you a word slipped; hearing your own attempt tells you what
   to change. Opt-in via the replay toggle — otherwise a tap while reading
   fires audio nobody asked for. */

/* The underline goes on the word itself, never on .word — the chip is a column
   flex container, so decorating it drew the dots under the score line too. */
.target.is-listen .word.can-play { cursor: pointer; }
.target.is-listen .word.can-play .w-main {
  text-decoration: underline dotted color-mix(in oklch, currentColor 45%, transparent);
  text-underline-offset: 0.2em;
  text-decoration-thickness: 2px;
}
.target.is-listen .word.can-play:hover .w-main { text-decoration-color: currentColor; }
.target.is-listen .word.can-play:hover { background: color-mix(in oklch, var(--fg) 8%, transparent); }
/* The app has one focus cue and this was the only element not using it — a 2px
   grey ring where everything else takes the 4px --ring. A keyboard user moving
   between a word chip and any other control saw the indicator change shape,
   which reads as two different kinds of focus. */
.target.is-listen .word.can-play:focus-visible {
  outline: none;
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}

/* Playing: a bar sweeps the word's width. Motion reads as "sounding now" in a
   way a colour change alone doesn't. */
.word.is-playing { background: color-mix(in oklch, var(--fg) 10%, transparent); }
.word.is-playing::after {
  content: "";
  position: absolute;
  left: var(--sp-8);
  right: var(--sp-8);
  bottom: 0;
  height: 2px;
  background: currentColor;
  transform-origin: left center;
  animation: word-sweep 0.45s linear;
}
@keyframes word-sweep {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  .word.is-playing::after { animation: none; transform: scaleX(1); }
}

/* ── Replay bar ─────────────────────────────────────────────────────────── */

.replay-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  flex-wrap: wrap;
  margin: 0 0 var(--sp-16);
}

/* Three deliberate stages, because the three controls stop fitting on one line
   long before anything resembling a phone. Measured: they need ~519px of bar,
   and the bar is the viewport less ~280px of page and card padding — so one row
   only works above ~800px. Everything between there and the old 520px
   breakpoint fell into a ragged flex wrap, with the volume stranded at half
   width under two buttons. */

/* Stage 2 — the buttons share a row, the volume takes the whole width beneath.
   This is the arrangement that reads as one group rather than three things. */
@media (max-width: 819px) {
  .replay-bar {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: stretch;   /* equal heights even if one label wraps */
    gap: var(--sp-8);
  }
  .replay-toggle, .replay-all { justify-content: center; }
  .replay-vol { grid-column: 1 / -1; }
  .replay-vol input[type="range"] { width: auto; flex: 1; }
}

/* Tighter labels once the two buttons start to crowd each other. */
@media (max-width: 520px) {
  .replay-toggle, .replay-all { font-size: var(--fs-13); padding: var(--sp-8); }
}

/* Stage 3 — a single column. Unlikely, but a 320px phone in a school drawer is
   not impossible, and two buttons at that width is two truncated labels. */
@media (max-width: 360px) {
  .replay-bar { grid-template-columns: 1fr; }
}
/* The replay toggle and play-all are the same control drawn twice, so they
   share one rule; only their icons differ. Play-all sits beside the toggle:
   hearing the phrase whole is what shows you pacing, which word-by-word can't. */
.replay-toggle,
.replay-all {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-13);
  color: var(--fg-muted);
  background: transparent;
  border: 2px solid var(--border);
  border-radius: var(--r-btn);
  padding: var(--sp-8) var(--sp-16);
  min-height: 44px;   /* thumb-sized: iPads and small laptops are the target */
  cursor: pointer;
  transition: color 0.18s var(--ease-out), border-color 0.18s var(--ease-out),
              background 0.18s var(--ease-out);
}
.replay-toggle svg {
  width: 16px; height: 16px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.replay-all svg { width: 13px; height: 13px; fill: currentColor; stroke: none; }
.replay-toggle:hover,
.replay-all:hover { color: var(--fg); border-color: var(--border-strong); }
/* Teal, and this one was actively misleading in green. The toggle sits directly
   under the hit / near / miss word chips, where green is the product's word for
   "you got this right" — so a pressed playback control was wearing the same
   colour as a correct answer, in the one place a child is reading colour for
   meaning. A control that is switched on is a selection, and selection is what
   the brand accent has always carried here. */
.replay-toggle.is-on,
.replay-all.is-on {
  color: var(--ink-on-bright);
  background: var(--c-teal);
  border-color: var(--c-teal);
}

/* Volume. Sized for a finger on an iPad, not a mouse on a desktop: the input is
   a 44px-tall hit area with an 8px track drawn through the middle, and the
   thumb is 26px. The filled portion is a gradient driven by --pct from JS,
   because no cross-browser pseudo-element gives you a fill. */
/* Same pill as the two buttons beside it, so the row reads as one control
   group. Vertical padding stays at zero: the input is already 44px, and the
   pill takes its height from that rather than adding to it. */
.replay-vol {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  color: var(--fg-muted);
  background: transparent;
  border: 2px solid var(--border);
  border-radius: var(--r-btn);
  padding: 0 var(--sp-16);
  min-height: 44px;
  --pct: 70%;
  transition: border-color 0.18s var(--ease-out), color 0.18s var(--ease-out);
}
.replay-vol:hover { color: var(--fg); border-color: var(--border-strong); }
.replay-vol:focus-within { color: var(--fg); border-color: var(--c-teal); }
.replay-vol svg {
  width: 19px; height: 19px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Opacities come from JS as a continuous function of the level, so the icon
   reads as a meter rather than as three fixed states. */
.replay-vol .vol-wave-1 { opacity: var(--w1, 1); }
.replay-vol .vol-wave-2 { opacity: var(--w2, 1); }
.replay-vol .vol-mute   { opacity: var(--wmute, 0); }
.replay-vol .vol-wave, .replay-vol .vol-mute { transition: opacity 0.12s linear; }
@media (prefers-reduced-motion: reduce) {
  .replay-vol .vol-wave, .replay-vol .vol-mute { transition: none; }
}
.replay-vol input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: clamp(110px, 26vw, 150px);
  /* 40 + the pill's 2px borders = 44, matching the buttons beside it. Still a
     generous drag target, and the pill around it catches near-misses. */
  height: 40px;
  background: transparent;
  margin: 0;
  outline: none;
  cursor: pointer;
  touch-action: none;         /* dragging shouldn't scroll the page */
}

.replay-vol input[type="range"]::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: var(--r-pill);
  background: linear-gradient(to right, var(--c-teal) var(--pct), var(--track) var(--pct));
  box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--fg) 8%, transparent);
}
.replay-vol input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 26px; height: 26px;
  margin-top: -9px;           /* centre the 26px thumb on the 8px track */
  border-radius: 50%;
  background: var(--c-teal);
  border: 3px solid var(--card);
  box-shadow: var(--shadow-chip);
  transition: transform 0.12s var(--ease-out);
}
.replay-vol input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.12); }

.replay-vol input[type="range"]::-moz-range-track {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--track);
  box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--fg) 8%, transparent);
}
.replay-vol input[type="range"]::-moz-range-progress {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--c-teal);
}
.replay-vol input[type="range"]::-moz-range-thumb {
  width: 26px; height: 26px;
  border-radius: 50%;
  background: var(--c-teal);
  border: 3px solid var(--card);
  box-shadow: var(--shadow-chip);
}
.replay-vol input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}
.replay-vol input[type="range"]:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}

.p-readout { display: flex; gap: var(--sp-8); margin-bottom: var(--sp-16); }
.readout {
  flex: 1;
  min-width: 0;
  background: var(--bg-soft);
  border-radius: var(--r-sm);
  padding: var(--sp-8);
  text-align: center;
}
.readout-num {
  display: block;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-20);
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}
.readout-cap { font-size: var(--fs-13); color: var(--fg-muted); font-weight: 600; }

.p-heard-label {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-13);
  color: var(--fg-muted);
  margin: 0 0 var(--sp-4);
}
.p-heard {
  font-size: var(--fs-16);
  background: var(--bg-soft);
  border-radius: var(--r-sm);
  padding: var(--sp-8) var(--sp-16);
  margin: 0 0 var(--sp-16);
}
.p-heard.is-empty { color: var(--fg-muted); font-style: italic; }

.p-notes { list-style: none; margin: 0 0 var(--sp-16); padding: 0; font-size: var(--fs-14); }
.p-notes li {
  position: relative;
  padding-left: var(--sp-24);
  margin-bottom: var(--sp-8);
}
.p-notes li::before { content: "→"; position: absolute; left: 0; color: var(--c-teal); font-weight: 700; }

/* The result block carried no top spacing of its own. With a score ring the
   ring's layout hid that; with nothing recognised the ring is hidden and the
   readout butted straight into "Tap to speak". The rule belongs here rather
   than on whichever child happens to be first. */
.p-result-block {
  margin-top: var(--sp-24);
  padding-top: var(--sp-24);
  border-top: 1px solid var(--border);
}

/* Assessment receipt. Quiet on purpose — a child who has just finished speaking
   should read "that went in" and nothing that invites them to guess how it did.
   No ring, no colour that could be mistaken for a verdict. */
/* The receipt had type and no container, so "Answer sent" floated on the card
   with nothing marking it as the outcome — the one screen in an assessment where
   a child is told the thing they just did counted. Same teal the marked result
   uses for its own outcome block, at a lower strength: this is an
   acknowledgement, not a score. */
.p-submitted {
  padding: var(--sp-16);
  border-radius: var(--r-sm);
  background: color-mix(in oklab, var(--c-teal) 12%, var(--card));
  border: 1px solid color-mix(in oklch, var(--c-teal) 35%, transparent);
}
.p-submitted .submitted-head {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-18);
  margin: 0 0 var(--sp-4);
}
.p-submitted .submitted-sub {
  color: var(--fg-muted);
  font-size: var(--fs-14);
  margin: 0 0 var(--sp-16);
}

/* ── Card heading ───────────────────────────────────────────────────────
   Above the card rather than inside it: freed from the card's padding it can
   take real size, and the card starts with the thing you're meant to do. */
.card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-8);
  margin: 0 0 var(--sp-8);
}
.card-title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.3rem, 5vw, 1.85rem);
  letter-spacing: var(--head-track);
  line-height: 1.15;
  margin: 0;
  min-width: 0;
}
.card-count {
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-13);
  color: var(--fg-muted);
  white-space: nowrap;
  margin: 0;
}

/* ── Feedback block ─────────────────────────────────────────────────────
   The notes were floating loose under the transcript. Giving them a titled,
   inset block makes it obvious this is the part you act on. */
.p-feedback {
  background: var(--bg-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: var(--sp-16);
  margin-bottom: var(--sp-16);
  text-align: left;
}
.p-feedback-head {
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-12);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0 0 var(--sp-8);
}
.p-feedback .p-notes { margin: 0; }

/* Engine name and timing: true, occasionally useful, and not what a student
   is here for. Collapsed by default. */
.p-tech {
  border-top: 1px solid var(--border);
  padding-top: var(--sp-8);
  text-align: left;
}
.p-tech summary {
  font-size: var(--fs-13);
  color: var(--fg-muted);
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-4) 0;
  transition: color 0.18s var(--ease-out);
}
.p-tech summary::-webkit-details-marker { display: none; }
/* A symmetric SVG rather than a CSS border-triangle: the triangle's visual
   centroid sits a third of the way across its box, so rotating it about the box
   centre swings it instead of spinning it. */
.p-tech summary svg {
  width: 14px; height: 14px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.5;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform 0.3s var(--ease-spring);
}
.p-tech[open] summary svg { transform: rotate(90deg); }
.p-tech summary:hover { color: var(--fg); }

/* Deliberately CSS-only. A JS height animation was tried and rejected: it makes
   the open/close state depend on an animation callback firing, so an
   interrupted click or a backgrounded tab leaves the element wedged open with
   an inline height. This can't get stuck — browsers without ::details-content
   simply open instantly, which is the behaviour it had before.
   `interpolate-size` goes on the ancestor, matching the landing-page FAQ. */
.p-result-block { interpolate-size: allow-keywords; }

.p-tech::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size 0.32s var(--ease-out),
              content-visibility 0.32s allow-discrete;
}
.p-tech[open]::details-content { block-size: auto; }

@media (prefers-reduced-motion: reduce) {
  .p-tech summary svg,
  .p-tech::details-content { transition: none; }
}
.p-tech-line {
  font-size: var(--fs-13);
  line-height: 1.5;
  color: var(--fg-muted);
  margin: var(--sp-8) 0 0;
}

.p-work { margin-bottom: var(--sp-16); text-align: left; }
.chip-row { display: flex; flex-wrap: wrap; gap: var(--sp-8); }
.work-chip {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-14);
  /* Gold means "needs attention" everywhere else in this product — on a late
     assignment, on a redo request, on a status pill. A question being worth
     three marks is neutral information, so it takes the neutral chip. */
  background: var(--bg-soft);
  border: 1px solid var(--border);
  color: var(--fg-muted);
  border-radius: var(--r-pill);
  padding: var(--sp-4) 0.75rem;
}

/* ── Confetti overlay ──────────────────────────────────────────────────── */

#confetti {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 50;
  display: none;
}
#confetti.is-active { display: block; }

/* ── Reduced motion ────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* Keep the busy state legible without movement: a static leading bar. */
  .grading-bar { animation: none; transform: none; }
  .dots::after { animation: none; content: "…"; }
}

/* ── Teacher library: folders and questions ─────────────────────────────────
   Deliberately file-manager shaped. A teacher already knows how folders behave,
   which is most of the training budget saved. */

/* ── The teacher shell: fixed frame, one scrolling region ───────────────────
   Chrome (top bar, folder list, status bar) is light; the work surface behind
   the question cards is the darker page colour. That split is what tells you at
   a glance which parts of the screen stay put. */

body[data-mode="teacher"] {
  height: 100vh;
  height: 100dvh;   /* the mobile address bar must not push the status bar off */
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
body[data-mode="teacher"] .topbar { flex: none; }
body[data-mode="teacher"] #view-teacher { flex: 1 1 auto; min-height: 0; }

.t-app {
  height: 100%;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto auto;
}

.t-split {
  display: grid;
  grid-template-columns: 264px minmax(0, 1fr);
  min-height: 0;   /* without this a grid row refuses to shrink below its content */
}

.t-side {
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: var(--sp-16);
  background: var(--bg-soft);
  border-right: 1px solid var(--border);
}
.t-side-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-8); margin-bottom: var(--sp-8); flex: none; }

/* Takes the slack, so "How marking works" sits at the bottom of the column
   rather than trailing whatever the folder list happens to be tall. */
.t-folders {
  display: flex; flex-direction: column; gap: var(--sp-4);
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  margin: 0 calc(var(--sp-16) * -1); padding: 0 var(--sp-16);   /* the scrollbar rides the edge */
}

.t-main {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  min-height: 0;
  min-width: 0;
}
.t-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;   /* stops the page bouncing when the list ends */
  min-height: 0;
  /* And min-width, for the same reason min-height is here. This is a grid item,
     and a grid item defaults to min-width:auto — it refuses to shrink below its
     content's min-content width. The Classes grid inside it is
     repeat(auto-fit, minmax(21rem, 1fr)), so min-content was three 21rem
     columns: the scroller pinned itself to ~1072px, auto-fit never collapsed,
     and the whole teacher surface scrolled sideways on anything narrower than a
     laptop, tablets included. Letting it shrink is what lets auto-fit do the job
     it was already written to do. */
  min-width: 0;
  padding: var(--sp-16) var(--sp-16) var(--sp-24);
}

@media (max-width: 820px) {
  /* Below this the sidebar stacks above the questions, and a nested scroller
     inside a stacked column is worse than no scroller at all — it hides content
     and fights the address bar. Hand scrolling back to the page. */
  body[data-mode="teacher"] { height: auto; overflow: visible; display: block; }
  .t-app { height: auto; display: block; }
  .t-split { grid-template-columns: minmax(0, 1fr); }
  .t-side { border-right: 0; border-bottom: 1px solid var(--border); }
  .t-folders, .t-scroll { overflow: visible; min-height: 0; }
  .t-scroll { padding-bottom: var(--sp-8); }
}

/* ── The tree: units, and the modules filed in them ─────────────────────── */

.t-unit { margin-bottom: var(--sp-8); }

.t-unit-head {
  position: relative;   /* the actions menu hangs off the head, not the whole unit */
  display: flex; align-items: center; gap: var(--sp-4);
  /* The head is chrome for the group, not a card of its own: no fill, no
     border. What separates units is the indent of what hangs under them. */
  padding: 0;
}

.t-unit-toggle {
  display: flex; align-items: center; gap: var(--sp-8);
  flex: 1; min-width: 0;
  font: inherit; font-family: var(--font-head); font-weight: 650; font-size: var(--fs-14);
  color: var(--fg);
  background: none; border: 0; border-radius: var(--r-btn);
  padding: var(--sp-4); min-height: 34px;
  text-align: left; cursor: pointer;
}
.t-unit-toggle:hover { background: color-mix(in oklch, var(--fg) 6%, transparent); }
.t-unit-chev {
  width: 12px; height: 12px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.5;
  stroke-linecap: round; stroke-linejoin: round;
  color: var(--fg-muted);
  transform: rotate(90deg);
  transition: transform 0.2s var(--ease-out);
}
.t-unit.is-closed .t-unit-chev { transform: none; }
.t-unit-ico { display: grid; place-items: center; flex: none; color: var(--fg-muted); }
.t-unit-ico svg { width: 16px; height: 16px; }
.t-unit-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.t-unit-count {
  font-family: var(--font-mono); font-size: var(--fs-13); font-weight: 500;
  color: var(--fg-muted); flex: none; padding: 0;
}
/* Overrides the absolutely-positioned, hover-revealed module variant: a unit
   head has no row to sit inside, and its actions are the only way to reorder or
   delete it, so they are always there. */
.t-unit-head .t-folder-dots {
  position: static;
  transform: none;
  opacity: 1;
  width: 28px; height: 28px;
}

/* Indented and rule-joined, so a module reads as belonging to the unit above it
   rather than as another row in one flat list. Kept tight — every pixel here
   comes out of the module name's width. */
.t-unit-body {
  display: flex; flex-direction: column; gap: var(--sp-4);
  margin-left: var(--sp-8);
  padding-left: var(--sp-8);
  border-left: 1px solid var(--border);
}

.t-unit-add,
.t-new-unit {
  display: flex; align-items: center; gap: var(--sp-4);
  font: inherit; font-family: var(--font-head); font-weight: 600; font-size: var(--fs-13);
  color: var(--fg-muted);
  background: none; border: 1px dashed var(--border-strong);
  border-radius: var(--r-btn);
  padding: var(--sp-4) var(--sp-8); min-height: 34px;
  cursor: pointer;
  transition: color 0.16s ease, border-color 0.16s ease;
}
.t-unit-add:hover, .t-new-unit:hover { color: var(--fg); border-color: var(--fg-muted); }
.t-unit-add svg, .t-new-unit svg {
  width: 13px; height: 13px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round;
}
.t-new-unit { width: 100%; justify-content: center; margin-top: var(--sp-8); min-height: 38px; }

/* Renaming happens where the name lives. */
.t-rename {
  width: 100%; min-width: 0;
  font: inherit; font-family: var(--font-head); font-weight: 650; font-size: var(--fs-14);
  color: var(--fg);
  background: var(--bg);
  border: 2px solid var(--c-teal); border-radius: var(--r-sm);
  padding: var(--sp-4);
}

.t-folder {
  display: flex; align-items: center; gap: 0.75rem;
  width: 100%; text-align: left; font: inherit; font-family: var(--font-head);
  /* `font: inherit` does NOT carry colour — without this the button falls back
     to the UA default (black), which vanishes on a dark card. */
  color: var(--fg);
  background: var(--card); border: 2px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-8) 0.75rem; min-height: 56px;
  cursor: pointer; transition: border-color 0.16s ease, background 0.16s ease;
}
.t-folder:hover { border-color: var(--border-strong); }
/* Mixing a hued colour with an achromatic one in oklch interpolates the HUE
   toward 0, so 8% blue in white came out pink (hue 350). Mixing with
   `transparent` only changes alpha, leaving hue intact — which is what the rest
   of this file already does. */
.t-folder.is-open { border-color: var(--c-teal); background: color-mix(in oklch, var(--c-teal) 12%, transparent); }
/* The wash darkens the backdrop enough to drop --fg-muted to 4.3:1 on it, so the
   open row's subtitle steps toward the foreground. Toward --fg rather than to a
   named blue: a fixed blue that passes on the light wash fails on the dark one. */
.t-folder.is-open .t-folder-meta { color: color-mix(in oklch, var(--fg-muted) 65%, var(--fg)); }
.t-folder-ico { display: grid; place-items: center; flex: none; color: var(--fg-muted); }
.t-folder-ico svg { width: 18px; height: 18px; }
.t-folder-body { display: flex; flex-direction: column; min-width: 0; }
.t-folder-name { font-weight: 650; font-size: var(--fs-14); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.t-folder-meta { font-size: var(--fs-13); color: var(--fg-muted); font-weight: 600; }

/* The name field is capped rather than stretched: full width buys nothing for
   "Cell biology", and the language belongs beside the folder it applies to
   rather than stranded at the far edge of a 1366px screen. */
.t-open-head {
  display: grid;
  grid-template-columns: 64px minmax(0, 32rem) 150px;
  justify-content: start;
  gap: var(--sp-8);
  padding: var(--sp-16);
}
@media (max-width: 620px) { .t-open-head { grid-template-columns: 64px minmax(0, 1fr); } .t-open-head .select-wrap { grid-column: 1 / -1; } }
.t-open-name { font-family: var(--font-head); font-weight: 650; font-size: var(--fs-16); }

.t-tools { display: flex; flex-wrap: wrap; gap: var(--sp-8); align-items: center; margin-bottom: var(--sp-16); }
.t-tools-gap { flex: 1; min-width: 0; }

.beta-tag {
  font-size: var(--fs-12); font-weight: 650; letter-spacing: 0.1em; text-transform: uppercase;
  /* 18%, not the heavier tint: gold-deep text stays above 4.5:1 on the tint
     over both the warm page and the warm card. */
  color: var(--c-gold-deep); background: color-mix(in oklch, var(--c-gold) 18%, transparent);
  border-radius: var(--r-pill); padding: 0 var(--sp-4); margin-left: var(--sp-4);
}

.t-questions { display: flex; flex-direction: column; gap: var(--sp-16); }
.t-empty {
  color: var(--fg-muted); font-size: var(--fs-14); background: var(--bg-soft);
  border: 1px dashed var(--border-strong); border-radius: var(--r); padding: var(--sp-16);
}

/* ── A question ─────────────────────────────────────────────────────────── */

.q-card {
  position: relative;
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--r); padding: var(--sp-16);
}

/* A card that was just created gets a ring that fades off it. The entrance
   animation alone is not enough when a duplicate lands next to an identical
   card — 200ms of movement is easy to miss, a ring that lingers is not. Opacity
   only, so it stays honest under reduced motion, where it is the whole signal. */
.q-card::after {
  content: "";
  position: absolute;
  inset: -1px;
  border: 2px solid var(--c-teal);
  border-radius: inherit;
  opacity: 0;
  pointer-events: none;
}
.q-card.is-fresh::after { animation: q-fresh 900ms var(--ease-out) forwards; }
@keyframes q-fresh {
  from { opacity: 1; }
  to   { opacity: 0; }
}
/* Opacity-only was not enough on its own: the ring's resting state is the *end*
   of the fade, so the global 0.001ms clamp plus `forwards` lands it on opacity 0
   before anyone can see it, and the new card is marked by nothing. Drop the fade
   and leave the ring lit — is-fresh is cleared on the next render of the list, so
   it marks the card for exactly as long as "just created" is true. */
@media (prefers-reduced-motion: reduce) {
  .q-card.is-fresh::after { animation: none; opacity: 1; }
}
.q-head { display: flex; align-items: center; gap: var(--sp-8); margin-bottom: var(--sp-8); flex-wrap: wrap; }
.q-num {
  font-family: var(--font-head); font-weight: 700; font-size: var(--fs-13);
  width: 26px; height: 26px; flex: none; display: grid; place-items: center;
  background: var(--bg-soft); border-radius: 50%; color: var(--fg-muted);
}
/* Follows .t-tabs. These are the same widget twice — a segmented control with
   one active half — and they disagreed on four separate decisions: track colour,
   outer radius, inner radius, and whether the active half gets a shadow. The
   track being --bg-soft was also a dark-mode inversion before today's elevation
   fix, and following .t-tabs onto --bg keeps it correct in both. */
.q-types {
  display: flex; gap: var(--sp-4);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-btn);
  padding: var(--sp-4);
}
.q-type {
  font: inherit; font-family: var(--font-head); font-weight: 600; font-size: var(--fs-13);
  border: 0; background: none; color: var(--fg-muted); cursor: pointer;
  border-radius: var(--r-xs); padding: var(--sp-4) var(--sp-8); min-height: 32px;
}
.q-type.is-on { background: var(--card); color: var(--fg); box-shadow: var(--shadow-card); }
.q-type.is-beta.is-on { color: var(--c-gold-deep); }
.q-acts { display: flex; gap: var(--sp-4); align-items: center; margin-left: auto; }

.icon-btn {
  display: grid; place-items: center; width: 34px; height: 34px; flex: none;
  border: 1px solid var(--border); background: var(--card); border-radius: var(--r-btn);
  color: var(--fg-muted); cursor: pointer; padding: 0;
  transition: color 0.16s ease, border-color 0.16s ease;
}
.icon-btn svg {
  width: 15px; height: 15px; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.icon-btn:hover { color: var(--fg); border-color: var(--border-strong); }
.icon-btn.is-danger:hover { color: var(--c-alert); border-color: var(--c-berry); }

/* Skin comes from .select-wrap.is-compact; only the width belongs here. */
/* .select-wrap.is-compact wins on specificity (0,2,1 vs 0,1,1), so the
   min-height and font-size here never landed and only max-width did — the
   select sat 4px taller than the icon buttons it shares a row with. */
.q-move .select-wrap.is-compact select { max-width: 130px; min-height: 30px; font-size: var(--fs-13); }

.q-text {
  width: 100%; font: inherit; font-family: var(--font-body); font-size: var(--fs-16);
  line-height: 1.5; color: var(--fg); background: var(--bg-soft);
  border: 2px solid var(--border); border-radius: var(--r-sm); padding: var(--sp-8) 0.75rem;
  resize: vertical; min-height: 60px;
}
.q-text:focus-visible { background: var(--bg); border-color: var(--c-teal); box-shadow: 0 0 0 var(--ring-w) var(--ring); }

.q-beta { font-size: var(--fs-13); color: var(--fg-muted); margin: var(--sp-8) 0 0; }

/* ── Key points ─────────────────────────────────────────────────────────── */

.kp { margin-top: var(--sp-16); }
/* Styling lives on .mono-label near the top of the teacher section. */
.kp-head { margin: 0 0 var(--sp-8); }
.kp-row { display: grid; grid-template-columns: 12px minmax(0, 1fr) 34px; gap: var(--sp-8); align-items: center; margin-bottom: var(--sp-8); }
/* The editor's bullet is chrome, not a score. Green in this product means a key
   point was HIT, and that meaning belongs to .kp-result-row.is-hit on the
   student's side — where it stays. Here nothing has been answered yet: this is
   a teacher typing the list. */
.kp-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--c-teal); justify-self: center; }
.kp-input {
  width: 100%; font: inherit; font-size: var(--fs-14); color: var(--fg);
  background: var(--bg-soft); border: 2px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-8); min-height: 40px;
}
.kp-input:focus-visible { background: var(--bg); border-color: var(--c-teal); box-shadow: 0 0 0 var(--ring-w) var(--ring); }
.kp-del { width: 34px; height: 34px; }
.kp-add {
  font: inherit; font-family: var(--font-head); font-weight: 600; font-size: var(--fs-13);
  color: var(--fg-muted); background: none; border: 0; cursor: pointer;
  padding: var(--sp-4) 0 0 calc(10px + var(--sp-8)); min-height: 36px;
}
/* -ink, not the fill: this is a word. Same reason .ln-link takes it. */
.kp-add:hover { color: var(--c-accent-ink); }

/* ── Student: the question, and the graded answer ───────────────────────── */

.p-question {
  font-family: var(--font-head); font-weight: 650;
  font-size: clamp(1.05rem, 3.4vw, 1.3rem); line-height: 1.35;
  margin: 0 0 var(--sp-8); text-wrap: balance;
}

/* How much of an answer is wanted. Sits under the question and above the mic,
   because it is read once on the way to pressing it. Quiet — it is instruction,
   not a warning, and a child about to speak does not need another loud thing. */
.p-asks {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-8);
  margin: 0 0 var(--sp-16);
  font-size: var(--fs-14);
  color: var(--fg-muted);
}
.p-marks {
  flex: none;
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-13);
  letter-spacing: 0.03em;
  color: var(--fg);
  /* Neutral, with a hairline instead of a gold wash. Gold means "this wants
     something from you" everywhere else in the product — a redo request, a late
     assignment, a withdrawn consent. "3 marks" wants nothing; it is the size of
     the question, told to a child before they answer, and dressing a neutral
     fact as a flag makes the flags mean less. */
  background: var(--bg-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: var(--sp-4) var(--sp-8);
  /* Sits on the first line of the depth text rather than above it. */
  position: relative; top: -1px;
}
.p-depth { min-width: 0; }

.kp-result { list-style: none; margin: 0; padding: 0; }
.kp-result-row {
  display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: var(--sp-8);
  font-size: var(--fs-14); margin-bottom: var(--sp-8); align-items: baseline;
}
.kp-mark { font-family: var(--font-head); font-weight: 700; justify-self: center; }
.kp-result-row.is-hit .kp-mark { color: var(--c-grass); }
.kp-result-row.is-missed { color: var(--fg-muted); }
.kp-result-row.is-missed .kp-mark { color: var(--c-gold-deep); }

/* Keyword fallback is not a grade, so the verdict must not look like one. */
.verdict-head.is-provisional { color: var(--c-gold-deep); }

/* ── Preview banner for pronunciation ───────────────────────────────────── */

.beta-bar {
  display: flex; align-items: center; gap: var(--sp-8); flex-wrap: wrap;
  background: color-mix(in oklch, var(--c-gold) 16%, transparent);
  border: 1px solid color-mix(in oklch, var(--c-gold) 45%, transparent);
  border-radius: var(--r-sm); padding: var(--sp-8); margin: 0 0 var(--sp-16);
  font-size: var(--fs-13); color: var(--fg);
}
.beta-bar svg {
  width: 15px; height: 15px; flex: none; fill: none; stroke: var(--c-gold-deep);
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.beta-bar.is-spent { background: color-mix(in oklch, var(--c-berry) 12%, transparent); border-color: color-mix(in oklch, var(--c-berry) 40%, transparent); }
.beta-bar.is-spent svg { stroke: var(--c-berry); }
.beta-link { color: var(--fg-muted); font-weight: 600; font-size: var(--fs-13); margin-left: auto; }
.beta-link:hover { color: var(--fg); }

/* ── Teacher UI, second pass ────────────────────────────────────────────────
   Written after looking at a screenshot rather than at the markup. The first
   version wrapped its toolbar, orphaned the primary action onto its own row,
   and gave every question ~370px of height — three questions filled the page. */

/* Adding content and managing the folder are separate jobs on separate rows,
   so neither can wrap into the other. */
.t-tools {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-8);
  flex-wrap: wrap;
  /* Part of the fixed frame now, not the scrolling list: these actions stay
     reachable however far down a folder you are. */
  padding: 0 var(--sp-16) var(--sp-16);
  margin: 0;
  border-bottom: 1px solid var(--border);
}
.t-tools-main,
.t-tools-folder { display: flex; gap: var(--sp-8); flex-wrap: wrap; align-items: center; }

/* Preview is a check, not a destination — it shouldn't compete with Add. */
.t-try svg { fill: currentColor; stroke: none; width: 12px; height: 12px; }

/* ── Questions: much tighter ────────────────────────────────────────────── */

.t-questions { gap: var(--sp-8); }

.q-card { padding: var(--sp-16); }
.q-head { margin-bottom: var(--sp-8); gap: var(--sp-8); }

/* Auto-growing: two lines by default, expands with content instead of
   reserving 60px of empty box on every card. */
.q-text {
  min-height: 0;
  padding: var(--sp-8);
  font-size: var(--fs-14);
  resize: none;
  overflow: hidden;
}

/* The heading was long and repeated on every card. The dot column already
   signals "list of points"; the label only needs to be said once, quietly. */
.kp { margin-top: var(--sp-8); }
.kp-head { margin-bottom: var(--sp-8); }
.kp-row { grid-template-columns: 10px minmax(0, 1fr) 28px; gap: var(--sp-8); margin-bottom: var(--sp-8); }
.kp-input { min-height: 34px; padding: var(--sp-4) var(--sp-8); font-size: var(--fs-14); }
.kp-del { width: 28px; height: 28px; }
.kp-del svg { width: 13px; height: 13px; }
.kp-add { padding-top: var(--sp-4); font-size: var(--fs-13); }

/* Row actions shrink so five of them fit without pushing the type toggle. */
.q-acts { gap: var(--sp-4); }
.icon-btn { width: 30px; height: 30px; }
.icon-btn svg { width: 14px; height: 14px; }
.icon-btn:disabled { opacity: 0.3; cursor: default; }
.icon-btn:disabled:hover { color: var(--fg-muted); border-color: var(--border); }
.q-move select { min-height: 30px; max-width: 96px; font-size: var(--fs-13); }

/* Type identity: the active state now carries colour, so scanning a folder
   tells you at a glance which questions are graded and which are preview. */
.q-type { padding: var(--sp-4) var(--sp-8); min-height: 28px; font-size: var(--fs-13); }
.q-card:has(.q-type.is-beta.is-on) { border-color: color-mix(in oklch, var(--c-gold) 55%, transparent); }
.q-type.is-beta.is-on { background: color-mix(in oklch, var(--c-gold) 18%, transparent); } /* keeps gold-deep text >= 4.5:1 on warm surfaces */

/* ── Sidebar ────────────────────────────────────────────────────────────── */

/* A module sits a level under its unit, so it reads a step quieter and a step
   shorter. The height was going on leading, not on content — two lines at 1.55
   is 40px of text in a 60px row. Tightened to 44px, which is still a thumb
   target on a tablet, and gets two more modules on screen per unit. */
.t-folder { min-height: 44px; padding: var(--sp-4) var(--sp-8); gap: var(--sp-8); }
/* Deliberately a step under .t-unit-name (0.85rem) — a child label that reads
   larger than the group heading above it inverts the hierarchy. */
.t-folder-name { font-size: var(--fs-13); line-height: 1.3; }
.t-folder-meta { font-size: var(--fs-13); line-height: 1.25; }
.t-unit-body { gap: var(--sp-4); }

/* ── Status ─────────────────────────────────────────────────────────────── */

/* A status bar across the bottom of the frame rather than a footer that has to
   be scrolled to — the point of it is to be glanceable while working. */
.t-foot {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  flex-wrap: wrap;
  padding: var(--sp-8) var(--sp-16) calc(var(--sp-8) + env(safe-area-inset-bottom, 0px));
  background: var(--bg-soft);
  border-top: 1px solid var(--border);
  box-shadow: var(--shadow-up);
}
.t-foot-note { font-size: var(--fs-13); color: var(--fg-muted); margin-left: auto; }
.t-status-note { margin: 0; padding: 0 var(--sp-16) var(--sp-8); }

.t-status { display: flex; gap: var(--sp-8); flex-wrap: wrap; }
.t-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  /* Mono, with .save-flag beside it: .t-foot is a diagnostic readout and its
     contents were rendering in two fonts side by side in one bar. */
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-13);
  border-radius: var(--r-pill);
  padding: var(--sp-4) var(--sp-8);
  border: 1px solid var(--border);
  color: var(--fg-muted);
  background: var(--card);
}
.t-pill-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.t-pill.is-ok { color: var(--c-grass-deep); border-color: color-mix(in oklch, var(--c-grass) 40%, transparent); }
.t-pill.is-off { color: var(--c-gold-deep); border-color: color-mix(in oklch, var(--c-gold) 45%, transparent); }
/* Amber for "nobody connected it", red for "connected and failing". They are
   different problems and used to share a colour. */
.t-pill.is-err { color: var(--c-alert); border-color: color-mix(in oklch, var(--c-berry) 45%, transparent); }

/* Said plainly, because a teacher writing questions deserves to know the
   grader is not connected before they write twenty of them. */
.t-status-note {
  font-size: var(--fs-13);
  color: var(--c-gold-deep);
  margin: var(--sp-8) 0 0;
}
.t-status-note.is-err { color: var(--c-alert); }

/* ── Third pass: hierarchy inside a question ────────────────────────────────
   The question and its key points were rendered as identical filled fields, so
   a card read as a stack of five equal inputs. The question is the content; the
   key points are notes about it, and they should not look the same. */

/* Keeps the heading weight so the question still outranks its key points, but
   stays visibly a field. The previous pass made it transparent with an
   invisible border, so it read as static text and nobody would think to click
   it — typography hierarchy at the cost of the affordance. */
.q-text {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-16);
  line-height: 1.4;
  background: var(--bg-soft);
  border: 2px solid var(--border);
  border-radius: var(--r-sm);
  padding: var(--sp-8);
  transition: border-color 0.16s ease, background 0.16s ease;
}
.q-text::placeholder { font-weight: 600; color: var(--fg-muted); }
.q-text:hover { border-color: var(--border-strong); }
.q-text:focus-visible {
  background: var(--bg);
  border-color: var(--c-teal);
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}

/* Key points sit quieter than the question they belong to. */
.kp-input { background: var(--bg-soft); border-width: 1px; }

/* ── What a question asks for ────────────────────────────────────────────
   Marks and expected depth, side by side above the key points: they describe
   the shape of a full answer, the key points describe its content, and the
   teacher fills them in that order. Marks is narrow because it holds at most
   three characters and a wide box invites a sentence. */
.q-guide {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: var(--sp-8);
  margin-top: var(--sp-8);
}
@media (max-width: 520px) {
  .q-guide { grid-template-columns: 1fr; }
}
.q-guide label { display: block; min-width: 0; }
.q-marks, .q-depth {
  width: 100%;
  font: inherit;
  font-size: var(--fs-14);
  color: var(--fg);
  background: var(--bg-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: var(--sp-4) var(--sp-8);
  min-height: 34px;
}
.q-marks { font-variant-numeric: tabular-nums; }
.q-marks:hover, .q-depth:hover { border-color: var(--border-strong); }
.q-marks:focus-visible, .q-depth:focus-visible {
  background: var(--bg);
  border-color: var(--c-teal);
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}
.q-depth::placeholder { color: var(--fg-muted); }

/* ── Sidebar help card ──────────────────────────────────────────────────── */

.t-help {
  display: block;
  margin-top: var(--sp-16);
  padding: var(--sp-16);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.t-help-head {
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-12);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0 0 var(--sp-8);
}
.t-help p { font-size: var(--fs-13); line-height: 1.5; color: var(--fg-muted); margin: 0 0 var(--sp-8); }
.t-help p b { color: var(--fg); }
.t-help-foot {
  border-top: 1px solid var(--border);
  padding-top: var(--sp-8);
  margin-bottom: 0 !important;
  font-style: italic;
}
/* Was hidden on tablet because it was an always-open card competing for a
   collapsed sidebar. Closed, the disclosure is one row — it can stay. */

/* On a tablet the folder list ran full width, so five folders pushed the
   questions off screen. Two columns keeps every folder visible (no hidden
   scroller) while halving the height it costs. */
/* Two columns of modules inside each unit, so a stacked sidebar costs half the
   height. The units themselves stay stacked — the tree is the structure. */
@media (max-width: 820px) {
  .t-unit-body { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-8); }
  .t-unit-add { grid-column: 1 / -1; }
}
@media (max-width: 480px) {
  .t-unit-body { grid-template-columns: 1fr; }
}

/* ── Busy state for anything that waits on a request ────────────────────────
   Motion, not brightness: a dimmed control already looks disabled, and a glow
   has to be re-tuned per theme. A small bar travelling the bottom edge reads as
   "working" on a filled button and an outlined one alike, because it inherits
   currentColor. Static fallback under reduced motion. */

.is-busy {
  position: relative;
  overflow: hidden;
  cursor: progress;
  pointer-events: none;   /* a second click would fire a second request */
}
.is-busy::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 3px;
  width: 30%;
  border-radius: var(--r-pill);
  background: currentColor;
  opacity: 0.85;
  animation: busy-sweep 1s cubic-bezier(0.55, 0, 0.45, 1) infinite;
}
@keyframes busy-sweep {
  from { transform: translateX(-110%); }
  to   { transform: translateX(440%); }
}
@media (prefers-reduced-motion: reduce) {
  .is-busy::after { animation: none; width: 100%; opacity: 0.35; }
}

/* The mic shows nothing while scoring. The working panel directly below it is
   already up, naming the stage and running its own travelling bar, and a second
   indicator said the same thing twice — worse, a ring spinning around a button
   that is disabled reads as though the button is still doing something. What
   .is-busy still buys here is the part that matters: pointer-events off, so a
   second tap can't fire a second request.

   The permission wait keeps its ring (.mic.is-waiting, above): that one has no
   panel to speak for it, which is exactly why it was added. */
.mic.is-busy::after { content: none; }

/* Grading takes seconds, so the panel says which stage it is at rather than
   showing an unlabelled spinner. .grading-text itself is set once, beside
   .grading in the busy-state section; this used to repeat it word for word. */
.grading-sub { font-size: var(--fs-13); color: var(--fg-muted); margin: var(--sp-8) 0 0; }

/* ── Folder row: select action + its own menu ───────────────────────────── */

.t-folder-shell { position: relative; }

/* Room on the right so a long folder name never runs under the ⋯ button. */
.t-folder { padding-right: 2.5rem; }

.t-folder-dots {
  position: absolute;
  top: 50%;
  right: 0.45rem;
  transform: translateY(-50%);
  width: 30px;
  height: 30px;
  background: transparent;
  border-color: transparent;
  opacity: 0;
  transition: opacity 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.t-folder-dots svg { stroke-width: 3; stroke-linecap: round; }
/* Always visible on touch, where there is no hover to reveal it. */
@media (hover: none) { .t-folder-dots { opacity: 1; } }
.t-folder-shell:hover .t-folder-dots,
.t-folder-dots:focus-visible,
.t-folder-dots.is-open { opacity: 1; }
.t-folder-dots.is-open { background: var(--bg-soft); border-color: var(--border); color: var(--fg); }

.t-menu {
  position: absolute;
  top: calc(100% - 4px);
  right: 0.4rem;
  z-index: 30;
  min-width: 168px;
  background: var(--card);
  border: 1px solid var(--border-strong);
  border-radius: var(--r);
  box-shadow: var(--shadow-pop);
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
}
/* An item inside a rounded container takes the smaller radius, or the two arcs
   trace each other. The same relationship .q-type now has with .q-types. */
.t-menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  width: 100%;
  font: inherit;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-13);
  color: var(--fg);
  background: none;
  border: 0;
  /* Smaller than the menu that holds it, by the menu's own padding: a popover
     on --r-card (14px) with 4px inside it leaves --r-sm (10px), so the two arcs
     run parallel instead of tracing each other. The same relationship .q-type
     has with .q-types. */
  border-radius: var(--r-sm);
  padding: var(--sp-8);
  min-height: 40px;
  cursor: pointer;
  text-align: left;
}
.t-menu-item svg {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.t-menu-item:hover { background: var(--bg-soft); }
/* Shown rather than removed: an item that vanishes at the ends of a list makes
   the menu jump, and the greyed row says "this exists, just not here". */
.t-menu-item:disabled { opacity: 0.35; cursor: default; }
.t-menu-item:disabled:hover { background: none; }
/* Destructive reads as destructive before it is clicked, not after. */
.t-menu-item.is-danger { color: var(--c-alert); }
.t-menu-item.is-danger:hover { background: color-mix(in oklch, var(--c-berry) 12%, transparent); }

/* ── Confirmation dialog ────────────────────────────────────────────────── */

.modal {
  border: 0;
  padding: 0;
  background: transparent;
  max-width: 100vw;
  max-height: 100vh;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
/* A flat scrim. The page behind used to be blurred as well, which is the
   frosted-glass look the Work Plan lists under Avoid; the scrim alone already
   separates the dialog from what it covers. */
.modal::backdrop {
  background: var(--scrim);
}
.modal-body {
  width: min(440px, calc(100vw - 2rem));
  margin: auto;
  position: absolute;
  inset: 0;
  height: fit-content;
  background: var(--card);
  color: var(--fg);
  border-radius: var(--r);
  padding: var(--sp-24);
  box-shadow: var(--shadow-modal);
  text-align: center;
}
.modal-icon {
  width: 48px; height: 48px;
  margin: 0 auto var(--sp-16);
  display: grid; place-items: center;
  border-radius: 50%;
  background: color-mix(in oklch, var(--c-berry) 14%, transparent);
  color: var(--c-alert);
}
.modal-icon svg {
  width: 22px; height: 22px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.modal-title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: var(--fs-20);
  margin: 0 0 var(--sp-8);
  text-wrap: balance;
}
.modal-text { font-size: var(--fs-14); color: var(--fg-muted); margin: 0; }

/* What actually disappears, itemised — the part people skip in a generic
   "are you sure?" is exactly the part that matters. */
.modal-list {
  list-style: none;
  margin: var(--sp-16) 0 0;
  padding: var(--sp-8) var(--sp-16);
  background: var(--bg-soft);
  border-radius: var(--r-sm);
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
}
.modal-list li {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-14);
}

/* The recording notice borrows .modal-list from the delete confirmations, where
   the itemised list IS the point — "what disappears" is the part people skip and
   must not. Here the list is supporting detail and the paragraph above it is the
   disclosure a child actually has to read, so the weighting was upside down: the
   explanation sat in muted grey at 0.9rem while the bullets under it ran at 700.
   Same components, weighting inverted.

   Weight and colour only, deliberately. --font-body and --font-head both resolve
   to --font-play here, so switching family would have been a no-op dressed up as
   a decision. */
#rec-modal .modal-text { color: var(--fg); font-size: var(--fs-16); }
.rec-list li { font-weight: 400; }

.modal-actions { display: flex; gap: var(--sp-8); margin-top: var(--sp-24); }
.modal-actions .btn { flex: 1; }

@media (prefers-reduced-motion: no-preference) {
  .modal[open] .modal-body { animation: modal-in 0.22s var(--ease-out); }
  @keyframes modal-in {
    from { opacity: 0; transform: translateY(8px) scale(0.98); }
    to   { opacity: 1; transform: none; }
  }
}

/* ── Destructive question control ───────────────────────────────────────── */

/* Red before it is clicked, not only on hover — a control that turns dangerous
   on approach tells you too late. */
.icon-btn.is-danger, .q-del { color: var(--c-alert); }
.icon-btn.is-danger:hover, .q-del:hover {
  color: var(--c-alert);
  border-color: var(--c-berry);
  background: color-mix(in oklch, var(--c-berry) 10%, transparent);
}

/* Armed: grows to fit the word, fills, and stays put for four seconds. Widening
   is deliberate — the button moving under the cursor is what stops a double
   click sailing straight through both states. */
/* .icon-btn is a centred grid, so a second child became a second ROW and the
   label fell outside the fixed height. Armed, it becomes a flex row instead. */
.q-del.is-armed {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  width: auto;
  min-width: 0;
  gap: var(--sp-4);
  padding: 0 var(--sp-8);
  background: var(--c-berry);
  border-color: var(--c-berry);
  color: var(--ink-on-bright);
}
.q-del.is-armed svg { flex: none; }
.q-del-label {
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-13);
  white-space: nowrap;
}

/* ── Undo ───────────────────────────────────────────────────────────────── */

.undo-bar {
  position: fixed;
  left: 50%;
  bottom: calc(1.1rem + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  max-width: calc(100vw - 2rem);
  padding: var(--sp-8) var(--sp-8) var(--sp-8) var(--sp-16);
  border-radius: var(--r);
  background: var(--fg);
  color: var(--bg);
  box-shadow: var(--shadow-float);
  font-size: var(--fs-14);
}
.undo-bar svg {
  width: 16px; height: 16px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.undo-bar #undo-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.undo-go {
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-13);
  color: var(--bg);
  background: transparent;
  border: 2px solid color-mix(in oklch, var(--bg) 45%, transparent);
  border-radius: var(--r-btn);
  padding: var(--sp-4) 0.75rem;
  min-height: 34px;
  cursor: pointer;
  flex: none;
}
.undo-go:hover { border-color: var(--bg); }

.undo-x {
  display: grid; place-items: center;
  width: 28px; height: 28px; flex: none;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: var(--r-btn);
  color: color-mix(in oklch, var(--bg) 70%, transparent);
  cursor: pointer;
}
.undo-x:hover { color: var(--bg); }

@media (prefers-reduced-motion: no-preference) {
  .undo-bar.is-in { animation: undo-in 0.24s var(--ease-out); }
  @keyframes undo-in {
    from { opacity: 0; transform: translateX(-50%) translateY(10px); }
    to   { opacity: 1; transform: translateX(-50%) translateY(0); }
  }
}

/* ── Page header: title + info ──────────────────────────────────────────── */

.t-head {
  position: relative;   /* the info tooltip hangs off this, overlaying the work surface */
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-8);
  padding: var(--sp-16);
  background: var(--bg-soft);
  border-bottom: 1px solid var(--border);
}
.t-head h1 { margin: 0; }

/* ── Teacher surface switch ──────────────────────────────────────────────────
   Segmented rather than tabs-with-underlines: there are exactly two, they are
   peers, and the control needs to survive sitting beside an h1. */
.t-tabs {
  display: flex;
  gap: var(--sp-4);
  padding: var(--sp-4);
  border-radius: var(--r-btn);
  background: var(--bg);
  border: 1px solid var(--border);
}
.t-tab {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-13);
  /* 24px tall was the smallest interactive target in the product, and it sat on
     the control that switches between its two halves. 36px is the floor
     .btn-mini already sets for a small control, so this joins that family
     rather than inventing a seventh size; 44px on a phone, where it is a thumb
     rather than a cursor. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: var(--sp-4) 0.75rem;
  border: 0;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--fg-muted);
  cursor: pointer;
}
.t-tab.is-active { background: var(--card); color: var(--fg); box-shadow: var(--shadow-card); }
.t-tab:hover:not(.is-active) { color: var(--fg); }
/* The shared focus ring is a box-shadow, and `.t-tab.is-active` (0,2,0)
   outranks `button:focus-visible` (0,1,1) — so the one tab that had focus was
   the one tab that showed nothing. Restate both shadows together rather than
   fighting the cascade from the other side. */
.t-tab.is-active:focus-visible {
  box-shadow: var(--shadow-card), 0 0 0 var(--ring-w) var(--ring);
}
@media (max-width: 640px) {
  .t-tab { min-height: 44px; }
}

/* ── Classes ─────────────────────────────────────────────────────────────── */
/* ── Class detail: a header and a grid of cards, not a column ──────────────── */

.cls-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--sp-16);
  flex-wrap: wrap;
  max-width: 66rem;
  margin: 0 0 var(--sp-16);
}
.cls-head-name { display: flex; align-items: center; gap: var(--sp-8); min-width: 0; }
/* break-word, not anywhere. Both break a name that genuinely cannot fit, but
   `anywhere` also shrinks the title's min-content to one letter, so beside a
   row of buttons in a flex header it gave up its width first and "Science"
   came out as "Scienc / e". With break-word the buttons wrap under the name. */
.cls-title { font-size: var(--fs-20); font-weight: 650; margin: 0; overflow-wrap: break-word; }
.cls-rename { display: flex; align-items: center; gap: var(--sp-8); flex-wrap: wrap; }
.cls-rename .t-input { width: 16rem; max-width: 60vw; }
/* Right-aligned as a block so the code and its hint read as one object. */
.cls-head-code { display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-4); }
.cls-code-hint { font-size: var(--fs-13); color: var(--fg-muted); margin: 0; max-width: 34ch; text-align: right; }
@media (max-width: 640px) {
  .cls-head-code { align-items: flex-start; }
  .cls-code-hint { text-align: left; }
}

.cls-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(21rem, 1fr));
  gap: var(--sp-16);
  align-items: start;
  max-width: 66rem;
}
.cls-span { grid-column: 1 / -1; }

/* Loading rows, the right shape and the right height, so the results card fills
   in rather than flashing the word "Loading…" — it is the first card on the page
   now, so that placeholder was what a teacher's eye landed on every time. The
   shimmer is decorative and aria-hidden; the real signal for a screen reader is
   aria-busy on the container. */
.cls-results-list.is-skeleton { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-8); }
.cls-results-list.is-skeleton li {
  height: 2.4rem;
  border-radius: var(--r-sm);
  background: linear-gradient(90deg,
    var(--bg-soft) 0%, color-mix(in oklab, var(--bg-soft) 60%, var(--card)) 50%, var(--bg-soft) 100%);
  background-size: 200% 100%;
  animation: cls-skeleton 1.4s ease-in-out infinite;
}
@keyframes cls-skeleton { from { background-position: 200% 0; } to { background-position: -200% 0; } }
/* A shimmer is pure decoration, so under reduced motion it simply holds still —
   the rows still occupy the space, which is the part that matters. */
@media (prefers-reduced-motion: reduce) {
  .cls-results-list.is-skeleton li { animation: none; }
}

.cls-card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--sp-16);
  min-width: 0;
}
/* These heads are h3s wearing .mono-label, so the three declarations that stop
   a browser's heading defaults from showing through — big, bold, and margined
   top and bottom — are load-bearing here in a way they are not for the labels
   and captions that share that class. Restated so a later trim over there can't
   quietly turn three card heads back into headings. Same values on purpose. */
.cls-card-head {
  font-size: var(--fs-12);
  font-weight: 500;
  margin: 0 0 var(--sp-8);
}

.cls-code-row { display: flex; align-items: center; gap: var(--sp-8); flex-wrap: wrap; margin: var(--sp-8) 0; }
/* The code is the one string in this app that gets read aloud across a room and
   copied off a whiteboard, so it is set large, monospaced and widely tracked —
   the shapes have to survive being squinted at from the back row. */
.cls-code {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-32);
  letter-spacing: 0.22em;
  /* Vertical padding was 0.3rem against a 10px radius, so the largest text on
     the Classes surface sat inside its own corner arc. The phantom right gap
     from tracking is still accounted for; only the floor moved. */
  padding: var(--sp-8) var(--sp-8) var(--sp-8) 0.75rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--card);
  color: var(--fg);
  cursor: copy;
}
.cls-code:hover { border-color: var(--c-teal); color: var(--c-accent-ink); }
.cls-hint { font-size: var(--fs-13); color: var(--fg-muted); margin: 0 0 var(--sp-24); max-width: 46ch; }

/* Amber, not red: this is something to know before typing, not a failure. */
.cls-warn {
  font-size: var(--fs-13);
  color: var(--c-gold-deep);
  background: color-mix(in oklch, var(--c-gold) 12%, transparent);
  border-radius: var(--r-sm);
  padding: var(--sp-8);
  margin: var(--sp-8) 0 var(--sp-16);
  max-width: 52ch;
}

.cls-roster { list-style: none; margin: 0 0 var(--sp-16); padding: 0; display: flex; flex-wrap: wrap; gap: var(--sp-8); }
.cls-student {
  display: inline-flex; align-items: center; gap: var(--sp-4);
  background: var(--bg-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: var(--sp-4) var(--sp-4) var(--sp-4) 0.75rem;
  font-size: var(--fs-13);
}
/* Both of these are .icon-btn now, so the size and the hover come from the one
   place that defines an icon button. What is left here is only the position in
   their rows — the six different icon-button sizes in this file were six
   components each deciding for themselves, and two of them were these. */
.cls-student-remove,
.cls-unassign { flex: none; }
/* ── Assigned work ───────────────────────────────────────────────────────
   Chips were wrong here the moment a per-module setting arrived: every chip had
   to repeat the words explaining its own switch. A table says it once in the
   header and leaves the rows to carry only what differs between them. */
.cls-assign-table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 var(--sp-24);
  font-size: var(--fs-14);
}
.cls-assign-table th,
.cls-assign-table td {
  text-align: left;
  padding: var(--sp-8) var(--sp-8) var(--sp-8) 0;
  vertical-align: middle;
}
.cls-assign-table thead th {
  padding-bottom: var(--sp-8);
  border-bottom: 1px solid var(--border);
}
.cls-assign-table tbody th {
  font-weight: 600;
  color: var(--fg);
}
.cls-assign-table tbody tr + tr th,
.cls-assign-table tbody tr + tr td {
  border-top: 1px solid var(--border);
}
/* width:1% with nowrap is "as narrow as the content allows" — and the widest
   content in this column is the heading, so the column sizes itself to the
   words that explain it and the module name takes whatever is left. Letting the
   heading wrap instead cost four lines of stacked capitals above one switch.

   Right-aligned so the switches sit at the end of their rows rather than
   floating in the middle of the card: left-aligned in a column sized by its
   heading put each switch a heading's width away from the row's right edge. */
/* The switch that used to live here became a named choice, so the column stops
   being a 1%-wide sliver sized to a toggle and takes the width its select and
   its one-line consequence need. Left-aligned now: a sentence right-aligned
   against a ragged left edge is unreadable, and the sentence is the point. */
.cls-assign-table .col-mode {
  width: 15rem;
  padding-right: var(--sp-16);
  text-align: left;
  vertical-align: top;
}
.cls-assign-table .cls-mode { min-height: 34px; font-size: var(--fs-13); }
/* Quiet, and allowed to wrap — it is the consequence of the control above it,
   not a second control. */
.cls-mode-hint {
  margin: var(--sp-4) 0 0;
  font-size: var(--fs-13);
  line-height: 1.45;
  color: var(--fg-muted);
}
@media (max-width: 620px) {
  .cls-assign-table .col-mode { width: auto; }
}
.cls-assign-table td:last-child { width: 1%; padding-right: 0; text-align: right; }
.cls-assign-table tr.is-pending { opacity: 0.55; }

/* ── Switch ──────────────────────────────────────────────────────────────
   The product's first one. A real checkbox, restyled: it keeps the label
   association, the focus behaviour and the keyboard handling that a div with a
   click listener would have to reimplement badly.

   State is carried by the thumb's position as well as by colour, so it survives
   a colourblind reader and a monochrome print. */
/* The label is the hit target, not the 30x18 track. This switch decides what
   every student in a class sees and it was a 30x18px tap on an iPad — under the
   24x24 minimum, never mind a comfortable one. The column is right-aligned, so
   the extra width lands in whitespace that was already there. */
.sw {
  display: inline-flex; align-items: center; justify-content: flex-end;
  gap: var(--sp-8); cursor: pointer;
  min-height: 44px; padding: 0 var(--sp-4);
}
.sw input {
  /* Not display:none — that removes it from the accessibility tree and from tab
     order. Kept in the layout, drawn by the track beside it. */
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  margin: 0;
}
.sw-track {
  position: relative;
  flex: none;
  width: 30px; height: 18px;
  border-radius: var(--r-pill);
  background: var(--border-strong);
  transition: background 140ms ease;
}
.sw-thumb {
  position: absolute;
  top: 2px; left: 2px;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--bg);
  transition: transform 140ms ease;
}
/* Teal. "Show AI results" being on is not a right answer, it is a setting the
   teacher chose — and this switch sits two columns from the marked / needs-review
   chips, which is where green does mean an outcome. */
.sw input:checked + .sw-track { background: var(--c-teal); }
.sw input:checked + .sw-track .sw-thumb { transform: translateX(12px); }
/* The app's ring, not a hard outline — this file's first rule is that focus is
   one soft ring and never a stacked outline, and this was the only place
   breaking it. */
.sw input:focus-visible + .sw-track {
  outline: none;
  box-shadow: 0 0 0 var(--ring-w) var(--ring);
}
.sw input:disabled + .sw-track { opacity: 0.5; }

@media (prefers-reduced-motion: reduce) {
  .sw-track, .sw-thumb { transition: none; }
}

/* For text that names a control for a screen reader and would be noise on
   screen — a column of × buttons needs a heading; nobody needs to read it. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Not an error and not a warning — just the honest state of an empty list. */
.cls-none { font-size: var(--fs-13); color: var(--fg-muted); margin: 0 0 var(--sp-16); list-style: none; }
.cls-add { display: flex; gap: var(--sp-8); flex-wrap: wrap; margin-bottom: var(--sp-24); }

/* The results card spans the grid now, so on a wide screen the row would run
   the full width of the page and put a child's name and their figures at
   opposite ends of a 1300px line. A measure cap keeps the two ends readable in
   one glance. */
.cls-results-list { list-style: none; margin: var(--sp-8) 0 0; padding: 0; max-width: 46rem; }
/* Shared row chrome for three different lists — the roster, the erasure list and
   the results — which is why the layout below is on a modifier and not here.
   Padding, rule and baseline are common; the columns are not. */
.cls-result-row {
  display: flex; align-items: baseline; gap: var(--sp-8);
  padding: var(--sp-8) 0;
  border-bottom: 1px solid var(--border);
}

/* The results row only, and a grid rather than a flex line. The score slots were
   given fixed widths so the figures read down as a column — and they do — but
   everything to their left was still flowing: .cls-result-name carries a
   min-width, which is a floor and not a width, so "Devanshi Ramakrishnan" pushed
   her own attempt count right and the "to review" badge after it, and no two
   rows started those two things in the same place. Half a table is not a table.

   Five explicit columns in DOM order — name, attempts, outstanding, figures,
   listen — so every cell starts where the one above it did, whatever is or is
   not in it. */
.cls-result-row.is-figures {
  display: grid;
  /* Every column fixed except the name, and that one deliberately: each row is
     its own grid, so any column sized to its own content resolves differently
     per row and the 1fr beside it absorbs the difference. With `auto` on the
     figures the student who had not started — no figures at all — collapsed that
     column to zero and her name column swallowed 220px, putting "not started"
     220px right of every other row's second cell.
     13.7rem is the figures block: two 6.5rem slots and the 0.7rem between them.
     The two before it are sized to their own headings rather than to their
     contents — "TO REVIEW" is wider than any count that will ever sit under it,
     which is the right way round: the heading is the fixed thing. */
  grid-template-columns: minmax(5rem, 1fr) 4rem 5.4rem 13.7rem 30px;
}
/* And it must never wrap, whatever the column does. A count that folds under
   itself reads as two facts. */
.is-figures .cls-attempts-toggle { white-space: nowrap; }

/* The header row. Named once here so no row has to carry its own labels, which
   is what forced an empty cell to read "— content" — a dash apologising for
   itself in a column that already had a name. */
.cls-result-row.is-head {
  padding-top: 0;
  border-bottom: 1px solid var(--border-strong);
}
.cls-col-head {
  font-family: var(--font-mono);
  font-size: var(--fs-12);
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
/* The numeric headings sit over columns of right-aligned figures, so they are
   right-aligned too — a heading that does not sit above its own column is worse
   than none. The name is the one left-aligned column. */
.is-head .cls-col-head { text-align: right; }
.is-head .cls-result-name { text-align: left; font-weight: 500; }

/* Now a bare figure under a column that names it, so it takes the same size,
   alignment and tabular figures as the two beside it. It was 0.66rem mono
   carrying its own words, which put a third type size in a row of numbers. */
.cls-result-pending {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  font-weight: 700;
  color: var(--c-alert);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
/* Every empty cell in the list, whatever column it is in: the same glyph, the
   same size, the same right edge as the figures above and below it. */
.is-figures .cls-attempts-toggle,
.is-figures .cls-result-pending,
.is-figures .cls-score-part { justify-self: stretch; text-align: right; }
/* The grid column is the width here, and a floor is what let a long name shunt
   the rest of its row. It wraps instead; baseline alignment keeps the figures
   level with its first line. The other two lists keep the floor — it is what
   lines their second column up. */
.cls-result-row.is-figures .cls-result-name { min-width: 0; }
.cls-result-row:last-child { border-bottom: 0; }
/* A row born optimistically: on screen already, inert until its real id
   arrives. Dimmed rather than spinnered — the wait is a round trip, not a job. */
.is-pending { opacity: 0.5; pointer-events: none; }
.cls-result-name { font-weight: 600; font-size: var(--fs-14); min-width: 8rem; }
.cls-result-meta { font-size: var(--fs-13); color: var(--fg-muted); }
/* No margin-left:auto — the grid places this, and "push it as far right as it
   will go" is exactly the instruction that made the column ragged. */
.cls-result-score { font-family: var(--font-mono); font-weight: 700; font-size: var(--fs-14); }
/* Amber rather than red: this is a child who needs a minute, not a fault. */
/* Two figures, not one blended number: answering correctly and speaking clearly
   are different skills, and a student can be strong at one and struggling at the
   other. That distinction is the product. */
.cls-result-score { display: flex; gap: var(--sp-8); align-items: baseline; }
/* Fixed slot, right-aligned, tabular. These are two columns of numbers meant to
   be compared down the list, and left-aligned proportional figures of differing
   width made every row start its number in a different place — so the list read
   as prose that happens to contain digits. */
.cls-score-part {
  font-family: var(--font-mono); font-size: var(--fs-13); font-weight: 700;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  min-width: 6.5rem;
  text-align: right;
}
.cls-score-part.is-low { color: var(--c-gold-deep); }
/* One marker for "nothing here", wherever it appears — the figures columns, the
   tries count, the review count. It takes the figures' own size, font and right
   edge so an empty cell lines up with the numbers above and below it instead of
   being a third thing in a row of two. Quieter than a real number, so it never
   reads as a score of some kind.

   This replaced three different renderings of the same fact: "not started" in
   body type on the left of the row, "— content" in mono on the right, and a
   blank cell in the review column that could not be told from a student who had
   never submitted anything.

   The muted colour is what makes it quieter. It also sat at 70% opacity once,
   which took supporting text below the contrast --fg-muted is tuned to hold. */
.cls-empty {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  font-weight: 700;
  color: var(--fg-muted);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.is-figures .cls-empty { justify-self: stretch; }
/* Reserved whether or not a recording exists, so its arrival stops shifting the
   figures beside it. Sized to .icon-btn's teacher footprint. */
.cls-listen-slot { flex: none; width: 30px; display: grid; place-items: center; }
/* Only appears for a student who has a recording kept, which is a subset of a
   subset — consent granted, and audio actually captured. */
/* .cls-result-row is align-items: baseline, which drags a button's text
   baseline into line with the figures beside it and leaves the button itself
   sitting high. .cls-consent-wrap was corrected; these two were missed. */
.cls-listen { margin-left: var(--sp-8); flex: none; align-self: center; }

/* ── The results row on a phone ─────────────────────────────────────────────
   Every fixed slot above is there so the figures line up as a column read down
   the list — which is right, and is the reason the list is a table rather than
   a paragraph. It also gives the row a floor of about 500px: an 8rem name, two
   6.5rem score slots, the attempts button, the "to review" badge and a 30px
   listen slot. In a 355px card on a phone that does not fold, it overflows —
   the whole page scrolled sideways by 170px, and the column the slots exist to
   create could not be read anyway.

   So on a phone the row folds instead: the name takes its own line and the
   figures sit under it, still tabular and still right-aligned against each
   other. Same information, in the shape the width allows.

   Worth recording why this went unnoticed: the Classes surface cannot render
   without a signed-in teacher, so every previous mobile check silently measured
   the Library instead and reported it clean. It is now reachable from the dev
   bar, which is what made this visible at all. */
@media (max-width: 640px) {
  /* Four columns instead of five, with the name lifted onto its own line above
     them. The name is the only cell whose width is unbounded, so giving it the
     full row is what lets the four that follow keep fixed positions — which is
     the same column discipline as the desktop layout, folded rather than
     abandoned. */
  /* Fixed here too, and for the same reason: `auto` columns resolve per row, so
     a row missing its figures would pull its neighbours out of line exactly as
     it did on the desktop layout. The name lifts onto its own line, which is
     what buys the width for the four that follow. */
  .cls-result-row.is-figures {
    /* Same two headings, same rule: each column is as wide as the word above it
       needs, and the figures take what is left. Both cells shrank once they
       stopped spelling out their own units on every row. */
    grid-template-columns: 3.4rem 4.8rem minmax(0, 1fr) 26px;
    column-gap: var(--sp-8);
    row-gap: var(--sp-4);
  }
  .is-figures .cls-result-name { grid-column: 1 / -1; }
  .cls-score-part { min-width: 4rem; }
  /* The other two lists stay flex, so their name only needs its floor dropped
     to stop forcing a second line at this width. */
  .cls-result-row:not(.is-figures) .cls-result-name { min-width: 0; }
}

/* Consent reads at a glance down the column, so the one state that permits a
   recording is the one that looks different. Amber and grey are both "no". */
/* The wrapper carries the layout, the select carries the state colour.
   align-self because the row is baseline-aligned for its text, and a control is
   not text — baselined against a name it sits a couple of pixels low. */
.cls-consent-wrap { margin-left: auto; align-self: center; }
.select-wrap select.cls-consent.is-granted {
  color: var(--c-grass-deep);
  border-color: color-mix(in oklch, var(--c-grass) 45%, transparent);
}
/* Withdrawn is not "not asked yet". One is a parent who said no and a recording
   the database has already destroyed; the other is paperwork nobody has done.
   They rendered identically, so the column could not be read down at a glance —
   which is the entire reason it is a column. Amber, following .t-pill.is-off:
   both are "no", and this is the one that was decided rather than skipped. */
.select-wrap select.cls-consent.is-withdrawn {
  color: var(--c-gold-deep);
  border-color: color-mix(in oklch, var(--c-gold) 45%, transparent);
}

/* Collapsed by default and visually set apart. This is not a thing to stumble
   into while tidying a class list. */
.cls-danger {
  border: 1px solid color-mix(in oklch, var(--c-berry) 35%, transparent);
  border-radius: var(--r-sm);
  padding: var(--sp-8) var(--sp-16);
  background: color-mix(in oklch, var(--c-berry) 5%, transparent);
}
.cls-danger > summary {
  cursor: pointer;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-13);
  color: var(--c-alert);
}
.cls-danger .cls-hint { margin: var(--sp-8) 0 var(--sp-16); }
.cls-remove { margin-left: auto; align-self: center; }
.cls-remove + .cls-delete { margin-left: var(--sp-8); }
.cls-delete { margin-left: auto; align-self: center; color: var(--c-alert); }
.cls-add-hint { margin: 0 0 var(--sp-8); }
.cls-delete:hover { background: color-mix(in oklch, var(--c-berry) 12%, transparent); }

.modal-confirm-text { display: block; text-align: left; margin: var(--sp-16) 0 0; }
.modal-confirm-text .t-input { width: 100%; margin-top: var(--sp-4); }
.cls-add .t-input,
.cls-add .select-wrap { flex: 1 1 14rem; max-width: 20rem; }

/* The route in for a whole roster at once. Collapsed by default: pasting a
   list happens once per term, typing one name happens all year. */
.cls-bulk { margin: 0 0 var(--sp-16); }
.cls-bulk > summary {
  cursor: pointer;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-13);
  color: var(--fg-muted);
}
.cls-bulk > summary:hover { color: var(--fg); }
.cls-bulk-area {
  display: block;
  width: 100%;
  min-height: 92px;
  margin: var(--sp-8) 0;
  resize: vertical;
  font-size: var(--fs-14);
}
.cls-hint-last { margin-bottom: 0; }

/* ── Demo banner ─────────────────────────────────────────────────────────────
   Sits in the top bar where the mode switch would be, because the first thing
   someone needs to know on this page is that it is not their account. */
/* Owns the whole bar now that there is no wordmark beside it. The trailing
   control — Leave class for a student, Sign in for the demo — is pushed to the
   far end, where a way out is expected to be. */
.demo-bar {
  display: flex; align-items: center; gap: var(--sp-8); flex-wrap: wrap;
  flex: 1;
  margin: 0;
  font-size: var(--fs-13);
  color: var(--fg-muted);
}
.demo-bar > :last-child { margin-left: auto; }
.demo-tag {
  font-family: var(--font-mono);
  font-size: var(--fs-12); letter-spacing: 0.09em; text-transform: uppercase;
  font-weight: 700;
  padding: var(--sp-4) var(--sp-8);
  border-radius: var(--r-pill);
  color: var(--c-gold-deep);
  border: 1px solid color-mix(in oklch, var(--c-gold) 45%, transparent);
}
/* Scoped to bare links. `.demo-bar a` beat .btn-quiet on specificity, so the
   sign-in control kept this colour and weight after being made a button — the
   markup fix landed and the styling did not. */
.demo-bar a:not([class]) { color: var(--c-accent-ink); font-weight: 600; }
.demo-bar a { white-space: nowrap; }

/* Stays in the bar for the whole session. Red because this is the one thing on
   the screen a child should be able to notice without being told to look, and
   the dot pulses only where motion is welcome. */
.rec-chip {
  display: inline-flex; align-items: center; gap: var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--fs-12); letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700;
  color: var(--c-alert);
  background: color-mix(in oklch, var(--c-berry) 12%, transparent);
  border: 1px solid color-mix(in oklch, var(--c-berry) 40%, transparent);
  border-radius: var(--r-pill);
  padding: var(--sp-4) var(--sp-8);
  white-space: nowrap;
}
/* Static, and teal rather than berry. A pulsing red dot means one thing
   everywhere a person has ever seen one — capturing, now — and this chip is on
   screen for the whole session, including on the class page where the
   microphone is not open. Teal is what the rest of the product already uses for
   a disclosure (the recording notice's own icon), and a dot that holds still
   marks a standing fact instead of announcing an event. */
.rec-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--c-teal); flex: none;
}

/* .modal-icon fills its disc with berry, which is right for the confirmations
   it was built for — deleting a student's data, leaving a class. The recording
   notice is not a warning and nothing has gone wrong: it is the first screen a
   child meets on entering a class, and it wore the same 48px red circle as the
   erase-everything dialog before they had done anything. Teal is the brand's
   voice colour, which is what this notice is actually about. */
.modal-icon.rec-icon {
  color: var(--c-accent-ink);
  background: color-mix(in oklch, var(--c-teal) 16%, transparent);
}
.rec-list { text-align: left; }
/* On a phone the tag and the way out are what matter; the sentence is the first
   thing to go rather than wrapping the bar onto three lines.

   Scoped to the two bars that HAVE a droppable sentence. It used to apply to
   every .demo-bar, and the student's session bar is one — so on any screen
   under 640px it hid .rec-chip, which is the product's one permanent,
   session-long notice that a child is being recorded. A disclosure that a
   breakpoint removes is not a disclosure, and phones are where students are. */
@media (max-width: 640px) {
  #demo-bar > span:not(.demo-tag),
  #preview-bar > span:not(.demo-tag) { display: none; }
}

/* ── The gate ────────────────────────────────────────────────────────────────
   Two cards side by side, because the pair is the point: a teacher weighing
   this up needs to see that students get in without an account, an email
   address, or anything of theirs being collected. */
.gate { max-width: 62rem; margin: 0 auto; padding: var(--sp-48) var(--sp-16); }
/* Sized in the markup so it reserves its space before the SVG arrives — a mark
   that pops in and shoves the heading down is worse than no mark. */
.gate-mark { display: block; margin: 0 auto var(--sp-16); height: auto; }
.gate-title { font-size: var(--fs-32); margin: 0; text-align: center; }
.gate-sub { color: var(--fg-muted); text-align: center; margin: var(--sp-8) 0 var(--sp-32); }

.gate-cards { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-16); align-items: start; }
@media (max-width: 760px) { .gate-cards { grid-template-columns: 1fr; } }

.gate-card {
  background: var(--card);
  border: 1px solid var(--border);
  /* Same radius as .card, and as every other card: --r is one value on every
     surface now. */
  border-radius: var(--r);
  padding: var(--sp-24);
  box-shadow: var(--shadow-card);
}
/* The audience, and now the largest thing on the card. It used to be an eyebrow
   in 0.66rem mono above a 1.12rem feature line, which put the scanning weight on
   the mechanism — a visitor is deciding which of the two cards is theirs, not
   comparing sign-in methods. */
.gate-card-title {
  font-family: var(--font-head);
  font-size: var(--fs-24);
  line-height: 1.2;
  margin: 0 0 var(--sp-8);
}
.gate-card-text { color: var(--fg-muted); font-size: var(--fs-14); margin: 0 0 var(--sp-16); }

.gate-form { display: flex; gap: var(--sp-8); flex-wrap: wrap; }
.gate-form .t-input { flex: 1 1 12rem; min-width: 0; }
/* The field and the button beside it are the same control to a person, so they
   are the same height. They were not: .t-input is pinned to height 44px, the
   touch-target floor, while .btn takes its height from its own padding and
   lands near 51 — and a fixed cross-size is exactly what stops flex stretch
   from resolving it.

   Restoring height:auto lets the row stretch the field to whatever the button
   turns out to be, so the pair stays matched if .btn padding ever changes,
   rather than pinning a measured number that quietly goes stale. min-height
   still holds the 44px floor for the wrapped layout, where the field is alone
   on its line and has nothing to match. */
.gate-form .t-input { height: auto; align-self: stretch; }
/* Join and Start do the same job one step after the other, so they are the same
   size. Left to their own text they were not — "Join" is four characters and
   "Start" is five — so the button edge stepped sideways as a child moved from
   one row to the next. One floor holds the column still, and it is a floor
   rather than a width so the teacher's longer "Email me a link" still sets its
   own. */
.gate-form .btn { min-width: 6.5rem; }

/* The way back out of step two. It sat flush against the name form with nothing
   between them, so it read as a third control belonging to that row rather than
   as the escape from it. */
#join-back { margin-top: var(--sp-16); }
/* The confirmation channel, kept apart from the error channel. Step two of the
   join ("Room 12 — what's your name?") was rendered on .gate-note, the same
   class that turns red for "that code didn't work" — so a child who typed the
   code correctly was answered in the shape of a rejection. Same size and
   position; only the colour says which of the two happened. */
.gate-note { font-size: var(--fs-13); color: var(--fg-muted); margin: var(--sp-8) 0 0; }
.gate-note.is-err { color: var(--c-alert); }
.gate-note.is-ok { color: var(--c-accent-ink); font-weight: 600; }

/* The student card is deliberately quieter, not hidden: pretending the feature
   is finished would be worse, and removing it would misrepresent the product.
   Dimming the whole card would take the explanation down with it, so only the
   controls recede — the text explaining how students get in stays readable. */
.gate-card.is-soon { background: var(--bg-soft); box-shadow: none; }
.gate-card.is-soon .gate-form { opacity: 0.55; }
.gate-card .is-soon-note { color: var(--c-gold-deep); }

/* The code is copied off a whiteboard character by character, so the field
   shows it the same way it is written: spaced, upper case, unmistakable. */
.join-code-input {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-16);
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.join-names { display: flex; flex-wrap: wrap; gap: var(--sp-8); margin: var(--sp-8) 0; }
/* Names are tap targets for a nine-year-old on a trackpad, not list rows. */
/* "Did you mean Ana?" — the yes. It is the primary action of this step, and it
   sat next to "No, that's not me" looking less pressable than the refusal: a
   flat outline against a control with an underside. It gets the app's press
   affordance so the child's likeliest answer is also the obvious one. */
.join-name {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: var(--fs-14);
  padding: var(--sp-8) 0.75rem;
  min-height: 44px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  background: var(--card);
  color: var(--fg);
  cursor: pointer;
  box-shadow: 0 3px 0 var(--under-surface);
  transition: transform 0.12s var(--ease-out), box-shadow 0.12s var(--ease-out),
              border-color 0.16s var(--ease-out);
}
.join-name:active { transform: translateY(3px); box-shadow: 0 0 0 var(--under-surface); }
.join-name:focus-visible { box-shadow: 0 3px 0 var(--under-surface), 0 0 0 var(--ring-w) var(--ring); }
@media (prefers-reduced-motion: reduce) {
  .join-name { transition: none; }
  .join-name:active { transform: none; }
}
.join-name:hover { border-color: var(--c-teal); color: var(--c-accent-ink); }
.join-name:disabled { opacity: 0.5; cursor: default; }

.gate-demo { text-align: center; color: var(--fg-muted); font-size: var(--fs-14); margin: var(--sp-32) 0 0; }
.gate-demo a { color: var(--c-accent-ink); font-weight: 600; }

/* Sits under the demo line and ranks below it: the demo is something to do, the
   credit is something to read. Smaller and quieter so it reads as a signature
   rather than as a third choice on a screen whose only question is which of two
   audiences you are. */
.gate-credit {
  text-align: center;
  color: var(--fg-muted);
  font-size: var(--fs-13);
  margin: var(--sp-8) 0 0;
}
.gate-credit a { color: var(--c-accent-ink); font-weight: 600; }

.t-account { margin-left: auto; display: flex; align-items: center; gap: var(--sp-8); }
/* .btn-quiet takes its face from --bg-soft, which is also this header's
   background — the two were byte-identical, so the button face vanished and all
   that remained was its drop shadow, reading as a stray underline beneath the
   word. On a soft surface the raised card colour is what makes it a button. */
.t-account .btn-quiet { background: var(--card); border: 1px solid var(--border-strong); }

/* Who you are, and the door to everything about you. Same raised-card face as
   its neighbours, for the same reason as above. */
/* One shape for "who you are", both audiences. The teacher's sits in their
   header and the student's rides the topbar, so the ground differs below — but
   an avatar disc, a name and a pill is the same control doing the same job, and
   drawing the student's as bare text made it read as a caption. */
.acct-btn,
.student-who {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  font: inherit;
  background: var(--card);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  padding: var(--sp-4) 0.75rem var(--sp-4) var(--sp-4);
  cursor: pointer;
  transition: border-color 0.16s ease;
}
.acct-btn:hover,
.student-who:hover { border-color: var(--fg-muted); }
.acct-avatar,
.profile-avatar {
  display: grid;
  place-items: center;
  flex: none;
  border-radius: 50%;   /* a true circle — the one licensed rounding */
  background: var(--bg-soft);
  border: 1px solid var(--border);
  font-family: var(--font-head);
  font-weight: 650;
  color: var(--fg-muted);
}
.acct-avatar { width: 26px; height: 26px; font-size: var(--fs-13); }
.profile-avatar { width: 46px; height: 46px; font-size: var(--fs-16); }

/* ── Account modal ──────────────────────────────────────────────────────── */

.profile-body { text-align: left; }
.profile-head {
  display: flex;
  align-items: center;
  gap: var(--sp-16);
  margin-bottom: var(--sp-8);
}
.profile-head .modal-title { margin: 0; }
.profile-email {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--fg-muted);
  margin: 0;
  overflow-wrap: anywhere;
}
.profile-note { min-height: 1.2em; margin: var(--sp-8) 0 0; font-size: var(--fs-13); }
.profile-note.is-err { color: var(--c-alert); }
/* Library in, library out — routine actions, set apart from the fields. */
.profile-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
  margin: var(--sp-16) 0 0;
  padding-top: var(--sp-16);
  border-top: 1px solid var(--border);
}

/* Subtle on purpose: a tinted hairline and a label, not a red box. It has to
   read as "different neighbourhood" at a glance without shouting over the
   fields above — the typed confirmation is where the real friction lives. */
.profile-zone {
  margin-top: var(--sp-16);
  padding: var(--sp-8) var(--sp-16) var(--sp-16);
  border: 1px solid color-mix(in oklch, var(--c-berry) 28%, transparent);
  border-radius: var(--r-sm);
}
.profile-zone-label { color: var(--c-alert); display: block; margin-bottom: var(--sp-8); }
/* The student's version of the same grouping. Leaving a class is an ending and
   belongs apart from Done for exactly the teacher's reason — but it is not
   danger, and this product does not put red in front of a child. */
.profile-zone.is-quiet { border-color: var(--border); }
.profile-zone.is-quiet .profile-zone-label { color: var(--fg-muted); }
/* A key point the student has not earned the wording of yet. Muted and italic so
   it reads as "held back for now" rather than as a point with no text. */
.kp-held { color: var(--fg-muted); font-style: italic; }
.kp-held-note { margin: var(--sp-8) 0 0; font-size: var(--fs-13); color: var(--fg-muted); }

/* Which of the four figures the list is showing, said once above it, with what
   it means beside it. A column of percentages with an implicit definition is
   the thing a teacher rebuilds in a spreadsheet to be sure of. */
/* .mono-label is display:block with a bottom margin, which is right where it
   captions a field and wrong inside a flex row — the caption sat 0.3rem above
   the control it names. */
.cls-formula .mono-label { margin: 0; }
.cls-formula {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  flex-wrap: wrap;
  margin: 0 0 var(--sp-8);
}
.cls-formula-note { font-size: var(--fs-13); color: var(--fg-muted); }

/* The attempt count is the way in to the attempts. Styled as the text it was,
   with just enough affordance to say it opens. */
/* The door into the review flow — the thing this whole surface exists to reach —
   was a dotted text link with no height and a hover that only changed colour.
   .t-unit-toggle is the disclosure pattern this app already has: a real target,
   a hover fill, and a state that shows when it is open. It keeps the dotted
   underline, which is the one part that was doing useful work: it says the
   number is pressable without turning it into a button-shaped object in a row
   of figures. */
.cls-attempts-toggle {
  font: inherit;
  background: none;
  border: 0;
  border-radius: var(--r-btn);
  padding: var(--sp-4) var(--sp-8);
  min-height: 30px;
  cursor: pointer;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}
.cls-attempts-toggle:hover { color: var(--fg); background: color-mix(in oklch, var(--fg) 6%, transparent); }
.cls-attempts-toggle[aria-expanded="true"] {
  color: var(--fg);
  background: color-mix(in oklch, var(--fg) 8%, transparent);
  text-decoration: none;
}
.cls-attempts {
  padding: var(--sp-8) var(--sp-16);
  /* --track, not --bg-soft. The tray holds cards that sit on --card, so it has
     to read as the recess behind them; --bg-soft is the well used for inputs and
     sat too close to the cards it contains. */
  background: var(--track);
  border-radius: var(--r-sm);
  margin: 0 0 var(--sp-8);
}
.cls-attempt-q + .cls-attempt-q { margin-top: var(--sp-8); }
.cls-attempt-qtext {
  margin: 0 0 var(--sp-8);
  font-size: var(--fs-13);
  font-weight: 600;
  color: var(--fg);
}
/* Was a row of chips showing a percentage each. An attempt is now something a
   teacher reads and decides on, so each one is a block in a column — a
   judgement does not fit on a chip. */
.cls-attempt-seq {
  display: grid;
  gap: var(--sp-8);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Was the tightest padding in the product on the card that carries the most —
   evidence, a transcript, a suggestion and a form. --card-pad is what every
   neighbour uses. */
.cls-attempt-item {
  padding: var(--card-pad);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font-size: var(--fs-13);
  color: var(--fg);
}

/* ── Reviewing one attempt ─────────────────────────────────────────────── */

.rv-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-8);
  margin-bottom: var(--sp-8);
}
.rv-ord { font-family: var(--font-head); font-weight: 650; font-size: var(--fs-13); }
.rv-when { font-size: var(--fs-13); color: var(--fg-muted); font-variant-numeric: tabular-nums; }
/* State carries a word as well as a colour — "needs review" is the one thing on
   this card that must survive being printed, or read by someone colourblind. */
/* A status chip, so it follows .t-pill rather than the micro-label formula —
   it reports a state, it does not name a region.

   `border: 1px solid currentColor` was the shouting: on "Needs review" that is a
   full-strength alert outline around two words, on the card a teacher sees most
   often. .t-pill's answer is a border mixed to 40-45% of the fill, which reads
   as a chip rather than as a warning. */
.rv-state {
  margin-left: auto;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-12);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: var(--sp-4) var(--sp-8);
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  color: var(--fg-muted);
  background: var(--card);
}
.rv-state.is-open { color: var(--c-alert);      border-color: color-mix(in oklch, var(--c-berry) 45%, transparent); }
/* The dead fallback is gone with it: --c-grass-deep is declared in both themes,
   so var(--c-grass-deep, var(--c-grass)) could never reach its second argument —
   the only such fallback in the file, written defensively rather than decided. */
.rv-state.is-done { color: var(--c-grass-deep); border-color: color-mix(in oklch, var(--c-grass) 40%, transparent); }
.rv-state.is-redo { color: var(--c-gold-deep);  border-color: color-mix(in oklch, var(--c-gold) 45%, transparent); }
/* Redone leaves the gold family on purpose. Gold on this card means the attempt
   is waiting on somebody; a redo that has been answered is waiting on nobody,
   and reading it in the same colour as the request is how "Redo asked for" sat
   there looking unfinished after the child had finished it. */
.rv-state.is-redone { color: var(--c-accent-ink); border-color: color-mix(in oklch, var(--c-teal) 45%, transparent); }

.rv-label { display: block; margin: 0 0 var(--sp-4); }
/* The student's own words, set apart. This is the evidence, and it should not
   read as one more field on a form. */
.rv-said {
  margin: 0 0 var(--sp-8);
  padding-left: var(--sp-8);
  border-left: 2px solid var(--border-strong);
  color: var(--fg);
  font-style: italic;
}
/* The one browser-bulleted list in the whole product. Everything else that
   enumerates key points uses .kp-dot; a UA disc here is the tell that this card
   was built beside the system rather than inside it. */
.rv-missed {
  margin: 0 0 var(--sp-8); padding-left: var(--sp-16);
  color: var(--fg-muted); font-size: var(--fs-13);
  list-style: none;
}
.rv-missed li { position: relative; padding-left: var(--sp-16); }
.rv-missed li::before {
  content: "";
  position: absolute; left: 0; top: 0.5em;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--c-gold-deep);
}
.rv-aifb { margin: 0 0 var(--sp-8); color: var(--fg-muted); font-size: var(--fs-13); }

.rv-suggested {
  display: flex; align-items: baseline; gap: var(--sp-8); flex-wrap: wrap;
  margin: 0 0 var(--sp-8);
  padding-top: var(--sp-8);
  border-top: 1px solid var(--border);
}
.rv-suggested .rv-label { margin: 0; }
.rv-figure { font-family: var(--font-mono); font-weight: 700; font-size: var(--fs-16); font-variant-numeric: tabular-nums; }
.rv-verdict { font-size: var(--fs-13); color: var(--fg-muted); }

/* The machine's half of this card has a hairline above it; the teacher's had
   none, so the suggestion and the decision ran together as one block on the
   densest card in the product. */
.rv-form {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr);
  gap: var(--sp-8);
  margin-bottom: var(--sp-8);
  padding-top: var(--sp-8);
  border-top: 1px solid var(--border);
}
@media (max-width: 560px) { .rv-form { grid-template-columns: 1fr; } }
.rv-mark, .rv-fb { display: block; min-width: 0; }
.rv-mark-input, .rv-fb-input {
  width: 100%; font: inherit; font-size: var(--fs-13); color: var(--fg);
  background: var(--bg-soft); border: 1px solid var(--border);
  /* A field, so --r-sm like every other input. It borrowed --r-btn for a while
     because --r-sm was then 12px against 8px of inline padding and the text
     began inside the corner arc; on the shared scale both are 10px. And
     .rv-mark-input had no height floor while the textarea beside it did, so
     the pair sat at different heights. */
  border-radius: var(--r-sm); padding: var(--sp-4) var(--sp-8);
  min-height: 38px;
}
/* Mono and tabular, like every other figure on this card: the mark a teacher
   types should look like the suggestion it replaces. */
.rv-mark-input { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.rv-fb-input { resize: vertical; min-height: 38px; font-family: inherit; }
.rv-mark-input:focus-visible, .rv-fb-input:focus-visible {
  background: var(--bg); border-color: var(--c-teal); box-shadow: 0 0 0 var(--ring-w) var(--ring);
}

.rv-acts { display: flex; flex-wrap: wrap; gap: var(--sp-8); }
/* Undo sits apart from the three that move the review forward. */
.rv-undo { margin-left: auto; color: var(--fg-muted); }

/* A value that is shown rather than edited: the same box the account fields
   use, so the two panels read as one family, with nothing to type in. */
.profile-static {
  margin: 0;
  padding: var(--sp-8) 0.75rem;
  background: var(--bg-soft);
  border: 1.5px solid var(--border);
  border-radius: var(--r-sm);
  font-size: var(--fs-14);
  line-height: 1.45;
  color: var(--fg);
}
.profile-zone-actions { display: flex; flex-wrap: wrap; gap: var(--sp-8); }
.profile-danger { color: var(--c-alert); }
.profile-danger:hover { color: var(--c-alert); border-color: color-mix(in oklch, var(--c-berry) 45%, transparent); }

/* The emoji picker in the student panel. Chunky targets on the playful
   surface's pill radius; the chosen one wears the same sky selection the rest
   of the app uses, so "picked" reads the same everywhere. */
.sprofile-emoji {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
  margin-top: var(--sp-8);
}
.sprofile-emoji-btn {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  font-size: var(--fs-20);
  background: var(--bg-soft);
  border: 2px solid var(--border);
  border-radius: var(--r-btn);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.12s var(--ease-spring);
}
.sprofile-emoji-btn:hover { border-color: var(--border-strong); transform: scale(1.06); }
.sprofile-emoji-btn.is-on {
  border-color: var(--c-teal);
  background: color-mix(in oklch, var(--c-teal) 14%, transparent);
}
/* The initials cell: same slot, but text — it must read as "no emoji", not as
   a broken glyph. */
.sprofile-emoji-btn.is-letters {
  font-family: var(--font-head);
  font-weight: 650;
  font-size: var(--fs-13);
  color: var(--fg-muted);
}
.sprofile-emoji-note { font-size: var(--fs-13); color: var(--fg-muted); margin: var(--sp-8) 0 0; }
/* The avatar wearing an emoji drops the initials treatment. */
.profile-avatar.is-emoji,
.acct-avatar.is-emoji { border-color: color-mix(in oklch, var(--c-teal) 35%, transparent); }
.profile-avatar.is-emoji { font-size: var(--fs-24); }
/* The disc is 26px, so an emoji has to be sized to it rather than inheriting
   the initials' 0.62rem, which rendered the parrot as a speck. */
.acct-avatar.is-emoji { font-size: var(--fs-16); }
@media (prefers-reduced-motion: reduce) { .sprofile-emoji-btn { transition: none; } .sprofile-emoji-btn:hover { transform: none; } }

/* The one thing that differs from .acct-btn: the topbar's own ground is --card,
   so a --card chip on it would be invisible but for its border. --bg-soft is
   what .topbar-back already uses to sit on this same bar. */
.student-who { background: var(--bg-soft); }
.student-who .t-account-who { color: var(--fg); }
/* The bar carries four things and wraps by design, but the chip is wider than
   the bare text it replaced, so it tightens rather than pushing Leave class to
   a second row on every phone. Nothing is hidden — 056d786 is the standing
   lesson about what a breakpoint is allowed to delete from this bar. */
@media (max-width: 520px) {
  .student-who { gap: var(--sp-4); padding: var(--sp-4) var(--sp-8) var(--sp-4) var(--sp-4); }
  .student-who .acct-avatar { width: 21px; height: 21px; font-size: var(--fs-12); }
  .student-who .acct-avatar.is-emoji { font-size: var(--fs-13); }
  .student-who .t-account-who { font-size: var(--fs-13); max-width: 14ch; }
}
.t-account-who {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--fg-muted);
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* An email is the one string here that is genuinely someone else's and can be
   any length, so it truncates rather than pushing the sign-out button off.

   Scoped to the teacher's control, and it has to stay that way. The student's
   name now uses this class too, and it is a short roster label rather than an
   email — it is also the only thing on a shared classroom laptop that tells a
   child which session they are in. Dropping it at a breakpoint would repeat
   exactly what 056d786 fixed when a media query deleted the Recorded chip. */
@media (max-width: 520px) { .acct-btn .t-account-who { display: none; } }

.t-field-auth { margin: var(--sp-16) 0 0; text-align: left; }
.t-field-auth .t-input { width: 100%; margin-top: var(--sp-4); }
.auth-note { margin-top: var(--sp-8); }
.auth-note.is-err { color: var(--c-alert); }

.info-btn {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  flex: none;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 50%;   /* a true circle, so it reads as an icon not a control */
  color: var(--fg-muted);
  cursor: pointer;
  transition: color 0.16s ease, background 0.16s ease;
}
.info-btn svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.info-btn:hover { color: var(--fg); background: var(--bg-soft); }
.info-btn.is-on { color: var(--c-teal); background: color-mix(in oklch, var(--c-teal) 14%, transparent); }

/* A popover, not a row. This was a flex item with flex-basis:100%, so opening
   it inserted a line into the header: the header grew, the whole work surface
   below it jumped down, and closing snapped it back. Overlaying moves nothing.

   Anchored to the header and bounded by the header's own padding rather than
   hung off the 26px button — on a phone that button sits near the right edge,
   and a bubble anchored to it would run off screen. This cannot, at any width. */
.t-tip {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 1.1rem;
  right: 1.1rem;
  z-index: 20;
  max-width: 62ch;
  margin: 0;
  padding: var(--sp-8) var(--sp-16);
  color: var(--fg-muted);
  font-size: var(--fs-14);
  background: var(--card);
  border: 1px solid var(--border);
  border-left: 3px solid var(--c-teal);
  border-radius: var(--r);
  box-shadow: var(--shadow-card);
}
@media (prefers-reduced-motion: no-preference) {
  .t-tip { animation: info-in 0.2s var(--ease-out); }
  @keyframes info-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
  }
}

/* ── Help card as a disclosure ──────────────────────────────────────────── */

.t-help { padding: 0; interpolate-size: allow-keywords; }

.t-help-head {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  list-style: none;
  cursor: pointer;
  padding: 0.75rem var(--sp-16);
  margin: 0;
  user-select: none;
}
.t-help-head::-webkit-details-marker { display: none; }
.t-help-head svg {
  width: 13px; height: 13px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.5;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform 0.28s var(--ease-spring);
}
.t-help[open] .t-help-head svg { transform: rotate(90deg); }
.t-help-head:hover { color: var(--fg); }

/* Height animates rather than snapping. Browsers without ::details-content just
   open instantly, which is the behaviour it had before. */
.t-help::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size 0.3s var(--ease-out),
              content-visibility 0.3s allow-discrete;
}
.t-help[open]::details-content { block-size: auto; }

.t-help-body { padding: 0 var(--sp-16) var(--sp-16); }
.t-help-body p:last-child { margin-bottom: 0; }

@media (prefers-reduced-motion: reduce) {
  .t-help-head svg,
  .t-help::details-content { transition: none; }
}


/* ==========================================================================
   A student's class page
   --------------------------------------------------------------------------
   The shape every child already knows from Google Classroom, in SayAble's own
   palette: work that carries a state and a due date, and marks that stay
   findable afterwards. All colour from tokens, so both themes follow without a
   second set of rules.
   ========================================================================== */

/* oklab, not oklch, wherever an accent is mixed into the ground.

   oklch is polar, so a mix interpolates the HUE: gold at 85 degrees blended into
   the navy card at 259 travels the short way round and lands near 220, which is
   blue. Every gold chip on this page rendered on a teal ground until this was
   changed. oklab is the same perceptual space in rectangular coordinates, so a
   mix darkens and desaturates toward the ground without rotating.

   The mixes against `transparent` below are unaffected either way: there is no
   second hue for a polar space to interpolate toward. */

.cp-title {
  font-family: var(--font-head);
  font-size: clamp(1.5rem, 5vw, 2rem);
  line-height: 1.15;
  margin: 0 0 var(--sp-4);
}
.cp-sub { color: var(--fg-muted); margin: 0; font-size: var(--fs-16); }

/* --- Tabs --- */
.cp-tabs {
  display: flex; gap: var(--sp-4);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--sp-16);
}
.cp-tab {
  font: inherit; font-weight: 600;
  padding: var(--sp-8) var(--sp-16);
  color: var(--fg-muted);
  background: none; border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
}
.cp-tab:hover { color: var(--fg); }
.cp-tab.is-on { color: var(--fg); border-bottom-color: var(--c-teal); }
.cp-tab:focus-visible { box-shadow: 0 0 0 var(--ring-w) var(--ring); border-radius: var(--r-sm); }

/* --- The lists --- */
.cp-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-8); }

.cp-row {
  width: 100%;
  display: flex; align-items: center; gap: var(--sp-16);
  text-align: left;
  font: inherit; color: inherit;
  padding: var(--sp-16);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r);
  cursor: pointer;
  transition: border-color 0.16s var(--ease-out), transform 0.16s var(--ease-out);
}
.cp-row:hover { border-color: var(--c-teal); }
.cp-row:active { transform: translateY(1px); }
.cp-row:focus-visible { border-color: var(--c-teal); box-shadow: 0 0 0 var(--ring-w) var(--ring); }

.cp-row-emoji {
  flex: none;
  width: 2.4rem; height: 2.4rem;
  display: grid; place-items: center;
  color: var(--c-accent-ink);
  background: var(--bg-soft);
  border-radius: var(--r-sm);
}
.cp-row-emoji svg { width: 20px; height: 20px; }
/* min-width:0, or a long module name refuses to ellipsize inside the flex row. */
.cp-row-body { flex: 1; min-width: 0; display: grid; gap: 0; }
.cp-row-name {
  font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cp-row-meta { color: var(--fg-muted); font-size: var(--fs-14); }
/* Carried at the front of the meta line and given the foreground back, because
   it is the one thing on the row that changes what the child is being asked to
   do. Weight rather than colour: the row already spends its colour on the
   status chip, and two coloured things competing on one line is how neither
   gets read. */
.cp-row-mode { color: var(--fg); font-weight: 600; }

/* --- State --- */
.cp-chip {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-12); font-weight: 500;
  letter-spacing: 0.04em; text-transform: uppercase;
  padding: var(--sp-4) var(--sp-8);
  border-radius: var(--r-pill);
  background: var(--bg-soft);
  color: var(--fg-muted);
  border: 1px solid var(--border);
}
.cp-chip.is-marked {
  background: color-mix(in oklab, var(--c-grass) 20%, var(--card));
  color: var(--c-grass-deep);
  border-color: color-mix(in oklch, var(--c-grass) 45%, transparent);
}
/* "Redone" joins "Sent" rather than getting a seventh chip colour, because it
   means the same thing to a child: you answered it and it is back with your
   teacher. Gold is reserved for the states that still want something from them,
   which is exactly what a redo stops wanting once it has been done. */
.cp-chip.is-sent,
.cp-chip.is-redone {
  background: color-mix(in oklab, var(--c-teal) 18%, var(--card));
  color: var(--c-accent-ink);
  border-color: color-mix(in oklch, var(--c-teal) 45%, transparent);
}
.cp-chip.is-redo,
.cp-chip.is-late {
  background: color-mix(in oklab, var(--c-gold) 22%, var(--card));
  color: var(--c-gold-deep);
  border-color: color-mix(in oklch, var(--c-gold) 50%, transparent);
}

.cp-empty {
  margin: 0;
  padding: var(--sp-16);
  color: var(--fg-muted);
  background: var(--card);
  border: 1px dashed var(--border);
  border-radius: var(--r);
}

/* --- Marks --- */
.cp-mark {
  padding: var(--sp-16);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.cp-mark-q { margin: 0 0 var(--sp-8); font-weight: 600; }
.cp-mark-row { display: flex; align-items: center; gap: var(--sp-8); flex-wrap: wrap; }
.cp-mark-figure { font-family: var(--font-head); font-size: var(--fs-18); }
.cp-mark-when { color: var(--fg-muted); font-size: var(--fs-13); }
.cp-mark-fb {
  margin: var(--sp-8) 0 0;
  padding-left: var(--sp-8);
  border-left: 3px solid var(--c-teal);
  color: var(--fg);
}
.cp-mark-by { margin: var(--sp-4) 0 0 var(--sp-8); color: var(--fg-muted); font-size: var(--fs-13); }

/* ==========================================================================
   One piece of work — where a teacher's mark finally reaches the student
   ========================================================================== */

.cw-head { margin-bottom: var(--sp-16); }
/* Scoped to this one eyebrow rather than added to the shared .mono-label rule,
   which every form-field caption in the teacher surface also uses. Size is
   deliberately unchanged — the module name below is still the subject, and the
   fix here is that an alert state should not be the same muted grey as a
   caption. */
#cw-eyebrow.is-redo,
#cw-eyebrow.is-late { color: var(--c-gold-deep); font-weight: 600; }
#cw-eyebrow.is-marked { color: var(--c-grass-deep); font-weight: 600; }
.cw-title {
  font-family: var(--font-head);
  font-size: clamp(1.35rem, 4.5vw, 1.75rem);
  line-height: 1.2;
  margin: 0 0 var(--sp-4);
}
.cw-sub { color: var(--fg-muted); margin: 0; font-size: var(--fs-14); }

/* The kind of work, and what that means, on the screen where the child decides
   to press Start. A panel rather than another grey subtitle line: this is the
   one sentence here that changes what they should do, and under .cw-sub in the
   same muted grey it would read as more housekeeping. */
.cw-mode {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-8);
  margin: var(--sp-8) 0 0;
  padding: var(--sp-8) var(--sp-16);
  border-radius: var(--r);
  border: 1px solid var(--border);
  background: var(--bg-soft);
  font-size: var(--fs-14);
  color: var(--fg-muted);
  line-height: 1.5;
}
.cw-mode-tag {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-12);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: var(--sp-4) var(--sp-8);
  border-radius: var(--r-pill);
  border: 1px solid transparent;
}
/* Teal for the one that counts, grass for the one that does not. The pairing is
   the product's existing vocabulary — teal is what a teacher's own mark wears,
   grass is what "you're done, nothing is riding on this" wears everywhere else —
   so a child does not have to learn a second colour language for this panel. */
.cw-mode.is-assess {
  border-color: color-mix(in oklch, var(--c-teal) 35%, transparent);
  background: color-mix(in oklab, var(--c-teal) 7%, var(--card));
}
.cw-mode.is-assess .cw-mode-tag {
  color: var(--c-accent-ink);
  border-color: color-mix(in oklch, var(--c-teal) 45%, transparent);
}
.cw-mode.is-practice {
  border-color: color-mix(in oklch, var(--c-grass) 35%, transparent);
  background: color-mix(in oklab, var(--c-grass) 7%, var(--card));
}
.cw-mode.is-practice .cw-mode-tag {
  color: var(--c-grass-deep);
  border-color: color-mix(in oklch, var(--c-grass) 45%, transparent);
}
.cw-mode.is-check {
  border-color: color-mix(in oklch, var(--c-gold) 40%, transparent);
  background: color-mix(in oklab, var(--c-gold) 9%, var(--card));
}
.cw-mode.is-check .cw-mode-tag {
  color: var(--c-gold-deep);
  border-color: color-mix(in oklch, var(--c-gold) 50%, transparent);
}

.cw-actions { margin-bottom: var(--sp-16); }
.cw-closed { margin: 0; color: var(--fg-muted); font-size: var(--fs-14); }

.cw-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-8); }
.cw-item {
  padding: var(--sp-16);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
.cw-item-head { display: flex; gap: var(--sp-8); align-items: baseline; }
.cw-num {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  color: var(--fg-muted);
}
.cw-q { margin: 0; font-weight: 600; }

.cw-label {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-12); font-weight: 500;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--fg-muted);
}
.cw-said {
  margin: var(--sp-4) 0 0;
  padding-left: var(--sp-8);
  border-left: 3px solid var(--border-strong);
  color: var(--fg-muted);
}
.cw-none,
.cw-pending { margin: var(--sp-8) 0 0; color: var(--fg-muted); font-size: var(--fs-14); }

.cw-mark {
  margin-top: var(--sp-8);
  padding: var(--sp-8) var(--sp-16);
  border-radius: var(--r-sm);
  background: color-mix(in oklab, var(--c-teal) 12%, var(--card));
  border: 1px solid color-mix(in oklch, var(--c-teal) 38%, transparent);
}
/* A machine's figure is deliberately quieter than a teacher's, and carries no
   brand colour: it is provisional, and it must not look like the verdict. */
.cw-mark.is-machine {
  background: var(--bg-soft);
  border-color: var(--border);
}
.cw-mark.is-redo {
  background: color-mix(in oklab, var(--c-gold) 16%, var(--card));
  border-color: color-mix(in oklch, var(--c-gold) 45%, transparent);
}
.cw-mark-row { display: flex; align-items: baseline; gap: var(--sp-8); flex-wrap: wrap; }
.cw-figure { font-family: var(--font-head); font-size: var(--fs-18); }
.cw-fb { margin: var(--sp-8) 0 0; }
.cw-redo { margin: var(--sp-8) 0 0; font-weight: 600; color: var(--c-gold-deep); }
/* Same sentence position, settled colour. Gold asks; this one reports. */
.cw-redone { margin: var(--sp-8) 0 0; font-weight: 600; color: var(--c-accent-ink); }

/* Narrow: the status chip stops competing with the name for the same row, and
   lines up under the body rather than under the emoji tile. Both offsets are
   the tile's 2.4rem plus the row's gap, so they follow the gap if it moves. */
@media (max-width: 420px) {
  .cp-row { flex-wrap: wrap; }
  .cp-row-body { flex-basis: calc(100% - 2.4rem - var(--sp-16)); }
  .cp-chip { margin-left: calc(2.4rem + var(--sp-16)); }
}

@media (prefers-reduced-motion: reduce) {
  .cp-row { transition: none; }
  .cp-row:active { transform: none; }
}

/* ==========================================================================
   Class page layout — banner, rail, main column
   --------------------------------------------------------------------------
   Classroom's shape: a class banner that carries identity and nothing
   actionable, a narrow rail holding only what wants doing, and the full list
   beside it. The rail is first in the DOM, so the single-column collapse puts
   what needs you above the list for free, with no ordering property to keep in
   sync with the breakpoint.
   ========================================================================== */

/* The hero and the class page share one .shell, and they want different
   widths — one narrow column against two. */
#p-screen-start.is-class { max-width: 1000px; }

.cp-banner {
  padding: var(--sp-24);
  margin-bottom: var(--sp-16);
  border-radius: var(--r);
  /* Toward the accent rather than at it: a full-strength teal panel behind
     white-ish text is the contrast failure the mic button already had to be
     rescued from. Flat, not the diagonal teal-to-card sweep it used to be: a
     decorative gradient is on the Work Plan's Avoid list, and one even tint is
     enough to say "this is your class". */
  background: color-mix(in oklab, var(--c-teal) 10%, var(--card));
  border: 1px solid color-mix(in oklab, var(--c-teal) 30%, var(--border));
}
.cp-banner-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-12); font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0;
}

.cp-grid { display: grid; gap: var(--sp-16); align-items: start; }
@media (min-width: 860px) {
  .cp-grid { grid-template-columns: 260px minmax(0, 1fr); }
  /* Sticky only where there is room to scroll past it. On a phone the rail is
     part of the flow and pinning it would eat the screen. */
  .cp-rail { position: sticky; top: 1rem; }
}

.cp-rail { display: grid; gap: var(--sp-16); }
.cp-panel {
  padding: var(--sp-16);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r);
}
/* The redo panel is the one thing on the page that asks the child to act, so
   it is the one thing allowed the warning colour. */
#cp-redo {
  background: color-mix(in oklab, var(--c-gold) 20%, var(--card));
  border-color: color-mix(in oklab, var(--c-gold) 55%, transparent);
}
/* The heading carries the colour rather than the panel drowning in it — the
   banner is already teal, and a second saturated block beside it would leave
   the page with two things shouting and no hierarchy between them. */
#cp-redo .cp-panel-head { color: var(--c-gold-deep); }
.cp-panel-head {
  font-family: var(--font-head);
  font-size: var(--fs-16);
  margin: 0 0 var(--sp-8);
}
.cp-panel-text { margin: 0 0 var(--sp-8); color: var(--fg-muted); font-size: var(--fs-14); }
.cp-panel-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-8); }

.cp-rail-link {
  width: 100%;
  display: grid; gap: 0;
  text-align: left;
  font: inherit; color: inherit;
  padding: var(--sp-8);
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.cp-rail-link:hover { background: var(--bg-soft); }
.cp-rail-link:focus-visible { box-shadow: 0 0 0 var(--ring-w) var(--ring); }
.cp-rail-name {
  font-weight: 600; font-size: var(--fs-14);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cp-rail-meta { font-size: var(--fs-13); color: var(--fg-muted); }
.cp-rail-meta.is-urgent { color: var(--c-gold-deep); font-weight: 600; }

.cp-rail-quiet {
  margin: 0;
  padding: var(--sp-16);
  font-size: var(--fs-14);
  color: var(--fg-muted);
  border: 1px dashed var(--border);
  border-radius: var(--r);
}

.cp-main { min-width: 0; }

/* --- Marks, grouped by the piece of work they belong to --- */

.cp-group + .cp-group { margin-top: var(--sp-16); }
.cp-group-head {
  display: flex; align-items: center; gap: var(--sp-8);
  padding: var(--sp-8) 0;
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--sp-8);
}
.cp-group-emoji {
  flex: none;
  width: 2rem; height: 2rem;
  display: grid; place-items: center;
  color: var(--c-accent-ink);
  background: var(--bg-soft);
  border-radius: var(--r-sm);
}
.cp-group-emoji svg { width: 18px; height: 18px; }
.cp-group-id { flex: 1; min-width: 0; }
.cp-group-name {
  font-family: var(--font-head);
  font-size: var(--fs-16);
  margin: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cp-group-meta { margin: 0; font-size: var(--fs-13); color: var(--fg-muted); }
.cp-group-figure {
  flex: none;
  font-family: var(--font-head);
  font-size: var(--fs-18);
}
.cp-group-list { gap: var(--sp-8); }

/* ── Appearance switcher ──────────────────────────────────────────────────
   A segmented group rather than a single toggle, because there are three
   answers and "Match device" is not the opposite of anything. */
.theme-choice { display: flex; gap: var(--sp-4); flex-wrap: wrap; }
.theme-btn {
  font: inherit;
  font-size: var(--fs-14);
  font-weight: 600;
  padding: var(--sp-8) 0.75rem;
  min-height: 40px;
  color: var(--fg);
  background: var(--bg-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-btn);
  cursor: pointer;
  transition: border-color 0.16s var(--ease-out), background 0.16s var(--ease-out);
}
.theme-btn:hover { border-color: var(--border-strong); }
.theme-btn:focus-visible { box-shadow: 0 0 0 var(--ring-w) var(--ring); }
/* Navy ink on the teal fill, the same measured pairing the mic and the primary
   button take — white on this fill is 2.56:1. */
.theme-btn.is-on {
  background: var(--c-teal);
  border-color: var(--c-teal);
  color: var(--ink-on-bright);
}
.theme-note { margin: var(--sp-8) 0 0; font-size: var(--fs-13); color: var(--fg-muted); }
@media (prefers-reduced-motion: reduce) { .theme-btn { transition: none; } }


/* ==========================================================================
   Parts added with the class switcher and the topbar rework
   ========================================================================== */

/* The class name in the topbar, which is now the student's way back to their
   class page. It keeps .demo-tag's look — mono, uppercase, tracked — because it
   is still the "you are in this class" marker; what it gains is a chevron and
   the behaviour of a control, since it replaced the button that used to sit
   here. A bare SVG with no width renders at its own intrinsic size, which is
   how this first shipped: a 24px-viewBox chevron filling the chip. */
.student-home {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  cursor: pointer;
  background: none;
  max-width: 42vw;
  transition: border-color 0.16s var(--ease-out), color 0.16s var(--ease-out);
}
.student-home > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.student-home-chev {
  width: 11px;
  height: 11px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.student-home:hover { color: var(--fg); border-color: currentColor; }
.student-home:focus-visible { box-shadow: 0 0 0 var(--ring-w) var(--ring); }
@media (prefers-reduced-motion: reduce) { .student-home { transition: none; } }

/* --- The class switcher, under the banner heading --- */

.cp-classes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-top: var(--sp-16);
}
.cp-class-pill,
.cp-class-add {
  font: inherit;
  font-size: var(--fs-13);
  font-weight: 600;
  min-height: 36px;
  padding: var(--sp-4) 0.75rem;
  border-radius: var(--r-pill);
  cursor: pointer;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cp-class-pill {
  color: var(--fg);
  background: var(--card);
  border: 1px solid var(--border-strong);
}
.cp-class-pill:hover { border-color: var(--c-teal); }
/* The class you are already looking at. Navy ink on the teal fill, the measured
   pairing — white on this fill is 2.56:1. */
.cp-class-pill.is-on {
  background: var(--c-teal);
  border-color: var(--c-teal);
  color: var(--ink-on-bright);
  cursor: default;
}
/* Dashed, because it makes something that is not there yet — the same language
   the teacher's "+ New class" and "+ Module" tiles use. */
.cp-class-add {
  color: var(--fg-muted);
  background: none;
  border: 1px dashed var(--border-strong);
}
.cp-class-add:hover { color: var(--fg); border-color: var(--fg-muted); }
.cp-class-pill:focus-visible,
.cp-class-add:focus-visible { box-shadow: 0 0 0 var(--ring-w) var(--ring); }

/* --- Joining a second class --- */

.addclass-form { margin-top: var(--sp-16); }

/* --- The answer result, with the donut gone --- */

/* The figure, beside the verdict word instead of inside a 104px ring. Mono and
   tabular so it reads as a measurement, and sized as one fact among several
   rather than as the headline — which was the whole objection to the donut. */
.verdict-figure {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-16);
  font-variant-numeric: tabular-nums;
  color: var(--fg-muted);
  flex: none;
}
/* The unit rides at the text floor and full --fg-muted. It was 0.72em at 75%
   opacity, roughly 12px of grey on grey. */
.verdict-figure::after {
  content: "%";
  font-size: var(--fs-13);
  margin-left: 0.05em;
}

/* The three stat tiles, now one line. Demoted deliberately: the counts are
   still true and occasionally useful, and none of them is something a child
   does anything with. */
.p-readout-line {
  margin: var(--sp-8) 0 0;
  font-size: var(--fs-13);
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 520px) {
  .student-home { max-width: 38vw; }
}
