/* ── Roster / access links ─────────────────────────────────────────────── */
.status-pill { display: inline-block; font-size: var(--text-xs); font-weight: 700; padding: 3px 10px; border-radius: 12px; margin-left: 8px; vertical-align: middle; }
.status-pill.status-not_started { background: var(--sand); color: var(--text-secondary); }
.status-pill.status-in_progress { background: #FFF3CD; color: var(--state-warn-text); }
.status-pill.status-completed { background: var(--correct-bg); color: var(--correct-text); }
.roster-link { color: var(--text-secondary); font-size: 13px; word-break: break-all; }

/* ── Editor ────────────────────────────────────────────────────────────── */
.field-label { display: block; font-weight: 700; font-size: 13px; margin: 20px 0 8px; color: var(--warm-tan); text-transform: uppercase; letter-spacing: 0.04em; }
/* A label sitting DIRECTLY inside a modal card is a flex child, and flex gaps
   do not collapse with margins — so the card's 16px gap stacked on top of the
   8px above and every label floated 24px from its own input. A field's label
   belongs closer to its input than the next field does, or the form reads as a
   list of unrelated boxes.
   New test was the only modal that looked right, because it wraps each
   label+input in a div: the margin then applies to the input directly and the
   gap falls between the groups. This gets the same 8px for the other nineteen
   without rewriting all their markup — and the > means the wrapped ones, which
   are already correct, are left alone. */
.login-card > .field-label { margin-bottom: -8px; }
.editor-position { color: var(--text-secondary); font-size: 13px; margin-bottom: 12px; }
.hint-text { color: var(--text-muted); font-size: 12px; margin-top: 6px; }
/* Instructions/End message sit side by side, but their hint text is a
   different length on each side — without a shared reserved height, the
   two rich-text boxes below start at different Y positions and look
   misaligned even though the columns are the same width. */
.field-hint-fixed { min-height: 32px; }
@media (max-width: 640px) { .field-hint-fixed { min-height: 0; } }

/* A genuinely useful reference (what combinations of toggles actually show
   a student), not just a muted aside — styled to stand apart from the
   surrounding hint-text so it reads as something worth reading once and
   remembering, not skimming past. */
.info-box {
  background: var(--sand); border: 1px solid var(--border-card); border-radius: var(--radius-md);
  padding: 14px 16px; margin-top: 14px; font-size: 13px; color: var(--brown-ink); line-height: 1.6;
}
/* THE TITLE ONLY. As `.info-box strong` this hit every bold word anywhere
   inside the box — so a list item reading "<strong>On the day</strong> — no
   score for that paper" broke after the bold and put the dash on a line of its
   own, and a sentence with an emphasised phrase in the middle tore in half.
   The rule exists to make the box's own heading a block; a direct child is
   what a heading is. */
.info-box > strong { display: block; margin-bottom: 6px; color: var(--text-primary); }
.info-box ul { margin: 4px 0 8px 20px; }
.info-box li { margin-bottom: 3px; }
/* The label is flex ONLY to hold the box beside the first line of text, so it
   must contain exactly two children: the input and one <span> wrapping all the
   words. Loose text is not one flex item — every text run and every <strong>
   inside becomes its own item, and the 8px gap then lands between each of them.
   That is what put the wide spaces around "practice" and "Horizon programme".
   A stray gap is the visible half; the invisible half is that flex items do not
   break across lines, so the sentence stops wrapping properly when narrow. */
.checkbox-label { display: flex; align-items: flex-start; gap: 8px; font-size: 14px; color: var(--text-primary); cursor: pointer; }
.checkbox-label input { margin-top: 3px; }
/* A checkbox that depends on another one — greyed while its condition is
   unmet, with the reason on hover, rather than clickable and then refused on
   save. Currently: "let them review each question" needs the score on. */
.checkbox-label-disabled { color: var(--text-muted); cursor: not-allowed; }

/* "reopened 3 Aug, 4:12 pm by Caryll" on a results row. Emphasised, because a
   sitting that ran twice is the one thing on that line a reader must not skim
   past — but not a warning colour: granting extra time is a legitimate act,
   not an incident. */
.reopen-note { font-weight: 700; color: var(--text-primary); }
.save-success { color: var(--correct-text); font-size: 13px; font-weight: 600; }

/* WHICH OF TWO STATES THIS QUESTION IS IN, beside the button that changes it.
   Not a toast: it does not appear and vanish, it just says what is true. The
   old "Saved." was small, green, under the question navigator — the far side
   of the screen from Save — and took itself away after two and a half seconds,
   so the only way to find out whether a save had landed was to press Save
   again (John, 22 Aug). */
.save-state {
  align-self: center; margin-right: 10px; white-space: nowrap;
  font-size: 13px; font-weight: 600; color: var(--text-secondary);
}
/* Sand rather than red. Unsaved work is the NORMAL state of a question being
   written — it is a fact to notice on the way past, not a fault to answer. */
.save-state.is-unsaved {
  color: var(--brown-ink);
  background: var(--sand); border-radius: 999px; padding: 3px 11px;
}
.save-state.is-saved { color: var(--correct-text); }

/* The button says it landed, where the pointer already is, and cannot be
   pressed while it does — which is the half that stops the second press. */
.btn-primary.is-saved {
  background: var(--correct-text); border-color: var(--correct-text);
  color: var(--warm-white); opacity: 1;
}
.category-badge {
  display: inline-block; font-size: var(--text-xs); font-weight: 600; color: var(--warm-tan);
  background: var(--sand); border-radius: 4px; padding: 2px 8px; margin-top: 4px;
}
/* "Needs your attention" variant — unmarked writing can't hide in a neutral
   badge color. */
.category-badge.badge-attention { background: #FFF3CD; color: var(--state-warn-text); }
/* Done just now — the green the finished/completed chips already use, so
   "set" reads the same here as it does everywhere else. */
.category-badge.badge-ok { background: var(--correct-bg); color: var(--correct-text); }

/* The key to the words on a result. Closed until asked for, and sitting
   directly under the chips it explains — at the top it would be read before it
   was needed and push the questions down every visit; at the bottom nobody
   reads it, which is exactly what happened to the paragraph this replaces. */
.review-glossary {
  margin: 0 0 12px; padding: 12px 14px;
  background: var(--parchment); border: 1px solid var(--border-card);
  border-radius: var(--radius-sm); font-size: 13px; line-height: 1.6;
}
.review-glossary dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; }
.review-glossary dt { font-weight: 700; color: var(--deep-ink); white-space: nowrap; }
.review-glossary dd { margin: 0; color: var(--text-secondary); }

/* Marks by skill, moved here from the screen that has gone.
   The 18px is not decoration: .review-section has no padding of its own, so
   every direct child sets its own gutter — the head, the question header and
   each question row all say 18px. This block said 0, so it alone sat flush
   against the card edge while everything above and below it lined up. */
.review-skills { padding: 10px 18px 4px; }
.review-skills-head {
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-muted); margin-bottom: 6px;
}
.review-skill-row { display: flex; align-items: center; gap: 12px; font-size: 13px; margin: 3px 0; }
.review-skill-name { min-width: 150px; color: var(--text-primary); }
.review-skill-bar {
  flex: 1; height: 8px; background: var(--sand); border-radius: 50px; overflow: hidden; max-width: 320px;
}
.review-skill-bar i { display: block; height: 100%; background: var(--deep-ink); }
.review-skill-num { color: var(--text-secondary); white-space: nowrap; }

@media (max-width: 700px) {
  .review-glossary dl { grid-template-columns: 1fr; }
  .review-glossary dt { margin-top: 6px; }
}

/* Missing-image warning on the Tests view — a broken diagram in a live
   sitting is a student-facing failure, so it gets a real banner. */
.storage-warning {
  background: #FFF3CD; border: 1px solid #E0B84D; border-radius: var(--radius-md);
  padding: 12px 16px; margin-bottom: 16px; font-size: 13px; color: var(--state-warn-text);
}
.storage-warning .muted-note { color: var(--text-muted); font-size: 12px; margin-top: 4px; }

/* ── Rich text editor (instructions / end message) ────────────────────────── */
.rte { border: 1.5px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; background: var(--warm-white); }
.rte-toolbar { display: flex; flex-wrap: wrap; gap: 2px; padding: 6px; background: var(--parchment); border-bottom: 1px solid var(--border); }
.rte-toolbar button {
  border: 1px solid transparent; background: none; border-radius: 4px; min-width: 28px; height: 28px;
  font-size: 13px; color: var(--text-primary); font-family: inherit;
}
.rte-toolbar button:hover { background: var(--sand); }
.rte-toolbar button.active { background: var(--sand); border-color: var(--border-card); }
.rte-toolbar .rte-sep { width: 1px; background: var(--border); margin: 4px 4px; }
.rte-editor {
  min-height: 220px; max-height: 420px; overflow-y: auto; padding: 12px 14px;
  font-size: 14px; line-height: 1.6; color: var(--text-primary); outline: none;
  /* Exam font, not the brand font: stems, options and passages are all
     rendered to the student inside the exam chrome, so authoring them in
     the brand body face meant the editor and preview never quite matched the
     real thing (line lengths and digit/fraction shapes especially). The two
     exceptions below render on the branded picker instead, and keep
     --font-body. */
  font-family: var(--exam-font);
}
/* The end message appears on the Warm Parchment picker screen, NOT in the
   exam chrome — so it stays on brand wherever it's edited. The new-test
   modal's instructions field is set-level and lands on the picker too; the
   settings page's instructions field can go either way, so its font is set
   with the surface in .startpage-frame below rather than pinned here. */
#new-test-instructions-editor, #new-test-end-message-editor,
#set-end-message-editor { font-family: var(--font-body); }
.rte-editor:empty::before { content: attr(data-placeholder); color: var(--text-muted); }
.rte-editor h2 { font-size: 19px; margin: 8px 0 4px; }
.rte-editor h3 { font-size: var(--text-xl); margin: 8px 0 4px; }
.rte-editor p { margin: 0 0 8px; }
.rte-editor ul, .rte-editor ol { margin: 0 0 8px 20px; }
.rte-editor img { max-width: 100%; border-radius: 6px; margin: 6px 0; }
/* THE ROW, WHILE IT IS BEING WRITTEN. Without this the editor stacked what the
   student would see side by side, so the layout could only be checked in the
   preview — and a row assembled by hand looked like it had failed. */
.rte-editor .fig-row {
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: 18px; margin: 12px 0;
}
.rte-editor .fig-row figure {
  margin: 0; flex: 1 1 0; min-width: 100px;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
}
/* The same picture band as the shared sheet, for the same reason: the row is
   being judged here, and a row whose captions line up in the preview and not
   in the editor is a layout you cannot trust while writing it. */
.rte-editor .fig-row figure img {
  margin: 0; width: 100%; height: 132px;
  object-fit: contain; object-position: center bottom;
}
/* The caption is the one part of a row that IS typed, so it says so when it is
   empty rather than looking like a gap where a picture failed to load. */
.rte-editor .fig-row figcaption { text-align: center; min-width: 60px; }
.rte-editor .fig-row figcaption:empty::before {
  content: 'Caption'; color: var(--text-muted);
}
/* Generous by default (TestPortal-style): the stem is the main writing
   surface on this page, so it gets real room instead of a letterbox. */
#stem-editor-rte { min-height: 200px; max-height: 45vh; }
.option-rte-editor { min-height: 64px; max-height: 220px; }

/* No background here on purpose: this base rule is what the Preview pane
   uses too (via the shared content-render.js), and Preview is meant to show
   exactly what the student sees — real exam maths doesn't get a highlight
   box, so neither should this. The subtle tint below is scoped to the *live*
   contenteditable only, as a hint that the chip is a single non-editable
   unit rather than plain text you can click into and edit character by
   character. */
.katex-chip { display: inline-block; vertical-align: middle; user-select: none; }
.rte-editor .katex-chip {
  padding: 1px 5px; border-radius: 4px; cursor: default; outline-offset: 2px;
  background: rgba(122, 90, 48, 0.06);
}
.rte-editor .katex-chip:hover { outline: 1.5px dashed var(--warm-tan); }

/* ── Passages panel + per-type editors ─────────────────────────────────── */
.passages-panel { background: var(--warm-white); border: 1px solid var(--border-card); border-radius: var(--radius-lg); padding: 16px 20px; }
/* Collapsed to just its header row when the section has no passages (Maths /
   Thinking Skills / Writing never use them) — the "+ New passage" button
   stays visible, so Reading authoring is one click away from expanding it. */
.passages-panel.collapsed { padding: 8px 20px; }
.passages-panel.collapsed .hint-text, .passages-panel.collapsed .card-list { display: none; }
.passages-panel-head { display: flex; align-items: center; justify-content: space-between; }
.passages-panel-head h3 { font-size: var(--text-xl); }
.passage-tab-card { margin-top: 12px; }
.passage-tab-card .rte-editor { min-height: 120px; max-height: 280px; }
.tab-label-input { margin-top: 4px; }
.gap-correct-label { display: block; margin-top: 10px; font-size: 13px; font-weight: 600; }
#gaps-editor .option-card textarea, #choices-input, #pool-input { margin-top: 6px; }

/* Gap chips — same inline non-editable mechanism as katex chips, but these
   SHOULD look like a placeholder box in both the editor and the preview. */
.cloze-gap-chip, .preview-gap {
  display: inline-block; min-width: 34px; padding: 1px 8px; margin: 0 2px;
  border: 1.5px dashed var(--warm-tan); border-radius: 4px; background: var(--sand);
  font-size: 12px; font-weight: 700; color: var(--brown-ink); text-align: center;
  vertical-align: middle; user-select: none;
}
.preview-gap { border-style: solid; background: #F3F8F3; border-color: var(--correct-border); color: var(--correct-text); }

/* ── Category filter chips ─────────────────────────────────────────────── */
.category-filter-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
/* Search and category are both "narrow it down by a value" — same row. The
   status chips are a different act (pick a state, see the counts) and get
   their own line. */
/* flex-end, not center: Sort is a two-row control (its label sits above the
   select) while the search box and the chips are one row. Centring a taller
   item pushes its select BELOW everything beside it, which is what made Sort
   look dropped out of the row. Aligning the bottoms puts the select on the same
   line as the search box and leaves its label in the space above. */
.test-filter-bar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin-bottom: 18px; }
.test-filter-bar .category-filter-chips { margin-top: 0; }
.test-filter-bar > .text-input { max-width: 320px; flex: 1 1 220px; }
.test-filter-bar #test-status-chips { flex-basis: 100%; }
.filter-chip {
  background: var(--warm-white); border: 1.5px solid var(--border-card); border-radius: var(--radius-pill);
  padding: 6px 14px; font-size: 13px; font-weight: 600; color: var(--text-secondary);
}
.filter-chip:hover { border-color: var(--brown-ink); color: var(--brown-ink); }
.filter-chip.active { background: var(--brown-ink); border-color: var(--brown-ink); color: var(--warm-white); }

/* ── Item analysis + cohort results ────────────────────────────────────── */
.item-analysis-panel { background: var(--warm-white); border: 1px solid var(--border-card); border-radius: var(--radius-lg); padding: 16px 20px; }
.item-analysis-panel h3 { font-size: var(--text-xl); }
.item-analysis-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0; border-top: 1px solid var(--border); font-size: 13px; }
.item-analysis-row:first-child { border-top: none; }
/* The stat cell is deliberately nowrap (score + bar stay one unit). On a
   phone the stem can wrap across several lines on its own (rendered content,
   not a single line of text), so the row stacks into a column entirely
   instead of flex-wrapping — wrap alone let the stat sit mid-way through the
   stem's own wrapped lines, overlapping it. */
@media (max-width: 520px) {
  .item-analysis-row { flex-direction: column; align-items: flex-start; gap: 6px; }
}
.item-analysis-stem { display: flex; align-items: baseline; gap: 6px; flex: 1; min-width: 0; }
.item-analysis-stem-content :is(p, div) { margin: 0; display: inline; }
/* A stacked fraction renders its digits at roughly 70% of the em box, so at
   a flat 1em they end up noticeably smaller than the words beside them.
   A small bump costs no row height and keeps them readable. */
.item-analysis-stem-content .katex { font-size: 1.1em; }
/* Same problem the question list had: an inline diagram at natural size turns
   one analysis row into a 230px slab that breaks its own sentence in half.
   Thumbnail it — enough to see WHICH diagram, without wrecking the row. */
.item-analysis-stem-content img {
  max-height: 30px; width: auto; vertical-align: middle;
  margin: 0 4px; border: 1px solid var(--border); border-radius: 3px;
}
.item-analysis-stat { display: flex; align-items: center; gap: 8px; white-space: nowrap; color: var(--text-secondary); }
.item-analysis-bar { display: inline-block; width: 80px; height: 8px; border-radius: 4px; background: var(--sand); overflow: hidden; }
.item-analysis-bar-fill { display: block; height: 100%; background: var(--correct-border); }
.item-analysis-bar-fill.bar-good { background: var(--correct-border); }
.item-analysis-bar-fill.bar-mid { background: #C99A3C; }
.item-analysis-bar-fill.bar-low { background: #C0392B; }

/* ── Item statistics: distractors and verdicts ───────────────────────────── */
/* The row is a flex line of stem + stat. Distractors and the all-papers line
   are additional FULL-WIDTH children rather than a third and fourth column —
   hence wrap plus a 100% basis, which leaves a row without them laid out
   exactly as before. */
/* ── Staff sign in ───────────────────────────────────────────────────────── */
/* A stacked form rather than the old single-row token box: two fields plus a
   button don't fit side by side at any sensible width. */
.login-form { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.login-form input {
  padding: 10px 14px; border: 1.5px solid var(--border);
  border-radius: var(--radius-sm); background: var(--warm-white);
  font-family: inherit; font-size: var(--text-lg);
}
/* The sign-in fields. This cleared the outline and swapped the border for
   --border-card, which is two shades off the resting --border and reads as
   nothing at all. The ring is left to the shared :focus-visible rule and the
   border now moves far enough to be seen, matching .login-field input. */
.login-form input:focus { border-color: var(--brown-ink); }
.login-form button { padding: 10px 14px; }
.sidebar-whoami {
  margin: 0 0 8px; padding: 0 4px;
  font-size: 12px; color: var(--text-muted); line-height: 1.35;
}
.sidebar-whoami strong { color: var(--text-secondary); }

/* ── Import review quarantine ────────────────────────────────────────────── */
/* A withheld question has to look different from one that merely needs a
   diagram added, or the two blur and both get ticked past. */

.item-analysis-row { flex-wrap: wrap; }
/* The option spread moved to the .ia-* grammar below; the rules that lived
   here described the old stacked row and made the new grid inert —
   .item-distractor was display:contents, which dissolves the element a grid
   would have been declared on. The lineage line keeps its full-width place. */
.item-lineage-line { grid-column: 1 / -1; }
/* A wrong option that outdrew the correct one. Worth colouring: it is the
   difference between a hard question and a broken one. */
.item-distractor.is-overpowering .item-distractor-bar > span { background: #C0392B; }
.item-distractor.is-overpowering .item-distractor-letter { color: #A5321F; }
/* Blank is negative but a DIFFERENT negative from wrong — same reasoning as
   .score-pill.score-none, and the same outlined treatment. */
.item-distractor.is-blank .item-distractor-bar > span { background: #C0654F; }

.item-stat-line { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.item-verdict {
  display: inline-block; padding: 1px 8px; border-radius: 10px;
  font-size: var(--text-xs); font-weight: 700; white-space: nowrap;
  background: var(--sand); color: var(--brown-ink);
}
.item-verdict-excellent, .item-verdict-good { background: var(--correct-bg); color: var(--correct-text); }
.item-verdict-fair, .item-verdict-easy, .item-verdict-hard { background: #FBEFD5; color: var(--state-warn-text); }
.item-verdict-poor, .item-verdict-too_easy, .item-verdict-too_hard { background: #FBE1DD; color: #A5321F; }
/* A suspected miskey is the only finding here with consequences for marks
   already awarded, so it is the only one outlined. */
.item-verdict-backwards {
  background: #F6D9D3; color: #8C2716; border: 1.5px solid #C0654F; padding: 0 7px;
}
/* Not a verdict — the absence of one. Must not look like a finding. */
.item-verdict-pending {
  background: transparent; color: var(--text-muted);
  border: 1px dashed var(--border); font-weight: 600;
}

.report-heading { font-size: var(--text-xl); margin: 20px 0 8px; }
.report-header-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.report-header-row .report-heading { margin: 0; }

/* ── Student progress-over-time (hand-rolled SVG, no charting library) ──── */
.progress-chart-block {
  margin-top: 18px; padding: 14px 16px; border: 1px solid var(--border-card);
  border-radius: var(--radius-md); background: var(--warm-white);
}
.progress-chart-block:first-child { margin-top: 0; }
.progress-chart-svg { width: 100%; height: auto; margin-top: 6px; display: block; }
.progress-chart-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.progress-chart-head strong { font-size: var(--text-lg); }
.progress-chart-summary { font-size: 13px; color: var(--text-secondary); margin-top: 3px; }
.progress-dot-hit:hover ~ circle { fill: var(--warm-tan); }

/* Custom chart tooltip — replaces SVG's native <title>, which has an OS
   delay of about a second, can't be styled, and renders too small to read
   a score off at a glance. */
.chart-tooltip {
  position: absolute; z-index: 300; transform: translate(-50%, -100%);
  background: var(--deep-ink); color: #FFFEF9; border-radius: var(--radius-sm);
  padding: 9px 12px; font-size: var(--text-sm); line-height: 1.45; pointer-events: none;
  box-shadow: 0 6px 18px rgba(46, 35, 24, 0.28); white-space: nowrap;
}
.chart-tooltip-score { font-size: 17px; font-weight: 800; margin-bottom: 2px; }
.chart-tooltip-row { opacity: 0.88; }
.report-bar-fill-label { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.report-bar-fill-label.good { color: var(--correct-text); }
.report-bar-fill-label.low { color: #A5321F; }

/* ── The cohort results table ──────────────────────────────────────────────
   A TABLE, NOT A LIST OF CARDS. Every value in a column shares an edge with
   the one above it, which is the whole point: "Reading ran 5, 4, 4, 2, 2" is
   invisible when those five numbers sit in five separately-laid-out rows.

   The grid lives on the list; the header row and every data row inherit its
   tracks with `subgrid`, and .section-status-mini uses `display: contents` so
   each chip becomes a cell of the parent grid rather than a box inside one
   cell. The DOM stays as goToTestResults emits it. The column count is set
   inline from JS, because it is however many papers this test has. */
#test-results-list { display: grid; gap: 8px 14px; align-items: center; }
.results-head-row, .card-row.is-results {
  display: grid; grid-column: 1 / -1; grid-template-columns: subgrid;
  gap: 10px 14px; align-items: center;
}
.section-status-mini { display: contents; }
/* Same horizontal padding and the same 1px border box as a data row, so
   column 1 starts at the same x rather than one pixel off. */
.results-head-row {
  padding: 0 16px 9px; border: 1px solid transparent; border-bottom: 0;
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--text-secondary);
}
.results-head-row .rh-num { text-align: right; }
.results-head-row .rh-unit { text-transform: none; letter-spacing: 0; font-weight: 400; display: block; }
.card-row.is-results { padding: 12px 16px; }
.section-status-chip {
  justify-self: stretch; text-align: right; font-size: var(--text-md); font-weight: 600; color: var(--text-primary);
  padding: 3px 8px; border-radius: 7px; border: 1px solid transparent;
}
/* Value only — the column header carries the name. Kept in the markup so the
   narrow layout, where the columns collapse, still says which paper is which. */
.section-status-chip .chip-label { display: none; }
.section-status-chip.status-not_started,
.section-status-chip.is-idle { color: var(--text-secondary); background: none; }
.section-status-chip.is-waiting {
  color: var(--brown-ink); background: var(--state-warn-bg);
  border-color: var(--flag-gold); font-size: var(--text-sm);
}
/* Under a third right. One quiet tint is the difference between a grid of
   numbers and a screen that answers a question — and it is not red, because a
   hard paper is not an error. */
.section-status-chip.is-low { background: var(--incorrect-bg); border-color: var(--incorrect-border); }
.results-total { text-align: right; font-weight: 700; font-size: 14.5px; }
.results-total small { display: block; font-weight: 400; font-size: var(--text-sm); color: var(--text-secondary); }
.results-total.is-idle { font-weight: 400; color: var(--text-secondary); }
.rank-badge {
  font-size: 12px; font-weight: 700; border-radius: 999px; padding: 3px 10px;
  background: var(--sand-soft); border: 1px solid var(--border-card);
  color: var(--text-secondary); white-space: nowrap; justify-self: start;
}
.rank-badge.rank-1 { background: var(--correct-bg); border-color: var(--correct-border); color: var(--correct-text); }
.rank-badge.is-tied { background: var(--partial-bg); border-color: var(--partial-border); color: #3A4A73; }
.results-waiting { font-size: 13px; color: var(--brown-ink); white-space: nowrap; }
.results-waiting a { color: inherit; }
.card-row.is-results .card-row-actions { justify-content: flex-end; }
.card-row.is-results.is-waiting-mark { background: var(--state-attention-bg); }
.card-row.is-results.is-idle .card-row-title { color: var(--text-secondary); }
/* Below this the columns cannot hold their meaning, so the chips go back to
   being labelled chips rather than silently losing their headers. */
@media (max-width: 1200px) {
  #test-results-list { display: flex; flex-direction: column; gap: 8px; }
  .results-head-row { display: none; }
  /* Name and chips take a line each; total, rank and the buttons share the
     third. Left as a grid, each of those three claimed a row of its own and a
     six-student cohort ran to four screens. */
  .card-row.is-results { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
  .card-row.is-results .card-row-title,
  .section-status-mini { flex: 1 0 100%; }
  .section-status-mini { display: flex; flex-wrap: wrap; gap: 6px; }
  .section-status-chip { justify-self: start; text-align: left; }
  .section-status-chip .chip-label { display: inline; }
  .results-total { text-align: left; }
  .results-total small { display: inline; margin-left: 6px; }
  .card-row.is-results .card-row-actions { margin-left: auto; }
}

/* ── Marking (written responses) ───────────────────────────────────────── */
.marking-item { margin-top: 14px; }
.marking-prompt { font-size: 14px; color: var(--text-secondary); border-left: 3px solid var(--border-card); padding-left: 10px; margin-bottom: 10px; }
.marking-response {
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff;
  padding: 12px 14px; font-size: 14px; line-height: 1.7; max-height: 320px; overflow-y: auto;
}
.marking-response p { margin: 0 0 8px; }
.marking-controls { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.preview-writing-box {
  border: 1.5px dashed var(--border); border-radius: var(--radius-sm); padding: 24px 14px;
  color: var(--text-muted); font-size: 13px; text-align: center;
}

/* ── Formula modal ─────────────────────────────────────────────────────── */
.formula-quick-row { display: flex; gap: 8px; flex-wrap: wrap; }
.formula-preview {
  min-height: 48px; border: 1.5px solid var(--border); border-radius: var(--radius-sm);
  padding: 10px 14px; background: var(--parchment); display: flex; align-items: center;
}
.text-input, textarea.text-input {
  width: 100%; padding: 8px 12px; border: 1.5px solid var(--border); border-radius: var(--radius-sm);
  background: var(--warm-white); font-size: 14px; color: var(--text-primary);
}
textarea.text-input { resize: vertical; min-height: 60px; }

/* min-width: 0 on both children is the part that is easy to leave out and
   expensive to debug. A grid item defaults to min-width: auto, which means it
   refuses to shrink below its widest child — so ONE oversized figure silently
   takes the track wider than its fr share, the other pane is squeezed, and the
   whole grid runs off the side of the page. The ratio below only holds because
   of this line. */
.editor-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 28px; align-items: start; }
.editor-form, .editor-preview { min-width: 0; }
.editor-form, .editor-preview { background: var(--warm-white); border: 1px solid var(--border-card); border-radius: var(--radius-lg); padding: 24px; }
/* Sticky, and therefore its own scrolling region. Without the max-height a
   question with two diagrams grew taller than the viewport, and because the
   panel is stuck to the top there was no way to reach its bottom — the page
   scrolled underneath it and the fifth answer option simply could not be seen.
   A sticky panel has to be able to scroll itself or it can only ever show what
   fits on one screen. */
.editor-preview {
  position: sticky; top: 24px;
  max-height: calc(100vh - 48px); overflow-y: auto;
}

/* The editor's two side-by-side panels (form | live preview) don't fit on
   narrow viewports — stack them, form first, and un-stick the preview so
   it scrolls with the page instead of shadowing the form's tail. */
@media (max-width: 900px) {
  .editor-grid { grid-template-columns: 1fr; gap: 20px; }
  .editor-preview { position: static; }
  .editor-form, .editor-preview { padding: 18px 16px; }
}

.options-list { display: flex; flex-direction: column; gap: 14px; margin-top: 10px; }
.option-card { border: 1.5px solid var(--border); border-radius: var(--radius-md); padding: 14px 16px; background: var(--warm-white); }
.option-card.is-correct { border-color: var(--correct-border); background: #F3F8F3; }
.option-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.option-header label { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
.option-remove { margin-left: auto; background: none; border: none; color: #C0392B; font-size: 12px; }

/* The preview pane's whole job is "this is what the student sees" — so it
   uses the exam font too, otherwise it was quietly lying about the result. */
.preview-card { border: 1px solid var(--border); border-radius: var(--radius-md); padding: 16px; font-family: var(--exam-font); }
/* The panel is a fraction of the exam's width, so the student-side type scale
   is stepped down here — the LAYOUT is the fidelity that matters in a side
   panel, not the point size. Everything else comes from the shared sheet. */
.preview-card .question-stem { font-size: 15.5px; margin-bottom: 16px; }
.preview-card .option-row { font-size: 14.5px; gap: 10px; padding: 5px 4px; }
.preview-card .option-row input { width: 20px; height: 20px; }
.preview-card .question-options { gap: 8px; }
.preview-card .match-statement { font-size: 14px; }
.preview-card .writing-editor { font-size: 14px; line-height: 1.6; }
.preview-card .writing-toolbar { flex-wrap: wrap; }
.editor-preview-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; min-height: 24px; }
.editor-preview-head h3 { margin: 0; }
.preview-answers-toggle { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: var(--text-sm); color: var(--text-secondary); cursor: pointer; }
.preview-answers-toggle input { cursor: pointer; }
.content-block-image { display: inline-block; max-width: 100%; max-height: 200px; vertical-align: middle; border-radius: 6px; margin: 4px 0; }
.content-block-text { display: inline; }

/* ── Answer options that carry pictures ────────────────────────────────────
   PICTURE OPTIONS ARE THE ONLY TYPE THAT CHANGES THE CARD'S SHAPE. Four
   stacked images is a scroll, and the student meets them side by side — so
   the editor draws them the way the paper will, four across, the moment one
   option is a picture. Typed answers, writing and cloze change the answer row
   only and are left alone. */
#options-editor.is-pictorial {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; align-items: start;
}
@media (max-width: 1000px) {
  #options-editor.is-pictorial { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
#options-editor.is-pictorial .option-card { padding: 10px; }
#options-editor.is-pictorial .option-header { flex-wrap: wrap; gap: 6px; }
#options-editor.is-pictorial .option-header label { font-size: 12px; }
#options-editor.is-pictorial .option-remove { font-size: var(--text-xs); }

/* The chooser, per option — a maths question is often three diagrams and a
   "none of these", so this cannot be a per-question setting. */
.option-modes { display: flex; margin-left: auto; border: 1px solid var(--border-card); border-radius: 7px; overflow: hidden; }
.option-mode {
  font-family: var(--font-ui); font-size: 11.5px; font-weight: 700;
  color: var(--brown-ink); background: transparent; border: 0;
  border-left: 1px solid var(--border-card); padding: 4px 9px; cursor: pointer;
}
.option-mode:first-child { border-left: 0; }
.option-mode.is-on { color: var(--parchment); background: var(--deep-ink); }

/* FIXED 4:3, IDENTICAL ACROSS THE FOUR. The tallest image must not set the
   others — an option bigger than its neighbours reads as the answer. The
   picture is FITTED inside, never cropped or stretched, so a wide diagram
   letterboxes rather than losing its edges. */
.option-pic {
  position: relative; aspect-ratio: 4 / 3; margin-top: 8px;
  background: var(--parchment); border: 1px solid var(--border);
  border-radius: var(--radius-sm); overflow: hidden; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.option-pic img { max-width: 100%; max-height: 100%; object-fit: contain; }
.option-pic.is-over { border-color: var(--brown-ink); background: var(--sand); }
/* The one fault that ships a question nobody can answer, so it is the one
   loud state on this card. An empty box that looked finished is what this
   replaces. */
.option-pic.is-empty { border: 1.5px dashed var(--incorrect-border); background: var(--state-bad-bg); }
.option-pic-hint {
  text-align: center; font-size: 11.5px; line-height: 1.5; color: var(--state-bad-text); padding: 8px;
}
.option-pic-hint strong { display: block; font-size: 12px; margin-bottom: 2px; }
.option-card.is-missing-pic { border-color: var(--incorrect-border); }

/* ── The passage editor: one object, two panes ─────────────────────────────
   The extract on the left, the questions that depend on it on the right. They
   are written against each other — an extract is only right when it supports
   its questions — and that used to mean holding six of them in your head on a
   different screen.

   The right pane scrolls on its own, so you can read down the questions
   without losing your place in the extract, and vice versa. */
.pe-panes { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 460px); gap: 22px; align-items: start; }
.pe-left { min-width: 0; display: flex; flex-direction: column; }
.pe-right {
  min-width: 0; position: sticky; top: 12px;
  border: 1px solid var(--border-card); border-radius: var(--radius-md);
  background: var(--warm-white); overflow: hidden;
  max-height: calc(100vh - 140px); display: flex; flex-direction: column;
}
.pe-right-head { padding: 14px 16px; border-bottom: 1px solid var(--sand); background: var(--sand-soft); }
.pe-right-head h3 { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: var(--text-lg); }
.pe-right-head .hint-text { margin: 4px 0 0; }
#passage-questions { overflow-y: auto; padding: 6px; }

/* A question, as the student meets it — stem then options, the key marked.
   READ-ONLY: clicking opens the editor it already has, because a second stem
   editor here would be a second implementation to keep in step. */
.pe-q {
  padding: 12px; border-radius: var(--radius-sm); cursor: pointer;
  border-bottom: 1px solid var(--hairline);
}
.pe-q:last-child { border-bottom: 0; }
.pe-q:hover { background: var(--parchment); }
.pe-q-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 5px; }
.pe-q-n { font-family: var(--font-ui); font-weight: 700; font-size: var(--text-sm); color: var(--text-secondary); }
/* The exam's own face, so a stem reads here as it will be sat. */
.pe-q-stem { font-family: var(--exam-font); font-size: var(--text-md); line-height: 1.5; }
.pe-q-stem :is(p, div) { margin: 0 0 4px; }
.pe-opts { display: grid; gap: 3px; margin-top: 7px; }
.pe-opt { display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; gap: 6px; font-size: var(--text-sm); align-items: baseline; }
.pe-opt-letter { font-weight: 700; color: var(--text-secondary); }
.pe-opt-body :is(p, div) { margin: 0; }
.pe-opt-body img { max-width: 90px; height: auto; display: block; }
.pe-opt.is-correct .pe-opt-letter { color: var(--correct-text); }
.pe-opt.is-correct .pe-opt-body { font-weight: 600; }

@media (max-width: 1000px) {
  /* Below this the two panes stop being two panes — a 300px extract is not
     something you can read, let alone write against. */
  .pe-panes { grid-template-columns: minmax(0, 1fr); }
  .pe-right { position: static; max-height: none; }
}

/* How the cohort split across the options, beside the options. Quiet, because
   it is evidence you glance at while writing rather than the thing you came
   for — except when a distractor beat the key, which is the one case worth a
   colour. */
.pe-opt-chosen {
  font-family: var(--font-ui); font-size: 11.5px; color: var(--text-secondary); white-space: nowrap;
}
.pe-opt-chosen.is-over-key { color: var(--state-bad-text); font-weight: 700; }

/* A READING MEASURE ON THE EXTRACT. A passage is long prose and the left pane
   is ~700px, which is 100-odd characters a line — comfortable to edit and
   tiring to read, and this is a surface you do both on. Capped near the
   measure the player uses so the extract reads here roughly as it will be sat.
   The cap is on the text, not the pane: the toolbar and the card keep the
   full width. */
.pe-left .tab-content-rte { max-width: 62ch; }

/* ── Tables in the editor ──────────────────────────────────────────────────
   Styled in the box you type them in the way the shared renderer ships them
   — the figure band's rule again: a layout you can only see in the preview
   is a layout you cannot trust. */
.rte-editor .q-table { border-collapse: collapse; margin: 16px 0; }
.rte-editor .q-table th,
.rte-editor .q-table td {
  border: 1px solid var(--border); padding: 9px 14px; text-align: left;
  min-width: 36px; vertical-align: middle;
}
.rte-editor .q-table th { background: var(--sand-soft); font-weight: 600; }
/* The cell the table controls act on. */
.rte-editor .q-table td.selected,
.rte-editor .q-table th.selected { outline: 2px solid var(--brown-ink); outline-offset: -2px; }

/* The box, in the editor, in the same grammar the student sees — the figure
   band's rule again: a frame you can only see in the preview is a frame you
   cannot trust. */
.rte-editor .q-box {
  border: 1.5px solid var(--brown-ink); border-radius: 2px;
  padding: 10px 14px; margin: 12px 0;
}
.rte-editor .q-box > :first-child { margin-top: 0; }
.rte-editor .q-box > :last-child { margin-bottom: 0; }

/* Alignment, shown while you author it — not only in the preview. */
.rte-editor .q-table.is-centred {
  width: fit-content; margin-left: auto; margin-right: auto;
}
.rte-editor .q-table .ta-c { text-align: center; }

/* A blank cell, shown blank while you author it. */
.rte-editor .q-table .is-blank { border: none; background: transparent; }

/* The cells a merge would act on. Shift-click paints a rectangle; the tint is
   the only way to know what "Merge 6" means before pressing it. */
.rte-editor .q-table .is-sel { background: var(--sand); }
.rte-editor .q-table th.is-sel { background: var(--sand-soft); }
.img-tools-hint {
  font-size: var(--text-xs); color: var(--text-muted); padding: 0 6px;
  font-style: italic;
}

/* A faint word above a run of buttons. A rule between two runs says they
   differ; a word says how — and the contextual bars already label their
   sections this way, so the toolbar now matches them. */
.rte-toolbar .rte-group {
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-muted);
  align-self: center; padding: 0 6px 0 2px;
}
.rte-toolbar .rte-group:not(:first-child) {
  margin-left: 6px; border-left: 1px solid var(--border); padding-left: 10px;
}

/* The cheat sheet. Wider than a confirm card because it is a reference, and
   scrollable because it will only grow. */
.login-card.help-card { max-width: 640px; text-align: left; }
.help-card #help-body { max-height: 60vh; overflow-y: auto; margin: 4px 0 14px; }
.help-card .help-section {
  font-size: var(--text-sm); font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-muted);
  margin: 16px 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--border);
}
.help-card .help-section:first-child { margin-top: 0; }
.help-card .help-list {
  display: grid; grid-template-columns: minmax(140px, auto) 1fr;
  gap: 6px 16px; margin: 0; align-items: baseline;
}
.help-card .help-list dt {
  font-weight: 600; color: var(--text-primary); font-size: var(--text-sm);
}
.help-card .help-list dd {
  margin: 0; color: var(--text-secondary); font-size: var(--text-sm);
}
@media (max-width: 560px) {
  .help-card .help-list { grid-template-columns: 1fr; gap: 2px 0; }
  .help-card .help-list dd { margin-bottom: 8px; }
}

/* The symbol palette. Every character in it is print-safe — see the JS. */
.symbol-pop {
  position: absolute; z-index: 60; padding: 8px 10px 10px;
  background: var(--warm-white); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: 0 6px 18px rgba(60, 45, 20, 0.18);
}
.symbol-group {
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-muted); margin: 6px 0 3px;
}
.symbol-group:first-child { margin-top: 0; }
.symbol-row { display: flex; flex-wrap: wrap; gap: 3px; max-width: 250px; }
.symbol-row button {
  min-width: 30px; height: 30px; font-size: 15px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--warm-white); color: var(--text-primary); font-family: inherit;
}
.symbol-row button:hover { background: var(--sand-soft); border-color: var(--brown-ink); }
.rte-toolbar sup, .rte-toolbar sub { font-size: 0.7em; }

/* A sub-option greyed while its parent is off: sit-once under practice. */
.checkbox-label.is-disabled { opacity: 0.5; }

/* One question, three answers: what kind of paper this is (item 96). */
.paper-kind { border: 0; padding: 0; margin: 0 0 6px; }
.paper-kind legend { font-weight: 700; font-size: 14px; color: var(--text-primary); margin-bottom: 6px; padding: 0; }
.paper-kind .radio-label { display: flex; align-items: flex-start; gap: 8px; font-size: 14px; color: var(--text-primary); cursor: pointer; margin: 4px 0; }
.paper-kind .radio-label input { margin-top: 3px; flex: 0 0 auto; }
