/* One stylesheet for the whole app. Kept deliberately small and plain --
   there is no framework here, and nothing below needs a build step. */

/* Palette
   ------------------------------------------------------------------
   --lesson-cyan is the exact background colour of the ActivePresenter
   lessons, rgb(171,241,254). It appears here as the HEADER and as
   accents, not as the page background: the RAG grid and the mark-entry
   grid both need a neutral surround, or red/amber/green cells lose
   contrast against a saturated field and become harder to tell apart.
   Students see more of it in the student portal; teachers see it in the
   header, so the product still reads as one family.

   The three button colours are the existing site's, kept so the new
   pages and the old ones match: rgb(51,102,255) at rest,
   rgb(185,203,255) on hover, rgb(0,35,138) when pressed.

   THE LABEL COLOUR HAS TO MOVE WITH THEM. Measured: white on the rest
   colour is 4.68:1 (clears WCAG AA's 4.5, but only just); white on the
   pale hover colour collapses to 1.61:1 and is genuinely unreadable,
   while dark ink on it reaches 10.14:1. The pressed colour is the
   reverse -- white on it is 13.06:1. So the button goes white-on-blue,
   dark-on-pale, white-on-navy, and stays readable throughout.

   Blue text directly ON the cyan header would only reach 3.73:1, so
   don't do that; the header carries dark ink and a white button. */
:root {
  --ink: #16212b;
  --ink-soft: #55636f;
  --line: #d7dfe4;
  --paper: #ffffff;
  --page: #f6f8f9;
  --lesson-cyan: #abf1fe;
  --lesson-cyan-deep: #7fdcf0;
  --banner: #f7f7f7;      /* the lessons' heading-banner grey */
  --brand: #3366ff;
  --brand-hover: #b9cbff;
  --brand-active: #00238a;
  --error: #b3261e;
  --error-bg: #fdecea;
  --success: #1b5e20;
  --success-bg: #e8f5e9;
  --info: #0d4f61;
  --info-bg: #e4f7fc;
  /* RAG, shown on the neutral page background rather than on cyan. */
  --rag-red: #f4c7c3;
  --rag-amber: #fde9b9;
  --rag-green: #c9e7c4;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* ---- Centred card, used by the login and sign-up pages ---- */

.auth-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 1.5rem;
}

.card {
  width: 100%;
  max-width: 26rem;
  padding: 2rem;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.card-brand {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin-bottom: 1.25rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line);
}

.card-brand img { width: 38px; height: 38px; }

.card-brand span {
  font-size: 1.05rem;
  font-weight: 600;
}

.card h1 {
  margin: 0 0 0.25rem;
  font-size: 1.5rem;
}

.subtitle {
  margin: 0 0 1.5rem;
  color: var(--ink-soft);
  font-size: 0.95rem;
}

/* ---- Forms ---- */

label {
  display: block;
  margin-bottom: 1rem;
  font-weight: 600;
  font-size: 0.9rem;
}

/* THE SAME FAULT AS .header-right[hidden] BELOW, and the second time it has
   shipped -- so read that comment too, because the pair of them is the whole
   lesson. `display: block` above comes from OUR stylesheet and so beats the
   browser's own `[hidden] { display: none }`, which means THE HIDDEN ATTRIBUTE
   SILENTLY DOES NOTHING ON A <label>. No error, no warning: the control simply
   renders.

   Found on 2026-08-14 walking journey 218. assessment-new.html hides the
   assessment-point control entirely when the school has defined no point for
   that class's year group -- an empty dropdown being a question a teacher
   cannot answer and cannot make go away -- and the label appeared anyway,
   carrying a dropdown whose only option was "— none —": precisely the thing the
   hiding exists to prevent.

   The general rule this keeps proving: `hidden` works only on an element
   nothing in here gives a `display` to. That is why `.message` deliberately
   gets position and padding but NO display, and it is the first thing to check
   when something hides on one page and not another. */
label[hidden] { display: none; }

input {
  display: block;
  width: 100%;
  margin-top: 0.35rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  font: inherit;
  font-weight: 400;
}

input:focus {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
  border-color: var(--brand);
}

.hint {
  display: block;
  margin-top: 0.3rem;
  color: var(--ink-soft);
  font-weight: 400;
  font-size: 0.8rem;
}

button {
  padding: 0.65rem 1.1rem;
  border: 0;
  border-radius: 6px;
  background: var(--brand);
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

/* The label flips to dark ink on the pale hover colour -- see the palette note.
   Without this the text would sit at 1.61:1 and effectively disappear. */
button:hover:not(:disabled) {
  background: var(--brand-hover);
  color: var(--ink);
}

button:active:not(:disabled) {
  background: var(--brand-active);
  color: #fff;
}
button:disabled { opacity: 0.6; cursor: default; }
button.full { width: 100%; }

button.link-button {
  padding: 0;
  background: none;
  color: var(--brand);
  font-weight: 400;
  text-decoration: underline;
}

/* Text links darken rather than lighten: the pale hover works behind a label,
   but as a text colour on white it would be unreadable. */
button.link-button:hover { background: none; color: var(--brand-active); }

/* An icon-only control -- the case where a tooltip earns its place. */
button.icon-button {
  padding: 0.3rem 0.5rem;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--line);
  font-size: 1rem;
  line-height: 1.2;
}

button.secondary {
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--line);
}

/* Secondary buttons keep their own quiet hover -- they are already dark-on-pale,
   so the brand hover colour would say nothing here. */
button.secondary:hover:not(:disabled) {
  background: var(--page);
  color: var(--ink);
  border-color: var(--ink-soft);
}

button.secondary:active:not(:disabled) {
  background: var(--brand-active);
  color: #fff;
  border-color: var(--brand-active);
}

/* "or" rule between the Microsoft button and the email form. */
.divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.25rem 0;
  color: var(--ink-soft);
  font-size: 0.85rem;
}

.divider::before,
.divider::after {
  content: '';
  flex: 1;
  border-top: 1px solid var(--line);
}

.form-footer {
  margin: 1.25rem 0 0;
  font-size: 0.9rem;
  text-align: center;
}

a { color: var(--brand); }
a:hover { color: var(--brand-active); }

/* A link that should look like a button. */
.button-link {
  display: inline-block;
  padding: 0.65rem 1.1rem;
  border-radius: 6px;
  background: var(--brand);
  color: #fff;
  font-weight: 600;
  text-decoration: none;
}

.button-link:hover { background: var(--brand-hover); color: var(--ink); }
.button-link:active { background: var(--brand-active); color: #fff; }

/* ---- Messages ---- */

/* The right padding is room for the dismiss button, which is positioned rather
   than laid out. That is deliberate and is worth not "tidying" into a flex row:
   `hidden` is how every one of these boxes is put away, and a `display` of our
   own on this selector would beat the browser's `[hidden] { display: none }` and
   silently stop hideMessage() working on all 18 pages. That exact fault cost an
   afternoon on `.header-right` in August 2026. */
.message {
  position: relative;
  margin: 0 0 1rem;
  padding: 0.7rem 2.4rem 0.7rem 0.85rem;
  border-radius: 6px;
  font-size: 0.9rem;
}

.message-error   { background: var(--error-bg);   color: var(--error); }
.message-success { background: var(--success-bg); color: var(--success); }
.message-info    { background: var(--info-bg);    color: var(--info); }

/* Added by showMessage(), so it appears on messages the page put up in reply to
   something you did -- and not on the static banners a panel renders to explain
   itself, which carry no button and would come back on the next redraw anyway.
   It inherits the message's own colour, so it is legible on all three kinds. */
.message-close {
  position: absolute;
  top: 0.3rem;
  right: 0.35rem;
  width: 1.7rem;
  height: 1.7rem;
  padding: 0;
  border-radius: 4px;
  background: transparent;
  color: inherit;
  font-size: 1.15rem;
  font-weight: 400;
  line-height: 1;
  opacity: 0.7;
}

/* The symbol is here rather than in the markup so that reading a message box's
   textContent gives the message and nothing else. */
.message-close::before { content: '\00d7'; }

.message-close:hover:not(:disabled),
.message-close:focus-visible {
  background: rgb(0 0 0 / 0.08);
  color: inherit;
  opacity: 1;
}

/* ---- Dashboard ---- */

.app-header {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
  justify-content: space-between;
  padding: 0.7rem 1.5rem;
  background: var(--lesson-cyan);
  border-bottom: 1px solid var(--lesson-cyan-deep);
}

.brand {
  display: flex;
  gap: 0.6rem;
  align-items: center;
}

.brand img {
  width: 32px;
  height: 32px;
}

.app-header h1 {
  margin: 0;
  font-size: 1.1rem;
  letter-spacing: 0.01em;
}

.header-right {
  display: flex;
  gap: 0.9rem;
  align-items: center;
  font-size: 0.9rem;
  color: var(--ink);
}

/* `display: flex` above comes from the author stylesheet and so beats the
   browser's own `[hidden] { display: none }`, which means the hidden attribute
   SILENTLY DOES NOTHING on this element. student-join.html hides this block
   until it knows somebody is signed in, and without this rule a Sign out
   button sits next to the Sign in button on the first state of the page. */
.header-right[hidden] { display: none; }

/* ---- Tooltips ----
   For ICON-ONLY controls. A button with its function written inside it does not
   need one, and shouldn't have one -- a tooltip repeating a visible label is
   just noise. Kept here because the mark-entry grid and the student portal are
   likely to want compact icon controls.

   Put data-tip on the element, and aria-label too, so the meaning is never
   hover-only: hover does not exist on a touchscreen and is invisible to a
   screen reader. */

[data-tip] { position: relative; }

[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 10;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--ink);
  border-radius: 4px;
  background: var(--lesson-cyan);
  color: var(--ink);
  font-size: 0.8rem;
  font-weight: 400;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s;
}

[data-tip]:hover::after,
[data-tip]:focus-visible::after { opacity: 1; }

/* Anyone who prefers less motion still gets the tip, just without the fade. */
@media (prefers-reduced-motion: reduce) {
  [data-tip]::after { transition: none; }
}

/* ---- Page width ----
   Three named widths, and every page picks one deliberately.
   Changed 2026-08-06: the default used to be 46rem, which was chosen when every
   screen was a short form. As tables gained columns they outgrew it -- the
   dashboard's class list reached "Login codes" and started scrolling sideways
   inside .table-scroll, which reads as a broken layout rather than as a wide
   table. Widening the default fixes that everywhere at once, including the
   screens not built yet.

   80rem (1280px) rather than no limit at all: a table is happier the wider it
   gets, but a paragraph is not, and `input { width: 100% }` means an
   unconstrained form turns "Class name" into a box the width of the monitor.
   80rem is wide enough for every table the app currently draws and still a
   sane measure for prose.

   The two exceptions both earn it:
     .wide   -- no cap. For grids sized by the DATA rather than by the design:
                the mark grid, the QLA sheet, the topic grid, the import
                preview. These are meant to use every pixel and to scroll when
                they run out.
     .narrow -- back to 46rem. For pages that are one column of form. */

main {
  max-width: 80rem;
  margin: 0 auto;
  padding: 1.5rem;
}

main.narrow { max-width: 46rem; }

.panel {
  padding: 1.5rem;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.panel h2 {
  margin: 0 0 0.5rem;
  font-size: 1.15rem;
}

.panel p { margin: 0 0 0.75rem; }
.panel p:last-child { margin-bottom: 0; }

.muted { color: var(--ink-soft); }

/* A panel that folds away, for a secondary route on a screen that already has a
   primary one -- the CSV upload on the mark grid. Native <details>, so it needs
   no JavaScript and keyboard and screen readers get it for free. Closed, it is
   one line; the padding shrinks to match so it does not sit there looking like
   an empty box. */
.panel.collapsible { padding: 0.9rem 1.5rem; }
.panel.collapsible[open] { padding-bottom: 1.5rem; }

.panel.collapsible > summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--brand);
}

.panel.collapsible[open] > summary { margin-bottom: 0.9rem; }

/* ---- Mark-entry grid ----
   Sized entirely by the data: as many columns as the paper has questions, as
   many rows as the class has students. The pinned name column and header row
   are what make a 60 x 30 grid usable -- without them you lose track of which
   student and which question you are on the moment you scroll. */

main.wide { max-width: none; }

.grid-scroll {
  overflow: auto;
  max-height: 70vh;      /* the grid scrolls, the page does not */
  margin: 0 -0.25rem;
}

.mark-grid {
  border-collapse: separate;   /* separate, or sticky cells lose their borders */
  border-spacing: 0;
  font-size: 0.9rem;
}

.mark-grid th,
.mark-grid td {
  padding: 0;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.mark-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 0.35rem 0.4rem;
  background: var(--banner);
  text-align: center;
  vertical-align: bottom;
}

.mark-grid .q-number { display: block; font-weight: 600; color: var(--ink); }
.mark-grid .q-max {
  display: block;
  font-size: 0.72rem;
  font-weight: 400;
  color: var(--ink-soft);
}

/* Student name pinned left, total pinned right: both stay visible however far
   across a long paper the teacher has scrolled. */
.mark-grid .student-name {
  position: sticky;
  left: 0;
  z-index: 1;
  min-width: 11rem;
  padding: 0.35rem 0.6rem;
  background: var(--paper);
  text-align: left;
  font-weight: 400;
}

/* TWO PINNED COLUMNS ON THE RIGHT SINCE 2026-08-18, not one: the row's total,
   and the total the teacher typed off the paper to check it against.

   THE OFFSET AND THE WIDTH ARE ONE FACT WRITTEN TWICE AND MUST AGREE -- the same
   sentence .rag-grid's three pinned columns carry, and the same arithmetic:
   `right` on the Total is the width of the Check column beside it. It is only
   safe because `* { box-sizing: border-box }` makes min-width include the
   padding, so 6.5rem is a width the content cannot exceed: the widest thing in
   there is the heading's second line, "their total", at 0.72rem. Widen that
   wording and this offset must widen with it, or the two columns overlap. */
.mark-grid .row-total {
  position: sticky;
  right: 6.5rem;
  z-index: 1;
  min-width: 4rem;
  padding: 0.35rem 0.6rem;
  background: var(--paper);
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.mark-grid .row-check {
  position: sticky;
  right: 0;
  z-index: 1;
  min-width: 6.5rem;
  padding: 0.35rem 0.6rem;
  background: var(--paper);
  text-align: right;
}

.mark-grid thead .student-name,
.mark-grid thead .row-total,
.mark-grid thead .row-check { z-index: 3; background: var(--banner); }

/* "Question" and "Marks" sit against the right edge, next to the columns they
   label. The student names below stay left-aligned. */
.mark-grid thead .student-name { text-align: right; }

/* The box sits centred in its column. Columns are as wide as their heading --
   "17bii" is wider than a two-digit mark -- so without this the input would
   hug the left edge and the grid would look ragged. */
.mark-grid td.cell { text-align: center; }

.mark-input {
  width: 3rem;
  /* `margin: 0 auto`, not `margin: 0`. The shared `input` rule above sets
     display:block, so this is a fixed-width BLOCK box -- and a block box ignores
     the parent's text-align and sits hard left. Auto side margins are what
     actually centre it in a column wider than 3rem, which is most of them once
     a heading like "17bii" sets the width. */
  margin: 0 auto;
  padding: 0.35rem 0.2rem;
  border: 0;
  border-radius: 0;
  background: transparent;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* The Check column's box. Wider than a mark box because it holds a paper total
   -- three digits rather than one -- and pushed to the right of its column so it
   lines up under the heading and beside the Total it is checking. */
.check-input {
  width: 3.6rem;
  margin: 0 0 0 auto;
  padding: 0.35rem 0.2rem;
  border: 0;
  border-radius: 0;
  background: transparent;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.mark-input:focus,
.check-input:focus {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.check-input.cell-unsaved {
  background: var(--brand-hover);
  color: var(--ink);
}

.check-input.cell-error {
  background: var(--error-bg);
  color: var(--error);
  font-weight: 600;
}

/* Cell states, in the order they take precedence:
     (nothing)     blank, or matching what is stored
     .cell-staged  cyan  -- typed by the STUDENT, filled in for checking
     .cell-unsaved blue  -- a mark entered or changed, not yet saved
     .cell-blank   red   -- blank that will be saved as 0, awaiting confirmation
     .cell-error   red   -- outside the range for its question
   Error last so it wins: a cell can be both changed and wrong, and wrong is
   the more urgent thing to say.

   .cell-staged is FIRST, so the teacher overtyping a staged mark turns it blue:
   at that moment whose number it was stops being the interesting fact and
   "changed, not yet saved" starts being it. The left border is there so the
   state is not carried by colour alone -- cyan and blue are a hard pair to tell
   apart, and this grid is read at speed. */
.mark-input.cell-staged {
  background: var(--info-bg);
  box-shadow: inset 3px 0 0 var(--info);
}

.mark-input.cell-unsaved {
  background: var(--brand-hover);
  color: var(--ink);
}

.mark-input.cell-blank {
  background: var(--error-bg);
  box-shadow: inset 0 0 0 2px var(--error);
}

.mark-input.cell-error {
  background: var(--error-bg);
  color: var(--error);
  font-weight: 600;
}

/* The "Marks to enter" list at the top of a child's own page. A stack on a
   phone and a row on anything wider, which is what `wrap` buys: the paper name,
   where they are up to, and the button, in reading order either way. */
.entry-list { margin: 0; padding: 0; list-style: none; }

.entry-list li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.9rem;
  align-items: center;
  padding: 0.6rem 0;
  border-top: 1px solid var(--line);
}

.entry-list li:first-child { border-top: 0; }
.entry-paper { font-weight: 600; }

/* "they do not match your teacher's total" is the one line here worth stopping
   at, and it is a sentence rather than a cell, so it takes the error colour
   without the error background -- a red block in a list of papers would read as
   the page having failed. */
.entry-warn { color: var(--error); font-size: 0.9rem; }

/* ---- A STUDENT TYPING THEIR OWN MARKS (student-marks.html, 2026-08-18) ----
   Three columns read down a phone, not a grid read across a laptop -- so none of
   the .mark-grid machinery above applies: nothing is pinned, nothing is
   rotated, and the rows are tall enough to be tapped rather than tabbed. */
.entry-grid { width: 100%; table-layout: fixed; }

/* THE WIDTHS ARE STATED RATHER THAN LEFT TO THE CONTENT, which is what
   `table-layout: fixed` buys. Auto layout sizes a column by its widest cell,
   and every cell here is one or two characters against a heading of eight to
   nine -- so all three columns came out as wide as their own headings, in
   proportions that had nothing to do with the data, and the boxes drifted away
   from the numbers they belong to. A third each is the honest answer: three
   columns of equal weight, read down. */
.entry-grid th,
.entry-grid td {
  width: 33.33%;
  padding: 0.4rem 0.6rem;
  /* CENTRED, headings and data alike. The inherited `.num { text-align: right }`
     and the left-aligned question number pulled the three columns three
     different ways on a screen only 46rem wide, so nothing lined up with the
     heading above it. */
  text-align: center;
}

/* The question number is the thing a child matches against the paper in their
   hand, so it is the one bold thing on the row. */
.entry-grid .q-number { font-weight: 600; }

.entry-grid .mark-input {
  width: 3.4rem;
  /* `margin: 0 auto` centres it in its column: the shared `input` rule sets
     display:block, so a fixed-width block box ignores the parent's text-align
     and sits hard left without side margins. Same fact as .mark-grid's box. */
  margin: 0 auto;
  padding: 0.45rem 0.3rem;
  text-align: center;
  font-variant-numeric: tabular-nums;

  /* A REAL BORDER, unlike every other box in this project's grids. The base
     .mark-input is `border: 0; background: transparent` because on the
     teacher's grid the ruled table IS the border -- sixty of them boxed in
     would be a wall of lines. Here there are three columns on a page of white,
     and an invisible box is one a child has to find by clicking about until
     they land on it. Reported on the walk of 2026-08-19. */
  border: 1px solid var(--ink-soft);
  border-radius: 4px;
  background: var(--paper);
}

.entry-grid tfoot th,
.entry-grid tfoot td {
  border-top: 2px solid var(--line);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* "This question is only worth 4." -- under the box it is about, because on a
   phone a message at the top of the page is off the screen by the time a child
   has scrolled to question 17. */
.cell-note {
  display: block;
  margin-top: 0.15rem;
  color: var(--error);
  font-size: 0.75rem;
  text-align: center;
  white-space: normal;
}

.error-list {
  margin: 0;
  padding-left: 1.1rem;
  font-size: 0.9rem;
}

.error-list li { margin-bottom: 0.25rem; }
.error-list .link-button { font-weight: 600; }

.save-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  align-items: center;
  margin-top: 1rem;
}

.panel.confirm {
  margin-top: 1rem;
  border-color: var(--error);
}

.panel.confirm h3 { margin: 0 0 0.5rem; font-size: 1.05rem; }

/* A ROW WHOSE MARKS DO NOT ADD UP TO THE TOTAL THE TEACHER TYPED. Asked for by
   the developer -- "these rows should be highlighted in marks.html so the
   teacher can double check them".

   The three PINNED cells are what carry it, not the question cells: the name is
   always on screen however far right the grid is scrolled, the Total and the
   Check are the two numbers that disagree, and tinting sixty cells between them
   would drown the .cell-staged shade that says where the marks came from.

   These rules sit AFTER the .student-name / .row-total / .row-check backgrounds
   above, which is what lets them win -- all four are one class deep. */
.mark-grid tr.row-mismatch .student-name,
.mark-grid tr.row-mismatch .row-total,
.mark-grid tr.row-mismatch .row-check { background: var(--error-bg); }

.mark-grid tr.row-mismatch .student-name { box-shadow: inset 3px 0 0 var(--error); }

/* ...and hover still wins over it, or the row a teacher is pointing at would be
   the one row she could not tell the state of. */
.mark-grid tbody tr:hover .student-name { background: var(--page); }

.save-status { font-size: 0.85rem; }
.save-saving  { color: var(--ink-soft); }
.save-unsaved { color: var(--ink-soft); }
.save-saved   { color: var(--success); }
.save-error   { color: var(--error); font-weight: 600; }

kbd {
  padding: 0.05rem 0.3rem;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 3px;
  background: var(--paper);
  font-size: 0.85em;
}

.panel + .panel { margin-top: 1rem; }

/* ---- Tables ---- */

/* Wide tables scroll inside their own box rather than pushing the page sideways
   -- the mark-entry grid will lean on this hard. */
.table-scroll {
  overflow-x: auto;
  margin: 0 -0.25rem;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}

th, td {
  padding: 0.55rem 0.5rem;
  text-align: left;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

th {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ink-soft);
}

tbody tr:last-child td { border-bottom: 0; }

.num { text-align: right; }

.inline-toggle {
  display: inline-flex;
  gap: 0.45rem;
  align-items: center;
  margin: 0;
  font-size: 0.9rem;
  font-weight: 400;
}

.inline-toggle input {
  display: inline-block;
  width: auto;
  margin: 0;
}

/* "Set all to", above the students table on class.html (2026-08-18). Two
   dropdowns and the one Save button that goes with them, on a row that wraps on
   a narrow screen rather than pushing the table sideways.

   NOTHING HERE GIVES THE BUTTON A `display`, and that is deliberate rather than
   an omission: it is shown and hidden with the `hidden` attribute, and a
   `display` of our own on it -- or on `.set-all button` -- would beat the
   browser's `[hidden] { display: none }` and leave a Save button that can never
   be put away. That is the same fault as `.header-right[hidden]` and
   `label[hidden]` above, which cost an afternoon each. A hidden flex child is
   removed from the layout altogether, so the `gap` leaves no phantom space
   either. */
.set-all {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
  margin: 0 0 1rem;
  padding: 0.7rem 0.9rem;
  background: var(--page);
  border: 1px solid var(--line);
  border-radius: 6px;
}

/* `select { width: 100% }` at the top of this file is right in a form and wrong
   in a control bar -- the same sentence .filters carries above, and the same
   fix. Without it each dropdown fills its own flex line and the bar stands
   three rows tall on a laptop. Measured rather than eyeballed: 210px before,
   50px after, at 900px wide. */
.set-all select {
  width: auto;
  min-width: 0;
  margin-top: 0;
}

.badge-quiet {
  background: var(--page);
  color: var(--ink-soft);
}

.actions a { margin-left: 0.9rem; font-size: 0.9rem; }
.actions a:first-child { margin-left: 0; }

/* An actions cell mixes links, which navigate, with buttons, which act -- so the
   buttons have to sit on the same line at the same size as the links beside
   them. The admin page sets its own spacing and is more specific, so it keeps
   the tighter gap it already had.

   :first-child matters since 2026-08-07, when the dashboard's class row lost its
   three links and Archive became the first thing in the cell: without it the
   whole column sat indented by a gap meant to separate it from something that
   is no longer there. */
.actions button { margin-left: 0.9rem; font-size: 0.9rem; }
.actions button:first-child { margin-left: 0; }

/* Destructive, and coloured to say so. Only ever on a control that asks before
   it acts -- the colour is a warning, not the safeguard. */
button.link-button.danger { color: var(--error); }
button.link-button.danger:hover:not(:disabled) { color: var(--error); }
button.link-button.danger:disabled { color: var(--ink-soft); }

select {
  display: block;
  width: 100%;
  margin-top: 0.35rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--paper);
  font: inherit;
  font-weight: 400;
}

select:focus {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
  border-color: var(--brand);
}

select:disabled { background: var(--page); color: var(--ink-soft); }

/* ---- Manage-students table (class.html) ----
   Same problem the admin page has: a table of controls rather than of text, so
   the widgets stop behaving like the ones in a form -- no top margin, and sized
   to their content rather than filling the cell.

   Scoped to the table's own class on purpose. Applied globally these would hit
   the mark-entry grid, which is hundreds of text inputs each sized to hold one
   character. */
.students-table td { vertical-align: top; }

.students-table input,
.students-table select { margin-top: 0; }

/* Wide enough for a double-barrelled surname without scrolling inside the box.
   The table can afford it: seven columns at this width still sit inside the
   80rem default, and .table-scroll catches it if a class name pushes past.
   (Six until 2026-08-17, when the tier joined them.) */
.students-table .name-input { min-width: 10.5rem; }

/* Wide enough for one digit and its padding, and no wider: a target grade is a
   single character, and a full-width box invites a sentence. */
.students-table .target-input { width: 3.5rem; text-align: center; }

.students-table select { min-width: 9rem; }

/* The tier overrides that, and narrower rather than wider: its longest option
   is "— not set —" and the class dropdown beside it has to hold a class name.
   Every rem this column does not take is a rem the seventh column does not
   push past the page width. */
.students-table .tier-select { min-width: 0; width: 8rem; }

.students-table .hint { margin-top: 0.3rem; }

/* A hidden Save button is still :first-child, so the gap has to be applied to
   what follows rather than to what precedes -- the same trap that left Remove
   and Put back indented on school-admin.html against a button nobody could see.
   The `a` selectors are kept though nothing in this cell is a link today: the
   Passport link was what put them here on 2026-08-17, and it went with the
   passport on 2026-08-18. A cell holding a row's actions is where the next link
   lands too. */
.students-table .actions button,
.students-table .actions a { margin-left: 0.6rem; }
.students-table .actions button:first-child,
.students-table .actions a:first-child { margin-left: 0; }

/* ---- Admin page ----
   Its schools and staff tables hold controls rather than text, so the widgets
   have to stop behaving like the ones in a form: no top margin, and wide enough
   to show a school name rather than filling the cell.

   Scoped to `main.admin` on purpose. The same rules applied globally would hit
   the mark-entry grid, which is a table of hundreds of text inputs sized to
   hold one character -- a min-width meant for a school dropdown would destroy
   it. */
.admin table select,
.admin table input {
  margin-top: 0;
  min-width: 9rem;
}

.admin .actions button { margin-left: 0.6rem; }
.admin .actions button:first-child { margin-left: 0; }

/* The students table appears on school-admin.html as well as class.html, and
   the rule above it -- every control in an .admin table at least 9rem wide --
   is meant for a school-name dropdown and is wrong for both of that table's
   narrow boxes. min-width beats width, so without these the one-character
   target grade box would be nine times wider than the character in it. */
.admin .students-table .target-input { min-width: 0; width: 3.5rem; }
.admin .students-table .name-input { min-width: 10.5rem; }

/* "Year 10" and nothing longer, so the class name beside it keeps the room. */
.admin table select.year-select { min-width: 0; width: 7rem; }

/* ---- Filters above a long table (school-admin.html) ----
   One row of boxes that narrows the table under it. Laid out as a wrapping flex
   row so it becomes a column on a phone without a breakpoint.

   The form rules at the top of this file make a label a block and an input
   full-width, which is right in a form and wrong here: these sit side by side
   and are sized to what goes in them. */
.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem 1rem;
  margin-bottom: 1.1rem;
}

.filters label {
  margin-bottom: 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.filters input,
.filters select {
  display: block;
  width: 100%;
  min-width: 0;
  margin-top: 0.25rem;
  font-size: 0.9rem;
}

/* Wide enough for a surname, and the same width whether it is an input or a
   select, so the row reads as one set of controls rather than four sizes. */
.filters label { flex: 0 1 11rem; }

/* The Clear button sits on the baseline of the boxes rather than of their
   labels. It is only in the markup while a filter is set -- see
   swapClearButton() -- so nothing is holding this space open when there is
   nothing to clear. */
.filters .link-button { padding-bottom: 0.5rem; }

/* A hidden button is still :first-child, so without this the Remove, Put back
   and Delete buttons sat indented by a gap meant to separate them from a Save
   button nobody can see -- and jumped left the moment a dropdown was changed.
   Same fault the note above :first-child describes, arrived at from the other
   direction.

   Deliberately NOT scoped to .admin, although that is where it was first
   needed: every row-at-a-time table hides its Save button the same way since
   2026-08-09, class.html's students included. Two classes and an attribute beat
   the two-class rules above it, so it wins in all three places without having
   to be repeated in each. */
.actions button[hidden] + button { margin-left: 0; }

/* Used on the admin page to name a database value exactly -- `trial`,
   `expired`, my_school_active() -- where a paraphrase would be a guess about
   what the column says. */
code {
  padding: 0.05em 0.35em;
  border-radius: 4px;
  background: var(--page);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
}

/* ---- RAG cells ----
   Thresholds live in one JavaScript file (a DECISIONS.md choice, so they can
   become configurable later); these are only the colours. Deliberately muted:
   a whole grid of saturated red/green is unreadable, and the percentage stays
   legible on top. Colour is never the only signal -- the number is always
   there, which also covers colour-blind users. */

.rag { text-align: right; font-variant-numeric: tabular-nums; }
.rag-red   { background: var(--rag-red); }
.rag-amber { background: var(--rag-amber); }
.rag-green { background: var(--rag-green); }

/* A cell with nothing behind it -- a topic this student's paper never tested,
   or a student who was absent. Deliberately left uncoloured: no data is not a
   score of nought, and colouring it red would say the opposite. */
.rag-empty { background: repeating-linear-gradient(
  45deg, transparent, transparent 4px, rgba(0, 0, 0, 0.04) 4px, rgba(0, 0, 0, 0.04) 8px); }

/* ---- The RAG analysis grid ----
   Same skeleton as the mark grid: pinned names, pinned header, and as many
   columns as the curriculum needs.

   COLUMNS ARE HEADED BY A SHORT CODE, WITH A KEY UNDERNEATH. That looks like a
   compromise and is actually forced by the content: a topic is one objective
   from the scheme of work written out in full, averaging 62 characters and
   reaching 261 at worst, and a unit name averages 27 but reaches 151. Rotated
   headings were tried first and could show about a third of a typical topic
   name, which is worse than useless -- several topics in a unit start with the
   same few words. So the grid stays compact and legible, and the words live in
   the key below it, where there is room to read them. */

.rag-grid {
  border-collapse: separate;
  border-spacing: 0;
  font-size: 0.9rem;
}

.rag-grid th,
.rag-grid td {
  padding: 0.3rem 0.45rem;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.rag-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--banner);
  vertical-align: bottom;
}

/* Rotated name above, code beneath. Rotation only became worth doing once
   topics had a short_name (2026-08-05): the full objectives averaged 62
   characters, of which a rotated heading showed about a third -- useless when
   several topics in a unit begin with the same words. The short names average
   35 and stop at 45, so the heading is now readable and the code below is just
   the anchor for the key.

   The global `th` rule uppercases and letter-spaces, which suits a word like
   "Student" but not a sentence. */
/* 15.5rem of header looked generous and was chosen from the data: headings run
   to 45 characters, and at that height 83% of them read in full rather than the
   56% that fitted at 12.5rem. The rest are one hover away, and every one is
   spelled out in the key below the grid.

   18.5rem SINCE 2026-08-18, when the unit code joined every question
   description: "(Unit 1a)" is about ten characters, a quarter as much again,
   and most headings lost it off the end. Reported by the developer. The type
   came down with the box -- 0.72rem to 0.66rem, roughly 9% more characters in
   the same space -- so the grid grew by less than the text did. */
.rag-grid .rag-head {
  height: 18.5rem;
  min-width: 2.6rem;
  padding: 0.4rem 0.2rem;
  vertical-align: bottom;
  text-align: center;
  text-transform: none;
  letter-spacing: normal;
}

/* vertical-rl turns the line on its side; the 180deg turn makes it read upwards
   out of the column it labels, rather than downwards into it. */
.rag-grid .rag-head-name {
  display: block;
  max-height: 17rem;
  margin: 0 auto 0.3rem;
  overflow: hidden;
  /* Wraps rather than clips -- see the note on `.qla-grid .q-desc`. */
  white-space: normal;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: 0.66rem;
  font-weight: 400;
  color: var(--ink-soft);
  text-align: left;
}

.rag-grid .rag-head-code {
  display: block;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ink);
}

/* The corner cell, naming the two stacked lines of every column heading. Bottom
   alignment comes from `.rag-grid thead th`, so "Unit"/"Topic" lands level with
   the foot of the rotated names and "Unit Code" level with the codes. */
.rag-grid thead .student-name { text-align: right; }

.rag-grid .rag-labels .rag-name-label {
  display: block;
  margin-bottom: 0.3rem;
  font-size: 0.72rem;
  font-weight: 400;
  color: var(--ink-soft);
}

/* Names are people's names and curriculum is prose: neither should shout. */
.rag-grid tbody .student-name,
.rag-grid tfoot .student-name {
  text-transform: none;
  letter-spacing: normal;
  font-size: 0.9rem;
}

.rag-grid .student-name {
  position: sticky;
  left: 0;
  z-index: 1;
  min-width: 11rem;
  background: var(--paper);
  text-align: left;
  font-weight: 400;
}

/* ---- The three pinned right-hand columns (2026-08-15) ----
   Total, then the whole-number grade, then the grade to two decimal places.
   All three are pinned to the right edge of the scroller, because they are what
   a teacher reads ACROSS from a name on the left -- a grade that scrolled out of
   sight on a 46-column topic grid would be a column nobody could use.

   THE OFFSETS AND THE WIDTHS ARE ONE FACT WRITTEN TWICE AND MUST AGREE. Each
   column is pinned past the width of the ones to its right, so `right` on the
   Total is the two grade columns added up. That is only safe because these three
   widths are FIXED by the min-widths below: the widest thing in a grade column
   is its own heading, "Grade", measured at 3.74rem, so 4rem is a width the
   content cannot exceed and the arithmetic cannot drift. Widen a heading here
   and the offsets must be widened with it, or the columns will overlap. */
.rag-grid .row-total {
  position: sticky;
  right: 8rem;
  z-index: 1;
  min-width: 4.5rem;
  background: var(--paper);
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.rag-grid .row-grade {
  position: sticky;
  z-index: 1;
  min-width: 4rem;
  background: var(--paper);
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.rag-grid .grade-whole   { right: 4rem; }
.rag-grid .grade-decimal { right: 0; }

/* A grade is a quieter fact than the mark it comes from: the Total is what the
   teacher checks against the mark grid, and three bold columns in a row would
   make the right-hand edge shout over the cells the grid is actually about. */
.rag-grid tbody .row-grade,
.rag-grid tfoot .row-grade { font-weight: 400; color: var(--ink-soft); }

.rag-grid tbody .grade-whole { font-weight: 600; color: inherit; }

.rag-grid thead .student-name,
.rag-grid thead .row-total,
.rag-grid thead .row-grade { z-index: 3; background: var(--banner); }

/* ---- The school's own grade columns (2026-08-31) ----
   Two more pinned columns -- the school grade and its decimal -- drawn only
   when the school has set its own boundaries for the grid's tier. Five pinned
   columns at the old widths would spend 20rem of every viewport, so when the
   `school-grades` class is on the table the five HEADINGS ROTATE (the .rot
   span, the same writing-mode trick as the question headings) and the four
   grade columns narrow to 3rem.

   THE OFFSETS AND THE WIDTHS ARE ONE FACT WRITTEN TWICE AND MUST AGREE, the
   same rule as the three-column stack above: each column is pinned past the
   widths of the ones to its right. A rotated heading takes no width, and the
   widest cell content is a decimal like "5.96", well inside 3rem. The Total
   column keeps 4.5rem -- in marks mode it can read "168/240". */
.rag-grid.school-grades .row-grade            { min-width: 3rem; }
.rag-grid.school-grades .school-grade-decimal { right: 0; }
.rag-grid.school-grades .school-grade-whole   { right: 3rem; }
.rag-grid.school-grades .grade-decimal        { right: 6rem; }
.rag-grid.school-grades .grade-whole          { right: 9rem; }
.rag-grid.school-grades .row-total            { right: 12rem; }

.rag-grid.school-grades thead .row-total,
.rag-grid.school-grades thead .row-grade { vertical-align: bottom; }

.rag-grid.school-grades thead .rot {
  display: inline-block;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  font-size: 0.78rem;
  line-height: 1.2;
  padding: 0.25rem 0;
}

/* The school pair is the quieter half of the stack: the official whole grade
   keeps its weight (the rule above), the school whole grade sits between it
   and the decimals. */
.rag-grid.school-grades tbody .school-grade-whole { font-weight: 600; color: var(--ink-soft); }

/* ---- One paper after another on the question grain (2026-08-15) ----
   A banner row above the question headings naming each paper, and a tint on the
   headings under it so the eye can see where one paper ends without reading
   anything. Only drawn with two or more papers: with one there is nothing to
   tell apart, and the subheading above the grid already names it.

   THE BAND'S HEIGHT AND THE DETAIL ROW'S OFFSET ARE ONE FACT WRITTEN TWICE and
   must agree, the same trap as the pinned right-hand columns above. The band is
   one line of small text and is given an explicit height so the arithmetic
   cannot drift; the row under it is pushed down by exactly that much, because
   both rows are sticky and would otherwise print through each other. Change the
   height and change the offset with it. */
.rag-grid .paper-band th {
  top: 0;
  height: 1.6rem;
  padding: 0.15rem 0.4rem;
  /* LEFT, not centred, and it was centred first. A banner spanning twenty-five
     questions puts its centred title in the middle of a run that is wider than
     the screen, so scrolling into a paper showed a colour with no name on it.
     Against the start of its run, the title is where the eye already goes when
     the colour changes. */
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  font-size: 0.78rem;
}

.rag-grid thead .head-detail th { top: 1.6rem; }

.rag-grid .paper-band-cell { border-left: 2px solid var(--line); }

/* Six tints, cycled. `paper-tint-0` is the ordinary heading colour, because the
   first paper keeps it and only the papers after it are tinted -- which also
   leaves a single-paper grid exactly as it was. The tints are pale on purpose:
   they sit behind heading text that has to stay readable, and they are grouping
   marks rather than a scale with a meaning. */
.rag-grid .paper-tint-0 { background: var(--banner); }
.rag-grid .paper-tint-1 { background: #e4eef7; }
.rag-grid .paper-tint-2 { background: #eaf3e8; }
.rag-grid .paper-tint-3 { background: #f6eee2; }
.rag-grid .paper-tint-4 { background: #efe8f4; }
.rag-grid .paper-tint-5 { background: #e3f1f1; }

/* The class average is the row a teacher looks for, so it stays on screen at
   the bottom of the scroller the way the header stays at the top.

   TWO ROWS SINCE 2026-08-15 -- the class average and the Edexcel averages -- so
   the same offset arithmetic applies here as at the top and on the right: the
   lower row sits at `bottom: 0` and the one above is pushed up by exactly its
   height, which is fixed below rather than left to the select inside it. */
.rag-grid tfoot th,
.rag-grid tfoot td {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--banner);
  border-top: 2px solid var(--line);
  font-weight: 600;
}

/* (Three rules stood here letting the class-average row be colour-coded like any
   other: the banner background on `.rag-grid tfoot td` out-specifies a bare
   `.rag-green`, so without them the row came out uncoloured. Removed on
   2026-08-16, when the developer asked for the average rows to be uncoloured on
   both screens -- which is what `analysis.html`'s has always been, and for the
   reason that row now carries: it sits under thirty coloured rows and a band
   there competes with the cells above instead of summarising them. Nothing in
   either tfoot is banded any more, so these selected nothing.

   THE SPECIFICITY TRAP THEY EXISTED FOR IS STILL REAL. Anything that ever wants
   colour inside a `<tfoot>` here needs a rule of its own, because the sticky
   banner background will otherwise win -- which is why this note is longer than
   the deletion. Relative performance sidesteps it entirely by painting inline.) */

/* BOTH pinned columns must out-stack the ordinary footer cells. The rule above
   puts every tfoot cell on z-index 2, which is above the .row-total's own
   z-index of 1 -- so without this the class-average cells scrolled *over* the
   pinned Total column and printed question averages underneath its heading.
   The same applies on the left, where the student-name column is pinned. */
.rag-grid tfoot .student-name,
.rag-grid tfoot .row-total,
.rag-grid tfoot .row-grade { z-index: 3; }

/* ---- The Edexcel averages row (2026-08-15) ----
   The national mean mark per column, for all candidates or for one grade,
   chosen by the dropdown in its own row label. DELIBERATELY UNCOLOURED: every
   other coloured row here judges somebody on the grid, and this one is the
   national picture — the comparison it exists for is reading it against the
   class average directly above.

   Its height is FIXED, and that is what the class-average row's `bottom` offset
   above is measured against. The select is sized to fit inside it. */
.rag-grid .national-row th,
.rag-grid .national-row td {
  height: 2.3rem;
  background: var(--page);
  color: var(--ink-soft);
  font-weight: 400;
}

/* PUSHED UP ONLY WHERE THERE IS SOMETHING TO BE PUSHED UP BY. Written without
   any condition for a few hours on 2026-08-15, when this row was the only tfoot
   on the site that had a neighbour -- so every OTHER grid was pinned 2.3rem
   clear of the bottom of its scroller, leaving a strip below the class average
   with the last student's row sliding under it. Reported on analysis.html, which
   is the grid that had no Edexcel row at the time.

   The lesson is the one the pinned right-hand columns carry: an offset is
   measured against something, and a rule that states the measurement without
   stating the condition applies it where there is nothing to measure.

   SAID WITH `:last-child` RATHER THAN `:has()` (2026-08-16, after a code
   review). The condition was `tfoot:has(.national-row)`, which is correct and
   which SILENTLY DROPS THE WHOLE RULE on any engine without `:has()` --
   Safari before 15.4, which is exactly the older school iPad a pilot is likely
   to meet. Both footer rows then pin to 0 and print over each other, which is
   the bug this rule exists to prevent, reappearing where nobody would look for
   it. There is no fallback for an unsupported selector: it matches nothing.

   The rule stated this way needs no support beyond `:last-child` and no class
   set from JavaScript, and it is also the truer statement of the intent -- the
   BOTTOM row of the footer sits on the floor, and anything above it clears that
   row's height. A footer of one row is its own last child and pins at 0, which
   is what analysis.html wanted all along. */
.rag-grid tfoot tr:last-child th,
.rag-grid tfoot tr:last-child td { bottom: 0; }

.rag-grid tfoot tr:not(:last-child) th,
.rag-grid tfoot tr:not(:last-child) td { bottom: 2.3rem; }

/* THE FLEX IS ON A SPAN INSIDE THE CELL, NOT ON THE CELL, and that is the whole
   of a bug fixed on 2026-08-16. It was on the `<th>` itself, which stops the
   element being a table cell: the table then wraps it in an anonymous cell, and
   a `position: sticky` box whose containing block is that anonymous cell has no
   scroll range to stick within — so this label scrolled sideways out of view
   while the class average label beside it, an ordinary cell, stayed pinned.

   Chrome keeps it working, which is why it survived a walk in one browser and
   was reported from another. Do not move `display` back onto `.student-name`
   here: that cell is pinned by `.rag-grid .student-name { position: sticky }`
   further up, and any `display` of ours on it takes the pinning away. Same
   family as the `[hidden]` traps on `.header-right` and `label` — our own
   `display` quietly beating something the browser was doing for us. */
.rag-grid .national-row .national-row-label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

.rag-grid .national-label { font-size: 0.8rem; }

/* The dropdown, wherever it is drawn: in the row label on a grid, and in a
   COLUMN HEADING on the four summary panels that gained the same average as a
   column (2026-08-15). One class rather than two selectors, so the control looks
   the same in all six places -- which is the point of it being one function in
   national.js. */
.national-select {
  min-width: 4rem;
  padding: 0.05rem 0.2rem;
  font-size: 0.78rem;
}

/* In a heading it goes on its own line under the words, and it has to undo the
   `th` rule's uppercasing and letter-spacing -- which apply to the <select>'s
   own text as well as to the heading's. */
thead .national-select {
  display: block;
  margin-top: 0.25rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.rag-grid tbody tr:hover .student-name { background: var(--page); }
.rag-grid td.rag { min-width: 3.4rem; text-align: right; }

/* ---- Several classes on one grid (2026-08-14, piece 4) ----
   topic-analysis.html?classes=a,b,c draws a <tbody> per class: a band naming
   it, its students, then its own average row.

   These three rules sit AFTER `.rag-grid .student-name`, and that placement is
   load-bearing rather than tidy. Both selectors are one class and one element
   deep, so they carry the same specificity and the later one wins -- put these
   above it and the pinned first column keeps its white background, leaving the
   class name and its average sitting in a band that stops at the second
   column. */

/* A sticky cell must stay OPAQUE or the rows scrolling underneath show through
   it, which is why the background is repeated on the first cell rather than set
   once on the row. Same reasoning as the tfoot rules above. */
.rag-grid .group-head th,
.rag-grid .group-head td {
  background: var(--banner);
  border-top: 2px solid var(--line);
  font-weight: 600;
}

/* A class's own average, at the foot of its group. Deliberately NOT sticky --
   only the grid-wide row in the tfoot is, and a screen with four classes on it
   would otherwise pin four rows to the bottom and leave no grid between them. */
.rag-grid .group-total th,
.rag-grid .group-total td {
  border-top: 1px solid var(--line);
  font-weight: 600;
}
.rag-grid .group-total .student-name { background: var(--banner); }

/* One row per class: every row in the body is an aggregate. */
.rag-grid .class-row th,
.rag-grid .class-row td { font-weight: 600; }

/* The Analysis panel's buttons and the sentence beside them (school-admin.html),
   on one line with a real gap rather than the accidental space between inline
   elements. Nothing ever sets `hidden` on this element -- if anything ever does,
   this `display` will beat the browser's `[hidden]` rule and silently do
   nothing, which is the trap `.header-right` and `label` both carry a rule for
   further up this file. */
.analysis-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

/* ---- The question-by-question QLA grid ----
   Built on .rag-grid (both classes are on the table), so it inherits the pinned
   name column, pinned header and sticky class-average row. What it adds is the
   description above each question.

   These headings CAN be rotated, where the topic grid's could not: a question
   description averages 41 characters against a topic's 62, and the class summary
   table directly below carries every description in full -- so the rotated text
   is a reminder, not the only copy. */

.qla-grid .q-head {
  /* 13rem until 2026-08-18, when the unit code joined every description --
     "Order positive and negative integers (Unit 1a)" -- and about ten
     characters more than this box was measured for meant MOST headings lost
     their new bracket off the end. Reported by the developer.

     THE BOX GREW BY LESS THAN THE TEXT DID, because the type came down with it:
     0.72rem to 0.66rem is roughly 9% more characters in the same space, so the
     height only has to find the rest.

     SIZED BY MEASURING rather than by eye: a 45-character description with its
     unit runs to 267px of rotated line, and 17rem of box is 272 -- the same
     capacity the curriculum grid gets, so there is one number to reason about
     rather than two. A description longer than that still clips, as the longest
     always have; the column key below the grid and the hover on every heading
     both carry the name in full. */
  height: 19.5rem;
  padding: 0.4rem 0.2rem;
  vertical-align: bottom;
  text-align: center;
  text-transform: none;
  letter-spacing: normal;
}

/* vertical-rl turns the line on its side; the 180deg turn makes it read upwards
   out of the column it labels, rather than downwards into it. */
.qla-grid .q-desc {
  display: block;
  max-height: 17rem;
  margin: 0 auto 0.3rem;
  overflow: hidden;
  /* WRAPPING, WHICH IS WHY EVERY HEADING NOW SHOWS IN FULL (2026-08-18, the
     developer). These spans inherit `white-space: nowrap` from the global
     `th, td` rule at the top of this file, so a heading longer than the box was
     never wrapped -- it was CLIPPED, and the tail of it simply vanished.
     Overriding that is what the added height above is for.

     IN A VERTICAL WRITING MODE THE HEIGHT LIMITS THE LINE and extra lines stack
     SIDEWAYS, so a wrapped heading is taller in the reading direction and wider
     on the page -- and it costs nothing here, because a second line is about
     13px against a column that is already 2.4-2.6rem wide at its narrowest.
     `overflow: hidden` stays as the backstop for a single word longer than the
     whole box, which nothing in the data is. */
  white-space: normal;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: 0.66rem;
  font-weight: 400;
  color: var(--ink-soft);
  text-align: left;
}

/* The labels for the three stacked lines in each question heading. They sit in
   the corner cell, right-aligned so they finish hard against the first column
   of data they describe -- the same idea as the mark grid's "Question/Marks"
   corner. Bottom alignment comes from `.rag-grid thead th`, so the three labels
   line up with the three lines they name. */
.qla-grid thead .student-name { text-align: right; }

.qla-grid .q-desc-label {
  display: block;
  margin-bottom: 0.3rem;
  font-size: 0.72rem;
  font-weight: 400;
  color: var(--ink-soft);
}

.qla-grid .q-number { display: block; font-weight: 600; color: var(--ink); }
.qla-grid .q-max {
  display: block;
  font-size: 0.72rem;
  font-weight: 400;
  color: var(--ink-soft);
}

.qla-grid td.qla {
  min-width: 2.4rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* The summary tables under the grid. The description column is the wide one and
   is the only thing here allowed to wrap. */
.summary-table .q-ref {
  width: 5rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.summary-table tfoot td {
  border-top: 2px solid var(--line);
  font-weight: 600;
}

/* ---- Relative performance (2026-08-16) ----
   This row's mark minus the Edexcel average on show. It carries its OWN
   background, painted inline from a scale whose midpoint is zero rather than
   fifty, so it deliberately overrides the row colour behind it — every other
   column in these tables shares one reading and this one does not.

   The left border is what says so: it separates the two columns that are about
   the country from the ones that are about the class, so a reader does not take
   the colour here for the same colour as the row. */
td.relative, th.relative {
  border-left: 2px solid var(--line);
  white-space: nowrap;
}

td.relative { font-variant-numeric: tabular-nums; }

/* ---- "Your view of this screen" (2026-08-16) ----
   The panel at the foot of both analysis screens: reset the controls, save them
   as your own defaults, forget them again. Quieter than the panels above it,
   because it is about the screen rather than about the marks -- a teacher
   looking for it will find it, and one reading their class's analysis should not
   have it competing with the grid. */
.prefs-panel { background: var(--page); }
.prefs-panel h2 { font-size: 1rem; }

.prefs-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.75rem 0;
}

/* A key and the sentence beside it, on one wrapping line -- the shape the
   gradient key below has always had, given its own name on 2026-08-16 when the
   two summary panels went from the fade to 75/50 bands and needed the swatches
   there instead of the bar. */
.rag-key-line {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: center;
  font-size: 0.8rem;
}

/* THE GRADIENT KEY IS STILL USED, and by one thing: nothing on these screens
   fades any more except the Relative performance column, whose scale is a
   DIFFERENCE with amber at level rather than a share of the marks with amber at
   halfway. The bar below therefore no longer describes anything on screen and
   is kept only because `gradient-key-bar` is the honest illustration if that
   column is ever given a key of its own. The three custom properties are the
   same ones the JavaScript reads, so a key built from it cannot show a
   different palette from the cells. */
.gradient-key {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: center;
  font-size: 0.8rem;
}

/* Bar and its numbers are one stacked unit, so the numbers stay under the bar
   however the row wraps. */
.gradient-key-scale {
  display: inline-flex;
  flex-direction: column;
  gap: 0.15rem;
  width: 12rem;
}

.gradient-key-bar {
  height: 0.85rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: linear-gradient(to right,
    var(--rag-red), var(--rag-amber), var(--rag-green));
}

.gradient-key-labels {
  display: flex;
  justify-content: space-between;
  color: var(--ink-soft);
}

.analysis-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  align-items: center;
  margin-top: 0.5rem;
}

.analysis-controls label { margin-bottom: 0; font-size: 0.9rem; }
.analysis-controls select { display: inline-block; width: auto; margin-left: 0.35rem; }

/* The summary tables' sort control sits directly above the table it orders,
   rather than in a row of controls at the top of the page, so it needs the gap
   underneath that the other controls get from the panel heading above them. */
.sort-controls { margin-bottom: 0.75rem; }

.rag-key {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.rag-key-swatch {
  display: inline-block;
  width: 1.1rem;
  height: 0.85rem;
  border: 1px solid var(--line);
  border-radius: 3px;
}

/* The key: what each column code means. Multi-column on a wide screen because
   a 46-topic list read as one tall column would push the grid off the page,
   and it collapses to one column on a phone. */
.column-key { columns: 22rem auto; column-gap: 3.5rem; margin: 0.75rem 0 0; font-size: 0.85rem; }

.column-key-toggle {
  font-weight: 600;
  cursor: pointer;
}

/* ---- The assessment picker (topic-analysis.html) ----
   The heading and the two controls are always visible; only the list of
   individual assessments folds. It was one <details> around the lot until
   2026-08-13, which hid the two things a teacher changes most often behind a
   disclosure arrow.

   A bare <details> rather than `.panel collapsible`, because it sits INSIDE the
   header panel and a panel drawn within a panel reads as a mistake. Same
   reasoning as the column key above, which is bare for the same reason. */
.assessment-picker-head h3 {
  margin: 0;
  font-size: 1rem;
}

.assessment-picker { margin: 0.75rem 0 0.5rem; }

/* Columns rather than one tall list: a class with a full mock series runs to a
   dozen assessments, and a single column pushes the grid off the screen before
   the teacher has chosen anything. Collapses to one column on a phone by
   itself, the same way .column-key does. */
.picker-list {
  columns: 20rem auto;
  column-gap: 2.5rem;
  margin: 0.75rem 0;
}

/* break-inside because a row split across two columns puts the tick-box at the
   foot of one and its label at the head of the next. */
.picker-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  break-inside: avoid;
  margin-bottom: 0.45rem;
}

/* The checkbox is a hair lower than the cap-height of the text beside it. */
.picker-row input { margin-top: 0.15rem; }

/* (`.inline-toggle-off` stood here until 2026-08-15 -- a greyed, disabled
   inline toggle with a sentence beside it saying why. Its only user was the
   picker's "Include practice assessments" box, which is gone, and a rule
   nothing selects is a thing the next reader has to work out the status of.
   Same judgement as the exports removed from assessment-select.js with it.) */

/* Flex rather than a hanging indent: a negative text-indent pulls the code
   outside the column box, where the panel's padding clips it. */
.column-key div {
  display: flex;
  gap: 0.6rem;
  break-inside: avoid;
  margin-bottom: 0.4rem;
}

.column-key .key-code {
  flex: 0 0 2.8rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The per-student list puts a topic name in a table cell, and a topic name is a
   whole scheme-of-work objective -- 62 characters on average and 261 at worst.
   The shared `th, td` rule is nowrap, which would drag the table several screens
   wide, so this column wraps and is given a sane maximum. */
.key-name {
  white-space: normal;
  min-width: 18rem;
  max-width: 42rem;
}

/* Preview only: the same grid dropped onto the lesson cyan for comparison. */
.rag-on-cyan { background: var(--lesson-cyan); }
.rag-on-cyan th { color: var(--ink); }
.rag-on-cyan td, .rag-on-cyan th { border-bottom-color: rgba(0, 0, 0, 0.15); }

.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.swatches span {
  display: block;
  width: 100%;
  height: 44px;
  margin-bottom: 0.35rem;
  border: 1px solid var(--line);
  border-radius: 5px;
}

.swatches > div { width: 7.5rem; }

/* Status pills in the CSV import preview. */
.pill {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border-radius: 4px;
  font-size: 0.75rem;
  font-weight: 600;
}

.pill-new       { background: var(--rag-green); color: #14421a; }
.pill-choose    { background: var(--info-bg);   color: var(--info); }
.pill-in-class  { background: var(--page);      color: var(--ink-soft); }
.pill-error     { background: var(--error-bg);  color: var(--error); }

/* The preview table is read carefully, so let long notes wrap rather than
   forcing the whole table sideways. */
/* Typing students in by hand. Collapses to one field per line on a phone,
   which is the case it exists for. */
.manual-row {
  display: grid;
  /* Five columns since 2026-08-17: the tier joined them. A grid template that
     names fewer columns than the row contains does not error -- the extra one
     is placed in an implicit column of its own -- so the Remove button would
     simply have wandered onto a second line with nothing to say why. */
  grid-template-columns: 1fr 1fr 8rem 9rem auto;
  gap: 0.75rem;
  align-items: end;
  margin-bottom: 0.5rem;
}

.manual-row label { margin-bottom: 0; }
.manual-row .remove-row { padding-bottom: 0.6rem; white-space: nowrap; }

@media (max-width: 40rem) {
  .manual-row {
    grid-template-columns: 1fr;
    padding-bottom: 0.75rem;
    border-bottom: 1px solid var(--line);
  }
  .manual-row .remove-row { justify-self: start; padding-bottom: 0; }
}

#preview td:nth-child(5) { white-space: normal; min-width: 13rem; }

/* Wide enough to read "Move into this class (leave 11X)" without truncation,
   but no wider -- every rem here is a rem the Action column costs on a laptop. */
#preview .choice { width: auto; min-width: 11rem; max-width: 15rem; margin: 0; }

/* A row still waiting for a decision, after the teacher has tried to import. */
#preview .choice.needs-choice,
#preview .m-middle-name.needs-choice {
  border-color: var(--error);
  box-shadow: inset 0 0 0 1px var(--error);
}

/* The middle-name box that appears under "create a new student", where the new
   child would otherwise be indistinguishable from one already on the system.
   Sits under its dropdown rather than beside it, so the Action column does not
   grow for the forty rows that will never show it. */
#preview .middle-prompt {
  display: block;
  margin: 0.4rem 0 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

#preview .m-middle-name { width: auto; min-width: 11rem; max-width: 15rem; margin: 0.15rem 0 0; }

.badge {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.1rem 0.4rem;
  border-radius: 4px;
  background: var(--info-bg);
  color: var(--info);
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* ---------------------------------------------------------------------------
   Student login codes (class-codes.html) and the slips that get printed
   --------------------------------------------------------------------------- */

/* The code in the on-screen table. Monospace so a teacher reading one out can
   tell the characters apart -- the alphabet already leaves out O/0 and I/1/L,
   and a fixed-width font finishes the job. */
.join-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

/* The slips exist in the page all along but are only ever seen by the printer.
   Kept out of the flow rather than display:none, because a printed element has
   to have been laid out. */
.slips { display: none; }

.slip {
  border: 1px dashed #666;
  padding: 0.7cm;
  text-align: center;
  break-inside: avoid;          /* never let a slip straddle two sheets */
}

.slip p { margin: 0 0 0.25rem; }
.slip-name { font-size: 1.1rem; font-weight: 600; }
.slip-class { color: #444; font-size: 0.85rem; }
.slip-instruction { margin-top: 0.5rem; font-size: 0.85rem; }

.slip-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.4rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  margin: 0.4rem 0;
}

.slip-footer { font-size: 0.75rem; color: #444; }

/* ---- Practice questions (practice.html, 2026-08-19) ----
   ------------------------------------------------------------------
   The maths input, the keypad and the verdict. Nothing else in the app uses
   these, and practice.html is the only page that loads MathQuill.

   NOTHING BELOW GIVES A `display` TO ANYTHING SHOWN AND HIDDEN WITH THE
   `hidden` ATTRIBUTE -- the two buttons and `.practice-note`. That is the
   `.header-right[hidden]` / `label[hidden]` / `.set-all button` rule, which has
   cost an afternoon each of the three times it was got wrong: our own `display`
   beats the browser's `[hidden] { display: none }` and the element can never be
   put away. `.practice-actions` is a flex CONTAINER, which is safe -- a hidden
   flex child is removed from the layout, gap and all. */

.practice-skill {
  margin: 0 0 0.25rem;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* WHETHER A CALCULATOR IS ALLOWED (2026-08-24). A real paper says this once on
   its front cover; a practice question has no front cover, so it says it here,
   on every question. It is deliberately NOT `.muted`: it changes how a child
   should work, so it must not read as small print beside the skill name. */
.practice-calculator {
  margin: 0 0 0.75rem;
  font-size: 0.9rem;
  font-weight: 600;
}

.practice-calculator.calculator-forbidden { color: var(--danger, #a3243b); }
.practice-calculator.calculator-allowed { color: var(--ink-soft, #4a5568); }

.practice-question {
  font-size: 1.25rem;
  line-height: 1.6;
  margin: 0 0 1rem;
}

.practice-figure:not(:empty) { margin: 0 0 1rem; }
.practice-figure svg { max-width: 100%; height: auto; }

.practice-note { margin: 0 0 0.75rem; font-size: 0.9rem; }

/* ---- The answer box ----
   THE VENDORED MATHQUILL HARD-CODES TWO ACTIVEPRESENTER SLIDE WIDTHS, AND BOTH
   ARE UNDONE HERE. Found by measuring the rendered field in a browser, not by
   reading the file -- the first one is obvious and the second is the one that
   was actually winning.

     mathquill.css:16   .mq-editable-field              800px
     mathquill.css:366  .mq-math-mode,
                        .mq-math-mode .mq-editable-field  460px

   An AP slide is a fixed pixel canvas, so fixed widths were right there. Here
   the first forces a 375px phone to scroll sideways past a box twice the width
   of the screen -- and the SECOND is worse than it looks, because
   `mathquill.js:3388` puts `.mq-math-mode` on **StaticMath** as well as on an
   editable field. Every rendered fraction inside a question stem is a StaticMath
   span, so without this every `2^5` and every `rac{1}{2}` in a question would
   be a 460px-wide centred block sitting in the middle of a sentence.

   The overrides are HERE rather than in the vendored file, which is left
   byte-identical to the developer's proven build so a future diff against it
   shows the truth. See vendor-README.md. The vendor stylesheet is loaded BEFORE
   this one (practice.html) so these win on the cascade -- the selectors are
   deliberately the same, not more specific, because out-specifying a vendor rule
   is a habit that stops working the moment the vendor adds an id. */
.mq-editable-field,
.mq-math-mode,
.mq-math-mode .mq-editable-field {
  min-width: 0;
  max-width: 100%;
}

/* Inline maths inside a question sits in the sentence rather than on a line of
   its own -- `.mq-math-mode` also carries `text-align: center`, which centres a
   fragment in whatever box it is given. */
.practice-question .mq-math-mode,
.practice-verdict .mq-math-mode {
  display: inline-block;
  text-align: left;
  vertical-align: middle;
}

/* ---- UK RECURRING-DECIMAL NOTATION: A DOT ON THE FIRST AND LAST DIGIT ----
   ------------------------------------------------------------------------
   Added 2026-08-21 at the developer's request, and it closes a MARKING fault
   rather than a cosmetic one.

   The vendored `\dot` command draws ONE dot as a block-level band above its
   whole block (`mathquill.js:3994`, `.mq-dot-recurring` in the vendor
   stylesheet, `text-align: center; height: 0.3em`). Over a single digit that is
   right. Over a block of several it centres the dot over the MIDDLE character,
   so `0.\dot{874}` rendered as a dot above the 7 -- which is not notation at
   all, and means nothing.

   THAT MATTERED BECAUSE THE PARSER READ IT CORRECTLY. `parseRecurring()` in
   answer-check.js reads a multi-digit `\dot` block as the whole repeating run,
   so `0.\dot{874}` was marked right while showing a child something that was
   wrong. **The developer's rule: do not mark correct an answer that is not
   displaying the correct notation on screen.** The honest fix is to make the
   display right, not to refuse the answer -- a child produces that form by
   pressing the dot key once and typing three digits, which the keypad allows.

   MEASURED, NOT GUESSED. With `0.\dot{874}` at 36.8px the vendor's single dot
   sat 18px to the RIGHT of the first digit's centre; the band itself sits at
   `top: -0.3em` and is `0.3em` tall, which is where these dots are put so they
   land exactly where a correctly-typed `\dot{8}` already puts one.

   IT IS DONE HERE RATHER THAN IN THE VENDORED FILE, which stays byte-identical
   to the developer's proven build -- the rule set out above and in
   vendor-README.md. `.mq-empty-box` appears in exactly one place in the whole
   vendored build (the `\dot` template), so these selectors cannot reach
   anything else. */
.mq-math-mode .mq-dot-recurring {
  /* `visibility` rather than `display`, so the 0.3em the band reserves above the
     line is still reserved and the dots below have somewhere to sit. */
  visibility: hidden;
}

.mq-math-mode .mq-dot-recurring-inner > .mq-empty-box > span {
  position: relative;
}

/* `mq-dot-first` and `mq-dot-last` are put on by markRecurringDots() in
   mathquill-keypad.js, NOT by `:first-child` / `:last-child`, and that is not
   fussiness -- it is the fault this rule shipped with for an hour.
   **In an EDITABLE field MathQuill puts its CURSOR inside the block**, a
   zero-width span among the digits, so `:last-child` is the caret and the last
   dot landed on it: typing 8, 7, 4 into a dot block gave a dot over the 8 and a
   dot over thin air. It was invisible to a check that rendered StaticMath, which
   has no cursor, and showed up the moment the keypad was driven for real.
   For a one-digit block both classes land on the same element, so it gets one
   dot. An EMPTY block gets one too: that is the state right after the key is
   pressed, and a dot with nothing under it is the cue that says "type the digits
   that recur". */
.mq-math-mode .mq-dot-recurring-inner > .mq-empty-box > .mq-dot-first::before,
.mq-math-mode .mq-dot-recurring-inner > .mq-empty-box > .mq-dot-last::before,
.mq-math-mode .mq-dot-recurring-inner > .mq-empty-box.mq-empty::before {
  content: '\02D9';
  position: absolute;
  left: 0;
  right: 0;
  top: -0.3em;
  height: 0.3em;
  text-align: center;
  visibility: visible;
  pointer-events: none;
}

.math-input {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.6rem 0.75rem;
  margin: 0 0 0.75rem;
  overflow-x: auto;
}

/* A REAL BORDER AND A WHITE GROUND, for the reason the child's mark-entry boxes
   got one on 2026-08-19: a borderless input is legible only where something else
   is drawing the box. There is no ruled table here -- just a field on a page of
   white -- so the box has to draw itself, or the child cannot find where to type. */
.math-field {
  display: block;
  min-height: 2.4rem;
  font-size: 1.3rem;
}

.math-field-done { opacity: 0.75; }

/* ---- The keypad ----
   Buttons with text on them. The ActivePresenter version of this was 43 PNG
   images and 5.1MB of resources; this is a few hundred bytes of CSS, scales to
   any screen, and can be read by a screen reader. */
.keypad {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0 0 1rem;
  max-width: 26rem;
}

/* EVERY ROW IS THE SAME 60-COLUMN GRID, and 60 is the lowest common multiple of
   the keypad's three row shapes: the 0-9 block is three keys across (span 20),
   the operations row four across (span 15), and every other row five across
   (span 12). One number here rather than three sets of widths that have to be
   kept in step, and all three shapes align to the same edges and to each other.

   IT WAS FLEX UNTIL 2026-08-20 AND THAT IS WHAT WAS WRONG WITH IT: `flex: 1`
   stretches a row to fill, so a set holding one key became a single button the
   width of the whole keypad. H-03am stacked FOUR of them -- the recurring dot,
   the fraction, `=` and the letter x -- pushing the question off a phone screen.
   A grid row simply leaves the unused columns empty, which is what "three keys
   the same size as all the others" requires. */
.keypad-row {
  display: grid;
  grid-template-columns: repeat(60, 1fr);
  gap: 0.4rem;
}

/* 3rem tall is a comfortable finger target (the WCAG 2.5.5 guidance is 44px;
   3rem is 48px at the 16px root this app uses). The WIDTH comes from the grid,
   so there is no `min-width` here: on a grid item a min-width larger than its
   track does not shrink, it overflows, and the keypad would scroll sideways on
   a phone rather than fit it. */
.keypad-key {
  min-width: 0;
  min-height: 3rem;
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 1.1rem;
  font-weight: 500;
  cursor: pointer;
}

.keypad-key:hover:not(:disabled) {
  background: var(--lesson-cyan);
  color: var(--ink);
}

.keypad-key:active:not(:disabled) {
  background: var(--lesson-cyan-deep);
  color: var(--ink);
}

/* Algebraic letters are set in italic serif, which is how they appear in the
   question and in the answer field -- a keypad offering an upright sans-serif
   `x` for a variable is telling a child the wrong thing about notation. */
.keypad-key-letter {
  font-family: "Times New Roman", Times, serif;
  font-style: italic;
  font-size: 1.2rem;
}

/* The three widths the layout uses, and each fills the slot a missing neighbour
   would have taken: a digit is one of three across, `0` takes its own slot and
   the empty one beside it (the calculator shape, and what replaced the second
   minus button), and `clear` takes the two the fourth and fifth edit keys would
   have had. */
.keypad-key { grid-column: span 12; }        /* five across, the default */
.keypad-key-op { grid-column: span 15; }     /* four across, the operations row */
.keypad-key-digit { grid-column: span 20; }  /* three across, the 0-9 block */
.keypad-key-zero { grid-column: span 40; }   /* 0, taking the slot beside it */
.keypad-key-wide { grid-column: span 24; font-size: 0.9rem; }  /* clear */

/* ---- A choice answer: tick every statement that is correct (2026-08-21) ----
   Generous hit areas, because six statements on a phone is six chances to miss;
   the whole row is the label, so a tap anywhere on the sentence ticks it. */
.choice-list {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
}

.choice-item {
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--paper);
  margin: 0 0 0.5rem;
}

.choice-item label {
  display: flex;
  gap: 0.7rem;
  align-items: baseline;
  padding: 0.7rem 0.85rem;
  margin: 0;
  cursor: pointer;
}

/* `width: auto` UNDOES `input { width: 100% }` at the top of this file, which is
   right in a form and wrong on a tick-box -- the checkbox took the whole row and
   pushed every statement out past the edge of its own box. Third time this
   stylesheet has met that rule from the wrong side; the note above `.filters`
   says the same thing about `select`. */
.choice-item input {
  width: auto;
  flex: none;
  margin: 0;
}

.choice-text { flex: 1; }

/* After submitting. The verdict goes on EVERY statement, not only the ones the
   child ticked: this question's whole content is knowing which sentences say
   something true, so being told "wrong" without being told which is which
   teaches nothing. `choice-missed` is the one they got wrong either way. */
.choice-correct { border-color: var(--success); background: var(--success-bg); }
.choice-wrong { border-color: var(--line); }
.choice-missed { box-shadow: inset 3px 0 0 var(--error); }

.choice-verdict {
  font-size: 0.85rem;
  font-weight: 600;
  white-space: nowrap;
}

.choice-correct .choice-verdict { color: var(--success); }
.choice-wrong .choice-verdict { color: var(--ink-soft); }

.choice-item input:disabled + .choice-text { opacity: 1; }

/* Why a statement is not acceptable. On its own line under the statement,
   because several of these are refused for reasons a child cannot see from the
   words -- "should have used 1000x" is wrong because 1000x − x is still wrong. */
.choice-why {
  display: block;
  margin: 0.35rem 0 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

/* Small, quiet, and under the lines rather than beside them: it is a way out of
   running short of room, not a thing to do. */
.math-add-line {
  font-size: 0.85rem;
  padding: 0.35rem 0.7rem;
  margin: 0 0 0.85rem;
}

/* ---- Actions and the verdict ---- */
.practice-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0 0 1rem;
}

.practice-verdict:not(:empty) {
  border-radius: 6px;
  padding: 0.7rem 0.9rem;
  margin: 0 0 0.75rem;
}

.practice-verdict p { margin: 0; }

.practice-verdict-right { background: var(--success-bg); color: var(--success); }
.practice-verdict-wrong { background: var(--error-bg); color: var(--error); }
.practice-verdict-blank { background: var(--info-bg); color: var(--info); }
/* A reveal is NOT a wrong answer: the child asked to be shown, and the red box
   would say "you tried and failed" about a question they chose not to answer.
   Same neutral colours as the blank prompt, its own class so a check (or a
   later restyle) can tell the two apart. */
.practice-verdict-revealed { background: var(--info-bg); color: var(--info); }

.verdict-mark { font-size: 1.2rem; font-weight: 700; margin-right: 0.3rem; }

/* THE PROBABILITY CAUTION (the developer, 2026-08-27) -- the only place in the
   product where a CORRECT answer carries a warning. It sits inside
   `.practice-verdict-right`, whose colours it inherits, so it reads as part of
   the same green box rather than as a second verdict; the rule above it and the
   slightly smaller type are what stop "you are right" and "but be careful" being
   read as one sentence. `margin-top` overrides the `p { margin: 0 }` above,
   which exists so a one-line verdict sits tight in its box. */
.practice-verdict .verdict-caution {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid currentColor;
  font-size: 0.9rem;
  opacity: 0.95;
}

.practice-score { margin: 0; font-size: 0.9rem; }


/* ---- The worked solution (SOL-1..4, 2026-08-30) ----
   ------------------------------------------------------------------
   A collapsed expander after the verdict; open when the child pressed
   "I don't know". The PROSE is in the handwriting face and the MATHS is
   standard MathQuill in both styles -- the whole point of the voice is that
   the working looks exactly like working. `.practice-solution-host` is hidden
   with the `hidden` ATTRIBUTE and deliberately gets no rule here (see the
   note above `.practice-skill`).

   `--solution-hand`: KALAM IS THE PICK (the developer, 2026-08-30, off the
   rendered sample at `tools/browser-checks/solution-fonts.html` -- SOL-1),
   AND IT IS VENDORED the same day: `app/vendor/kalam/` holds Kalam-Regular
   and Kalam-Bold from the google/fonts repository with their OFL.txt (SIL
   Open Font License -- see vendor-README.md at the root). The app loads no
   font from Google's servers; these two faces ship with the site.
   `font-display: swap` so a slow font never blanks the prose -- the
   fallbacks show until it arrives. */
@font-face {
  font-family: 'Kalam';
  src: url('../vendor/kalam/Kalam-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Kalam';
  src: url('../vendor/kalam/Kalam-Bold.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

.practice-solution {
  border: 1px solid var(--line);
  border-radius: 6px;
  margin: 0 0 0.75rem;
  --solution-hand: 'Kalam', 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', cursive;
}

.practice-solution summary {
  cursor: pointer;
  padding: 0.6rem 0.9rem;
  font-weight: 600;
}

.solution-body { padding: 0 0.9rem 0.75rem; }

.solution-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

.solution-head .solution-caption { margin: 0; font-size: 0.85rem; }

.solution-style-toggle { font-size: 0.8rem; padding: 0.25rem 0.6rem; }

.solution-part-label { margin: 0.75rem 0 0.25rem; font-weight: 600; }

.solution-step { margin: 0.5rem 0 0; }

.solution-say {
  margin: 0;
  font-family: var(--solution-hand);
  font-size: 1.05rem;
  line-height: 1.5;
}

/* SOL-1's one-tap print style: prose drops to the ordinary face; the maths
   was never in the handwriting face to begin with. */
.solution-plain .solution-say { font-family: inherit; font-size: 1rem; }

.solution-write { margin: 0.25rem 0 0 1.25rem; }

/* ---- A step's DIAGRAM (SOL-8, 2026-08-30) ----
   The same SVG a part's `figure` carries, in the same place in the cascade as
   `.practice-figure` — the developer's first full read of the retrofit batch
   rejected ten of thirteen solutions and EVERY rejection was a picture this
   step shape could not carry. `max-width: 100%` is what keeps a labelled
   compound shape inside a phone-width card. */
.solution-figure { margin: 0.5rem 0 0 1.25rem; }
.solution-figure svg { max-width: 100%; height: auto; }

/* ---- STACKED COLUMN ARITHMETIC (SOL-9, 2026-08-30) ----
   ------------------------------------------------------------------
   The developer's ruling: an HTML TABLE OF MathQuill CELLS. Every cell is its
   own `data-math` span, so `renderMathIn` draws it with exactly the MathQuill
   the child types with; the horizontal rule is a CSS BORDER; and the alignment
   — terms under terms, equals sign under equals sign — falls out of the table.
   Nothing was added to the vendored MathQuill: its own `\begin{matrix}` was
   measured DEAD through this same path first.

   `border-spacing: 0` with the SEPARATE model — not `collapse` — is what makes
   the rule one unbroken line: each bordered cell paints its own bottom border
   and they butt together with no gap. Both models draw the same line, and this
   one is the one a CHECK CAN MEASURE. Under `collapse`, Chrome's
   `getComputedStyle` reports a resolved half-border on the neighbouring cells
   and on the gutter, so two true statements — that the rule stops at the
   gutter, and that the row above carries none — both measured FALSE against a
   screen that was drawing them correctly (measured 2026-08-30, and the screen
   was looked at before the CSS was changed). A rule nothing can measure is a
   rule that will be quietly lost.

   The rule deliberately stops at the operator gutter, which is where the
   papers stop it.

   The wrapper scrolls rather than the card: a long division on a phone must
   not push the whole question sideways (the standing rule about wide content). */
.solution-columns-wrap {
  margin: 0.4rem 0 0 1.25rem;
  overflow-x: auto;
}

.solution-columns {
  /* SHRINK-TO-FIT, EXPLICITLY. The stylesheet's own `table { width: 100% }`
     (the data grids, near the top) applies here too and stretched the cells
     across the whole card — "36a  +  6b  =  186" spread over 450px, which
     stops looking like column arithmetic at all. Measured on the specimen
     page before this line existed; the browser check beside it pins the
     table narrower than the body it sits in. */
  width: auto;
  border-collapse: separate;
  border-spacing: 0;
  /* Column arithmetic is set in the maths face, not the handwriting one — the
     working must look exactly like working, in both styles. */
  font-family: inherit;
}

.solution-columns td {
  padding: 0.1rem 0.22rem;
  text-align: right;
  vertical-align: middle;
  white-space: nowrap;
  border: 0 solid currentColor;
}

/* The gutter: the `-` of a column subtraction, the `\times` of a column
   multiplication, the divisor of a bus stop. Always present, empty or not —
   it is what holds every row's cells in the same columns. */
.solution-columns td.solution-columns-op {
  text-align: center;
  padding-right: 0.5rem;
}

/* The bar UNDER a row. */
.solution-columns-ruled td:not(.solution-columns-op) {
  border-bottom-width: 2px;
}

/* The bus-stop frame (METHOD-3): a bar OVER the cells and a vertical rule
   between the divisor in the gutter and them. */
.solution-columns-bracket td:not(.solution-columns-op) {
  border-top-width: 2px;
}

.solution-columns-bracket td.solution-columns-op + td {
  border-left-width: 2px;
  padding-left: 0.42rem;
}


/* ---- Working lines and the mark scheme (2026-08-19) ----
   ------------------------------------------------------------------
   A practice question is a direct copy of a real exam question, so a three-mark
   proof needs somewhere to show working and the mark scheme has to be readable
   afterwards. NOTHING HERE GIVES A `display` TO ANYTHING HIDDEN WITH THE `hidden`
   ATTRIBUTE -- `.practice-working`, `.practice-answer`, `.practice-scheme` and the
   three buttons are all shown and hidden that way. See the note above
   `.practice-skill`. */

.math-lines { display: flex; flex-direction: column; gap: 0.5rem; }

.math-line-label {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-right: 0.4rem;
}

.practice-working-prompt,
.practice-answer-label { margin: 0 0 0.4rem; font-size: 0.9rem; }

/* ---- Parts, answer fields and cells (2026-08-21) ----
   A question with (a), (b) and (c) is three blocks under one stem, one keypad
   and one Submit -- marked together, the way the paper marks it. */
.practice-part + .practice-part {
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}
/* The part letter sits with its question rather than above it, so (a) reads as
   labelling the words beside it and not the whitespace over them. */
.practice-part-label {
  margin: 0 0 0.2rem;
  font-weight: 600;
}
/* The shared stem: given once, above every part it belongs to. */
.practice-intro { margin-bottom: 0.75rem; }
.practice-intro[hidden] { display: none; }

/* On a multi-part question each part carries its own verdict, because a total
   at the foot cannot say WHICH part went wrong. */
.practice-part-verdict { margin: 0.5rem 0 0; }

/* Several answer boxes on one part -- "work out x and y", a coordinate. Laid
   out in a row that wraps, so two short boxes sit side by side on a phone and
   four do not run off the edge. */
.answer-field + .answer-field { margin-top: 0.5rem; }
.answer-cell { display: flex; align-items: center; gap: 0.4rem; }

/* A SINGLE ANSWER FILLS THE LINE (2026-08-24, the developer). `.answer-cell` is
   a flex row and a flex item sizes to its CONTENT, so the one box on a question
   collapsed to 39px inside a 638px row -- and the empty MathQuill field inside
   it to 13px, which is a 13px target on a phone.

   `flex: 1` on the box and nothing else: the field is `display: block` inside a
   stretching flex column, so it fills the box once the box fills the row. Widths
   on `.mq-editable-field` itself do NOT work and were measured not to -- the
   field was never the constrained thing, its ancestor was.

   SEVERAL boxes stay snug, which is why this is a modifier and not a change to
   `.answer-cell`: `values` puts "x =" and "y =" side by side and a full-width
   box each would stop the row reading as a row. The papers draw it the same way.
   The label is excluded so it keeps its own width and the box takes the rest --
   "x = ______", as the papers print it. */
.answer-cell-sole > div:not(.answer-cell-label) { flex: 1; }
.answer-cell + .answer-cell { margin-top: 0.4rem; }
.answer-cell-label { font-size: 0.9rem; white-space: nowrap; }

/* ---- A TABLE OF GIVEN DATA IN A STEM (2026-08-22) ----
   Built by `app/js/stem-table.js`, which owns the markup; this block owns the
   look. The two must be read together -- that is the whole point of there being
   a module rather than a `<table>` inside one generator.

   THE GREY WAS MEASURED, NOT PICKED. `Exam Papers/2017 Nov/2017 Nov Found
   P2.pdf` page 9 at 300 dpi: every rule in the cricket club's table is
   rgb(168, 169, 173) at about 1pt. One value for every stem table in the
   product, because the developer's ruling is that it "would be consistent
   across all papers" -- so it is a custom property and there is one place to
   change it.

   THREE THINGS HERE ARE UNDOING THE GLOBAL TABLE RULES rather than adding
   anything, and they are the reason this block is longer than it looks:
   `th, td` at the top of this file sets a bottom border, left alignment and
   `white-space: nowrap`, and bare `th` sets small uppercase soft-coloured type
   for a data grid. A question paper prints none of that -- headings are bold,
   full size and normal case -- so every one has to be said again. */

/* THE SCROLLER, and it is not belt-and-braces. `max-width: 100%` on a TABLE is
   capped by its MINIMUM CONTENT WIDTH -- a table will not lay out narrower than
   its cells' longest unbreakable content however small the max-width -- so a
   two-column table with "Membership fees" down one side measures 306px and does
   not fit a 300px column, and what it does instead is put a sideways scroll on
   the child's whole page. Same answer as every other wide table here.

   It costs nothing where the table fits: the table keeps `margin: 0 auto`, so it
   is centred and no scrollbar appears. The vertical margin lives HERE rather
   than on the table, this being the box in normal flow. */
/* THE UNITS FIELD (2026-08-24). A dropdown where a real paper prints the unit on
   its answer line, so it sits INLINE beside the answer box rather than under it.
   `width: auto` is load-bearing: the global `select { width: 100% }` this
   stylesheet carries for forms would otherwise make it fill the row, which is
   the same global-rule trap the stem table hit on 2026-08-22 and the control bar
   on 2026-08-18. */
.units-field {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-left: 0.4rem;
}

.units-select {
  width: auto;
  min-width: 5.5rem;
  padding: 0.25rem 0.4rem;
}

/* The correct unit, shown beside the child's choice when they got it wrong.
   Nothing is shown when they were right — the tick already says so. */
.units-answer {
  font-size: 0.9rem;
  color: var(--danger, #a3243b);
}

/* A DIAGRAM THAT COULD NOT BE DRAWN (2026-08-25). The generator's underlay
   threw and the canvas fell back to plain graph paper, so the question may not
   match the picture. Amber rather than red: nothing the child did is wrong. */
.draw-underlay-failed {
  margin: 0.5rem 0 0;
  padding: 0.5rem 0.7rem;
  border-left: 3px solid var(--warn, #b8860b);
  background: var(--warn-bg, #fdf6e3);
  font-size: 0.9rem;
}

/* THE DECISION FIELD (2026-08-25). `[ Yes/No ] because [ box ]`, built by the
   `decision` branch of generator-runtime.js.

   IT WRAPS, and that is deliberate rather than incidental: at 375px a dropdown,
   the word "because" and a maths box do not fit one line, and the alternative to
   wrapping is a sideways scroll on a child's phone. `align-items: baseline` so
   the connector sits on the same line as the words either side of it rather
   than floating against the taller select. */
/* ---------------------------------------------------------------------------
   THE WRITTEN ANSWER  (marking-rules.md rule 11, 2026-08-27)
   ---------------------------------------------------------------------------
   A plain textarea, sized like the working boxes beside it rather than like a
   form field, because on this kind the answer IS prose and a one-line box tells
   a child to write one line. `width: 100%` with `box-sizing` inherited from the
   reset, so it cannot put a sideways scroll on a phone -- the fault the stem
   table shipped on 2026-08-22. */
.words-answer {
  width: 100%;
  min-height: 4.5rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--line, #c8ccd4);
  border-radius: 4px;
  background: #fff;
  font: inherit;
  font-size: 1rem;
  line-height: 1.5;
  resize: vertical;
}

.words-answer:focus {
  outline: 2px solid var(--focus, #2b6cb0);
  outline-offset: 1px;
}

/* A marked question is frozen, and a read-only box must LOOK read-only or a
   child goes on typing into it. */
.words-answer[readonly] {
  background: var(--surface-muted, #f4f5f7);
  color: var(--ink-muted, #4a5162);
}

.decision-field {
  /* BLOCK-LEVEL FLEX, not inline-flex, and the difference is the whole of the
     2026-08-24 answer-line ruling: an inline-flex box sizes to its CONTENT, so
     `flex: 1` on the reason box has no free space to take and the box stays at
     its minimum however wide the screen. The units field beside it is
     `inline-flex` correctly — it sits BESIDE an answer box, where this field IS
     the answer row. */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
}

.decision-select {
  width: auto;
  min-width: 5.5rem;
  padding: 0.25rem 0.4rem;
}

.decision-because {
  color: var(--ink-soft, #555);
}

/* THE REASON BOX MUST GROW, and this is the 2026-08-24 answer-line ruling
   arriving on a new field. A flex item sizes to its CONTENT, and an EMPTY
   MathQuill field is 13px wide — measured at 375px on the real generator before
   this rule existed, which is a 13px typing target on a phone. `flex: 1` makes
   it take the rest of the row; `min-width` stops it collapsing below a
   thumb-width when the row wraps. */
.decision-reason {
  flex: 1;
  min-width: 8rem;
}

/* The whole correct answer, shown when either half was wrong. */
.decision-answer {
  font-size: 0.9rem;
  color: var(--danger, #a3243b);
}

/* THE SCAFFOLD'S WARNING, printed whether or not the child was right. It gets a
   line of its own because it is about the EXAM rather than about this answer,
   and reading it as a comment on their mark would be the wrong lesson. */
.decision-exam-note {
  flex-basis: 100%;
  margin-top: 0.35rem;
  font-size: 0.85rem;
  color: var(--ink-soft, #555);
}

/* THE FORMULA BOX (2026-08-24). Built by `app/js/formula-box.js`, which owns the
   markup; this block owns the look. A real paper rules a plain box round the
   formulae a question is allowed to be given, sets it apart from the stem, and
   keeps it narrow -- so it reads as something HANDED to the child rather than as
   part of what they are being asked. Deliberately not `.muted`: it is given
   information the child needs, not small print. */
.formula-box {
  display: inline-block;
  max-width: 100%;
  margin: 0.75rem 0;
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--line, #c9ced6);
  border-radius: 4px;
  background: var(--surface-soft, #f7f8fa);
}

.formula-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* The name and the maths sit on one line where there is room and stack on a
   phone, which is what keeps a long label from squeezing the notation. */
.formula-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
}

.formula-name { font-size: 0.9rem; }

.stem-table-scroll {
  max-width: 100%;
  overflow-x: auto;
  margin: 1.25rem 0;
}

.stem-table {
  --stem-table-line: #a8a9ad;
  border-collapse: collapse;

  /* `width: auto` UNDOES THE GLOBAL `table { width: 100% }`, and without it the
     `auto` margins have nothing to centre -- a full-width table is already
     against both edges. Found by measuring: the table came out at exactly its
     container's 1186px with the margins resolving to zero, which looks like the
     centring rule not being written rather than being overruled. A question
     paper's table is as wide as its contents and no wider. */
  width: auto;
  max-width: 100%;
  margin: 0 auto;                /* centred; the space above and below is on
                                    the scroller, which is the box in flow */

  /* DELIBERATELY THE PAGE'S BASE SIZE AND NOT THE STEM'S 1.25rem. The stem is
     set large for reading on a phone, which is larger than a paper's body text;
     at that size a two-column table with this padding runs past 375px and starts
     wrapping cells mid-phrase, which is worse than a table a step smaller. */
  font-size: 1rem;
}

.stem-table caption {
  /* The accessible name, and deliberately not on screen: the sentence above the
     table already says what it shows, and printing it twice is not what the
     paper does. Clipped rather than `display: none`, which would take it out of
     the accessibility tree as well and leave the table unnamed. */
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.stem-table th,
.stem-table td {
  border: 1px solid var(--stem-table-line);
  padding: 0.45rem 0.9rem;
  text-align: center;
  white-space: normal;
  font-size: inherit;
  text-transform: none;
  letter-spacing: normal;
  color: inherit;
  vertical-align: middle;
}

.stem-table th { font-weight: 700; }
.stem-table th[scope='row'] { text-align: left; }

/* THE LAST ROW KEEPS ITS BOTTOM LINE, and this rule is here to undo another of
   this file's global data-grid rules: `tbody tr:last-child td { border-bottom: 0 }`
   near the top, which is how a list of rows is meant to end -- ruled between its
   entries and open at the foot. A question paper's table is a closed BOX.

   **IT NAMES `td` AND NOT `th`, WHICH IS WHY THE FAULT LOOKED SO ODD**: on the
   bottom row the row LABEL kept its line and the value beside it lost one, so
   what the developer saw (2026-08-22) was the line missing under the second
   column alone. A whole missing edge would have read as a rule nobody wrote;
   half of one reads as a rendering glitch.

   Second time a global table rule has quietly beaten this block, after
   `table { width: 100% }` defeating the centring -- so the browser checks now
   sweep all four edges of every cell rather than sampling one. */
.stem-table tbody tr:last-child td { border-bottom: 1px solid var(--stem-table-line); }

/* THE BLANK CORNER IS UNDRAWN, which is read off the paper rather than assumed:
   where a table has both a header row and row labels, its top-left cell has no
   borders at all. */
.stem-table .stem-table-corner { border: 0; }

/* A cell holding several sub-items, each with its own value -- the cricket
   club's Quiz row. Two columns inside the one cell, labels left and values
   centred under each other, which is how the paper sets it. */
.stem-table-lines {
  display: grid;
  grid-template-columns: auto auto;
  gap: 0.1rem 1.75rem;
  justify-content: center;
}

.stem-table-line-label { text-align: left; }
.stem-table-line-value { text-align: center; }
.stem-table-line-full { grid-column: 1 / -1; text-align: center; }

/* ---- A TABLE ANSWER (2026-08-22) ----
   "Complete the table of values", a frequency table, a two-way table. Some
   cells are PRINTED and some are blank, and the two must be told apart at a
   glance or the child fills in the question.

   IT SCROLLS SIDEWAYS INSIDE ITS OWN BOX rather than widening the page. A table
   of values runs to six or seven columns and a phone is 375px, so the choice is
   between a scroller and a table nobody can read; `.table-scroll` is the same
   device every grid in the product uses.

   THE GLOBAL `th, td { white-space: nowrap }` IS WANTED HERE, unlike on the
   analysis grids, because every cell holds a number or a short heading. */
.answer-table-scroll { overflow-x: auto; }
/* NOT `.table-scroll`, WHICH IS THE WRONG DEVICE HERE though it is the one every
   other grid in the product uses. That class carries `margin: 0 -0.25rem`, a
   deliberate bleed so a full-width panel's table reaches the panel edge -- and
   it makes the scroller 8px WIDER than its parent, which is a sideways scroll on
   a 375px phone rather than a fix for one. Same negative margin that put the
   printed student sheet 2mm over a page on 2026-08-18 and cost a whole second
   sheet; found here by measuring the page at 375px, not by reading the rule. */

/* `width: auto` because the global `table { width: 100% }` would stretch three
   columns across a phone and put an answer box in the middle of nowhere. A
   table of values is as wide as its numbers. */
.answer-table { border-collapse: collapse; margin: 0.25rem 0; width: auto; }
.answer-table caption {
  caption-side: top;
  text-align: left;
  font-size: 0.9rem;
  padding-bottom: 0.3rem;
}
.answer-table th,
.answer-table td {
  border: 1px solid var(--line);
  padding: 0.35rem 0.5rem;
  text-align: center;
  vertical-align: middle;
}
.answer-table th { background: var(--banner); font-weight: 600; }

/* A PRINTED CELL IS PART OF THE QUESTION and reads as ordinary text; an input
   cell carries the box the child types in. The distinction is the whole of what
   makes this a table rather than a row of answer boxes, so it is drawn rather
   than left to the presence of a cursor. */
.answer-table .table-given { display: inline-block; padding: 0 0.2rem; }
.answer-table td:has(.table-input) { background: var(--paper); padding: 0.2rem; }
.answer-table .table-input .math-input { margin: 0; }

/* THE BOX MUST BE VISIBLE AGAINST WHITE (the developer, 2026-08-26). MathQuill's
   field is `border: 0` and transparent, which is right where something ELSE is
   drawing the box -- the teacher's ruled mark grid -- and wrong here: a child
   could not tell an answer cell from a printed one without clicking about. Same
   rule as `.mark-input` on the student's own form, 2026-08-24, and the same
   reason.

   `min-width` came DOWN from 3.5rem with it. At 135px an answer cell the table
   measured 617-740px against a 375px screen; the developer's ruling is that a
   scroll on a phone is acceptable and the desktop view is not to be compromised
   for it, so this is sized for the content -- three characters -- rather than
   against the phone. */
.answer-table .table-input .math-field {
  min-width: 2.75rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: #fff;
  padding: 0.1rem 0.2rem;
}
.answer-table .table-input .math-field.mq-focused {
  border-color: var(--brand);
  box-shadow: 0 0 0 2px var(--brand-hover);
}

/* After marking. The right answer is written into the cell that got it wrong,
   under what the child typed, because a list of six numbers under the grid is a
   puzzle about which column each one belongs to. */
.answer-table .table-cell-wrong { background: var(--error-bg); }
.answer-table .table-answer {
  display: block;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

/* WHY WORKING IS WORTH WRITING (2026-08-21). Under the Stage 4 marking rules a
   correct answer scores full marks on its own, so a child could reasonably
   decide working is pointless -- this is the sentence that says when it pays.
   Smaller and quieter than the prompt above it: the prompt is an instruction,
   this is a reason, and it should not compete with the box it sits over.
   `hidden` works on it because nothing here gives it a `display`. */
.practice-working-why { margin: -0.2rem 0 0.5rem; font-size: 0.8rem; }

/* The answer box is separated from the working above it, or on a question with
   both the child cannot see which box is which. */
.practice-answer { margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--line); }

/* ---- The mark scheme ----
   Read AFTER answering, so it is quiet rather than prominent: a bordered panel in
   the working-area grey rather than the cyan a live control would take. */
.practice-scheme {
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--banner);
  padding: 0.9rem 1rem;
  margin: 0 0 1rem;
}

.practice-scheme h3 { margin: 0 0 0.3rem; font-size: 1rem; }
.practice-scheme > p { margin: 0 0 0.75rem; font-size: 0.9rem; }

/* ---- The child asks a person to look (2026-08-24) ----
   Under the mark scheme, because that is the order the decision is made in: a
   child reads why the marks were given and only then decides the marker was
   wrong. Quiet by default -- it is an invitation and not a call to action, and
   a child who has just been marked down should not be nudged towards
   disputing it.

   NOTHING HERE GIVES A `display` TO `.practice-query`, which the runtime shows
   and hides with the `hidden` attribute. See the note above `.practice-skill`
   and the two occasions a `display` silently defeated it (`.header-right`,
   2026-08-10; `label[hidden]`, 2026-08-14). */
.practice-query { margin: 0 0 1rem; }

.practice-query-prompt { margin: 0 0 0.4rem; font-size: 0.9rem; }

/* FULL WIDTH AND A REAL BORDER. This sits on a page of white with nothing else
   drawing a box round it -- the student-marks.html lesson of 2026-08-19, where
   borderless inputs were legible only because a ruled table drew the box and
   the developer "could only find them by clicking at random places". */
.practice-query-note {
  width: 100%;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  padding: 0.5rem;
  resize: vertical;
}

.practice-query-actions {
  margin: 0.5rem 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.practice-query-error:empty { display: none; }
.practice-query-error {
  margin: 0.4rem 0 0;
  font-size: 0.9rem;
  color: var(--error);
}

/* SENT. Deliberately not the success green every other confirmation takes:
   nothing has gone right yet -- a person has been asked, and the answer may
   still be that the marker was correct. */
.practice-query-sent {
  margin: 0;
  padding: 0.6rem 0.8rem;
  border-radius: 6px;
  background: var(--info-bg);
  color: var(--info);
  font-size: 0.9rem;
}

.scheme-list { list-style: none; margin: 0; padding: 0; }

/* Three columns on a laptop, stacked on a phone: the code, the scheme's wording,
   and what happened to the mark. */
.scheme-item {
  display: grid;
  grid-template-columns: 4.5rem 1fr auto;
  gap: 0.5rem 0.75rem;
  align-items: start;
  padding: 0.6rem 0;
  border-top: 1px solid var(--line);
}

.scheme-item:first-child { border-top: 0; }

/* An AWARDED mark is tinted, and deliberately in the same green as a correct
   answer elsewhere -- a mark earned is a mark earned, however it was found. */
.scheme-item-awarded { background: var(--success-bg); }

.scheme-code {
  font-weight: 700;
  font-size: 0.85rem;
  color: var(--ink-soft);
  white-space: nowrap;
}

.scheme-text { font-size: 0.9rem; line-height: 1.45; }
.scheme-mark { font-size: 0.85rem; white-space: nowrap; }
.scheme-got { color: var(--success); font-weight: 600; }

/* The tick-box a child claims a mark with. A real label wrapping the input, so
   the words are part of the target -- this is pressed with a thumb. */
.scheme-mark label {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  cursor: pointer;
  min-height: 2.2rem;
}

.scheme-mark input[type="checkbox"] { width: 1.15rem; height: 1.15rem; }

/* A partly-earned question is neither the green of full marks nor the red of
   none, and it is the commonest outcome on a three-mark proof. */
.practice-verdict-part { background: var(--info-bg); color: var(--info); }

@media (max-width: 34rem) {
  .scheme-item { grid-template-columns: 1fr; gap: 0.3rem; }
  .scheme-code { font-size: 0.8rem; }
}

/* On a phone the question and the answer want the room, so the keys lose a
   little height. The WIDTHS need nothing: the grid divides whatever it is given
   by 15, so the keypad fits any screen by construction and cannot wrap. */
@media (max-width: 26rem) {
  .keypad-key { min-height: 2.75rem; font-size: 1rem; }
  .keypad-row { gap: 0.3rem; }
  .practice-question { font-size: 1.15rem; }
}


/* ===========================================================================
   PRACTICE QUERIES -- the teacher's queue of disputed practice marks
   (practice-queries.html, 2026-08-22)
   =========================================================================== */

/* One dispute. Bordered rather than panelled, because several sit INSIDE one
   panel and a panel within a panel reads as two levels of grouping where there
   is one. */
.query-card {
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 1rem;
  margin: 0 0 1rem;
}

.query-card:last-child { margin-bottom: 0; }

.query-head h3 { margin: 0; font-size: 1.05rem; }
.query-head p { margin: 0.15rem 0 0.75rem; font-size: 0.85rem; }

/* THE CHILD'S OWN WORDS, and they come first because they are the thing being
   answered -- the maths below is the evidence, not the question. */
.query-note {
  margin: 0 0 1rem;
  padding: 0.6rem 0.9rem;
  border-left: 3px solid var(--info);
  background: var(--info-bg);
  border-radius: 0 4px 4px 0;
  font-size: 0.95rem;
}

/* The question and what they wrote, side by side on a laptop so the comparison
   is one glance, stacked on a phone. */
.query-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin: 0 0 1rem;
}

.query-tag {
  margin: 0 0 0.3rem;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.query-question, .query-response { min-width: 0; }
.query-question p { margin: 0 0 0.4rem; }
.query-figure { margin: 0.5rem 0; }
.query-figure svg { max-width: 100%; height: auto; }

.query-answer { margin: 0 0 0.6rem; font-size: 0.95rem; }

/* Numbered, because a teacher reading a dispute refers to "line 2" -- and the
   child's lines are what the mark scheme is being checked against. */
.query-working {
  margin: 0;
  padding-left: 1.6rem;
  font-size: 0.95rem;
}

.query-working li { margin: 0 0 0.25rem; }

.query-card code {
  background: var(--banner);
  border-radius: 3px;
  padding: 0.05rem 0.3rem;
}

/* What the engine gave. Stated plainly and NOT coloured: every other coloured
   thing in the product judges somebody, and this is the number under dispute. */
.query-marks { margin: 0 0 0.75rem; }

.query-resolved { margin: 0 0 0.5rem; font-size: 0.95rem; }

.query-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
}

.query-actions label {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  font-size: 0.95rem;
}

/* NOT `.mark-input`, DELIBERATELY, and this is the student-marks.html lesson of
   2026-08-19 applied before it could bite twice. That class is
   `border: 0; background: transparent` because in the mark grid the ruled table
   IS the border and sixty boxed cells would be a wall of lines. There is no
   table here -- one box on a card of white -- so a borderless input is a box
   nobody can find. A borderless input is legible only where something else is
   drawing the box. */
.query-mark-input {
  width: 3.5rem;
  margin: 0;
  padding: 0.35rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #fff;
  text-align: center;
  font: inherit;
}

.query-mark-input:focus {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

.query-error { color: var(--error); font-size: 0.9rem; }

/* The four facts that regenerate the question exactly. Small and last: a teacher
   never needs them, and the one time somebody does -- reproducing a disputed
   question in the review tool -- they need every character. */
.query-provenance {
  margin: 0.75rem 0 0;
  font-size: 0.75rem;
  word-break: break-word;
}

/* The accepted forms, under the scheme's own wording. Indented so the eye reads
   scheme-line then what-it-takes, rather than two sentences of equal weight. */
.scheme-expect {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

@media (max-width: 48rem) {
  .query-body { grid-template-columns: 1fr; }
}

/* The drawing canvas (draw-canvas.js, 2026-08-23)
   ------------------------------------------------------------------
   Built by `app/js/draw-canvas.js`, which owns the markup; this block owns
   the look. The SVG surface is PAPER and stays white in any theme -- ink on
   graph paper is what the exam gives, and red/blue ink on a dark surface
   would change what the child is looking at, not just how it looks.

   `touch-action: none` on the surface is load-bearing, not styling: without
   it a finger drawing a curve scrolls the page instead, and the canvas is
   unusable on the one device most students have. */
.draw-canvas { max-width: 100%; }

.draw-stage { position: relative; }

.draw-surface {
  display: block;
  width: 100%;
  height: auto;
  background: #ffffff;
  border: 1px solid var(--line);
  border-radius: 6px;
  touch-action: none;
  cursor: crosshair;
}

/* The toolbar buttons are the SECONDARY button shape at toolbar size -- the
   global `button` rule paints everything brand blue at input-field height,
   which would make ten tools louder than the question they serve. The active
   tool is marked by aria-pressed, which the module keeps true, so the state
   the child sees and the state the code holds cannot drift. */
.draw-tools { margin-bottom: 0.5rem; }

.draw-tool-group {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0 0.7rem 0.4rem 0;
  vertical-align: top;
}

.draw-tracing-controls {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.draw-tool-group button {
  padding: 0.3rem 0.6rem;
  font-size: 0.82rem;
  font-weight: 500;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 5px;
}

.draw-tool-group button:hover:not(:disabled) {
  background: var(--brand-hover);
  color: var(--ink);
}

.draw-tool-group button[aria-pressed="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}

/* The ink swatches show their actual ink -- a button reading "red" in the
   interface colour would be a small lie about what comes out of the pen. */
.draw-ink-swatch {
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background: var(--ink-colour);
  margin-right: 0.35rem;
  vertical-align: -0.05rem;
}

.draw-ink[aria-pressed="true"] {
  background: var(--page);
  border-color: var(--ink-colour);
  border-width: 2px;
  color: var(--ink);
}

/* Frozen after marking: the drawing is evidence now, and the surface says so
   the way a disabled input does. Pointer events are refused in the module;
   this is only the look. */
.draw-canvas-disabled .draw-surface { cursor: default; opacity: 0.92; }

/* The readout floats above the pointer -- above, because on a phone the
   finger is exactly on top of the coordinate it most needs to see. */
.draw-readout {
  position: absolute;
  transform: translateX(-50%);
  padding: 0.1rem 0.45rem;
  background: var(--ink);
  color: #fff;
  font-size: 0.8rem;
  border-radius: 4px;
  pointer-events: none;
  white-space: nowrap;
}

/* Printing this page means printing the slips and nothing else: the header,
   the table and the buttons are all screen furniture, and a teacher who has to
   trim them off by hand will print the page once and never again.

   Two per row on A4 portrait, which gives a slip big enough to read the code
   off and small enough that a class fits on three or four sheets. */
@media print {
  .slips {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5cm;
  }
}

/* ---------------------------------------------------------------------------
   The student join page
   --------------------------------------------------------------------------- */

/* A form with one field in it should not stretch to the width of a laptop. */
.panel.narrow { max-width: 32rem; }

/* Matches the printed slip: same monospace, same spacing, so what the student
   is copying and what they are typing look like the same thing. */
.code-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.2rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* ---------------------------------------------------------------------------
   The school picker — school-join.html and admin.html
   ---------------------------------------------------------------------------
   One widget on two screens since 2026-08-09: js/school-picker.js. A teacher
   naming their school, and the site admin adding one. */

/* The results sit under the box and are replaced on every keystroke, so they
   need a little air of their own -- without it the first row reads as part of
   the input's own hint text. */
.picker-results { margin-top: 0.6rem; }

/* On admin.html the picker is followed by the form it fills in, and the two
   want a visible seam between them. On school-join.html the panel ends after
   the results, so the margin costs nothing there. */
.picker-results:not(:empty) { margin-bottom: 1rem; }

/* A list of things to pick, one per line, with a generous hit area. The rows
   are buttons rather than links because nothing navigates: choosing a school
   fills in a form, or opens a panel further down the same page. */
.pick-list {
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
  max-height: 26rem;
  overflow-y: auto;
}

.pick-list li {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}

.pick-list li:last-child { border-bottom: none; }

/* The name is the thing being chosen, so it carries the weight; the local
   authority and postcode beside it are there to tell two schools of one name
   apart, not to be read first. */
.pick-list .link-button {
  font-weight: 600;
  text-align: left;
}


/* ---------------------------------------------------------------------------
   Printing, and PDF (2026-08-18, piece 2)
   ---------------------------------------------------------------------------

   PDF IS window.print(). There is no library and no server-side rendering: the
   browser's own "Save as PDF" is on every machine a pilot will use, it needs no
   dependency in a project that deliberately has none, and it gives a teacher the
   printer as well for nothing.

   THE PAGE BUILDS WHAT IT PRINTS, into `.print-area`, and the rules below reveal
   that and hide the screen. It is NOT a stylesheet laid over the live DOM, and
   the reason is decisive rather than stylistic: THIRTY STUDENTS' SHEETS DO NOT
   EXIST IN THE DOM. Those screens draw either the class grid or one child, so no
   stylesheet can print what was never rendered. Splitting a wide grid into
   column chunks is a DOM operation too.

   The drift that risks is answered by building the printed tables from the SAME
   functions the screen uses -- see print.js.

   `class-codes.html` INVENTED THIS PATTERN and still uses it: `#slips` is a
   sibling of `<main>`, filled by the page module, seen only by the printer. What
   was wrong until 2026-08-18 is that the rule hiding the screen was UNSCOPED, so
   all fifteen other pages in the product printed a blank sheet -- no header, no
   main, and nothing of their own to put there. It is `body.prints-own` now, and
   a page that has not been taught to print simply prints as it looks. */

.print-area { display: none; }

/* THE DEFAULT PAGE IS A4 LANDSCAPE. These grids are wider than they are tall --
   thirty students by twenty-five questions -- and portrait would chunk every one
   of them. 10mm because the content is a table with its own borders and a wider
   margin buys nothing. */
@page { size: A4 landscape; margin: 10mm; }


/* `@media print` HOLDS ONLY THE THREE THINGS THAT ARE GENUINELY ABOUT THE
   MEDIUM. Everything about how the printout LOOKS is below it, outside, which
   reads as a mistake until you try to split a wide grid across sheets: it has to
   be MEASURED first -- how wide did this table actually come out at print sizes?
   -- and a `display: none` element reports every rectangle as zero. There is
   also no way to ask a browser to apply `@media print` on demand.

   So the appearance rules are scoped to `.print-area`, which is `display: none`
   on screen and therefore costs nothing there, and the page adds `.measuring` to
   put the container off-screen at exactly one printed page's width with every
   print rule already in force. See measureOffScreen() in print.js. The
   alternative was a second copy of those rules under another class name, which
   is two sets that agree until one of them is fixed. */
@media print {
  /* WITHOUT THIS EVERY RAG COLOUR IS STRIPPED and the grids are pointless --
     browsers drop backgrounds when printing unless asked not to. The -webkit-
     twin is not redundant: Safari still needs it, and Safari is the old iPad a
     pilot will meet. */
  *, *::before, *::after {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* A page that builds its own printout replaces the screen with it. Anything
     else prints as it looks, which is the honest fallback. */
  body.prints-own > .app-header,
  body.prints-own > main { display: none; }

  .print-area { display: block; }
}

/* Off-screen at exactly the width of one printed page, so a table measured here
   is a table measured on paper. THE WIDTH FOLLOWS THE ORIENTATION -- print.js
   sets `--print-page-width` when it writes the @page rule, 277mm for landscape
   and 186mm for portrait -- because a grid measured against a landscape sheet
   and then printed portrait would come out a third too wide. The fallback is
   landscape, which is what a grid takes. */
.print-area.measuring {
  display: block;
  position: fixed;
  left: -20000px;
  top: 0;
  width: var(--print-page-width, 277mm);
}

/* One printed document per sheet. `break-after` rather than a margin, because
   what separates two children's sheets is a page and not a gap. */
.print-sheet { break-after: page; }
.print-sheet:last-child { break-after: auto; }

/* A grid too wide for one sheet is cut into chunks, and each after the first
   starts a new page -- see splitWideTable() in print-grid.js. */
.print-chunk + .print-chunk { break-before: page; }

/* Which sheet of how many, and which columns are on it. Without it a
   continuation page is a wall of numbers with no way to tell where it belongs. */
.print-chunk-note {
  margin: 0 0 0.3rem;
  font-size: 0.75rem;
  color: #555;
}

/* The type a grid was shrunk to so that it would fit one sheet. The page sets
   the number; chunkPlan() in print-grid.js decides how far it may go, and splits
   instead rather than printing something nobody can read. */
.print-area .print-shrink .rag-grid {
  font-size: calc(7.5pt * var(--print-shrink, 1));
}

/* A name on paper is a name, not a link to somewhere nobody can click. */
.print-area a { color: inherit; text-decoration: none; }

/* ---- The summary table, on A4 portrait ----
   THE HEADINGS WERE THE WIDEST THING IN FOUR OF THE SIX COLUMNS (reported by the
   developer, 2026-08-18). "Relative performance" is twenty characters over cells
   holding "+0.9 (+22%)", and "Edexcel average" fifteen over "4.2/5 (84%)" -- and
   with `white-space: nowrap` inherited from the global `th, td` rule, each of
   those columns was as wide as its own name. On a portrait sheet that is most of
   the width spent on four headings.

   So the headings WRAP and are set smaller than the figures under them, which is
   the right way round: the heading is read once and the column is read thirty
   times. The description column is the one allowed to take the room that frees
   up, being the only prose on the sheet. */
/* `border-collapse: collapse` is not tidiness: a plain table defaults to
   `separate` with a border-spacing of 2px, and seven gaps of that put the table
   2mm over a 186mm sheet -- which is a whole second page holding a sliver of one
   column. Found by measuring the printed width against the sheet, not by eye. */
/* TWO SHAPES OF LIST, ONE SET OF RULES. `.summary-table` is analysis.html's
   class summary and student sheet -- six columns, a description among them.
   `.ranked-table` is topic-analysis.html's whole-class ranked list and a child's
   own breakdown -- five columns, a skill or unit name among them. They are the
   same KIND of table and had the same fault: headings twice the width of what is
   under them (the developer, 2026-08-18, having asked for it once already on the
   other screen). Everything typographic is shared; only the column widths differ,
   because one has six columns and the other five. */
.print-area .summary-table,
.print-area .ranked-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 7pt;
  line-height: 1.15;
}

.print-area .summary-table th,
.print-area .ranked-table th {
  white-space: normal;
  font-size: 6.5pt;
  line-height: 1.15;
  vertical-align: bottom;
}

.print-area .summary-table td,
.print-area .ranked-table td { white-space: nowrap; }

/* The description wraps and takes what is left; every other column is as narrow
   as its content now that its heading can fold.

   `table-layout: fixed` IS WHAT MAKES THE WIDTHS BELOW MEAN ANYTHING, and it was
   added after measuring rather than by design: with the browser's automatic
   layout the percentages are a suggestion, the nowrap figures push past them,
   and the table came out 188mm on a 186mm sheet -- two millimetres over, which
   is a second sheet holding one sliver of a column. Fixed layout makes the
   percentages exact, and they add to 100 on purpose. */
.print-area .summary-table,
.print-area .ranked-table { table-layout: fixed; }

.print-area .summary-table td.key-name,
.print-area .summary-table th:nth-child(2),
.print-area .ranked-table td.key-name,
.print-area .ranked-table th:nth-child(1) { white-space: normal; }

/* Six columns: question, description, mark, max, Edexcel, relative. */
.print-area .summary-table th:nth-child(1) { width: 7%; }
.print-area .summary-table th:nth-child(2) { width: 42%; }
.print-area .summary-table th:nth-child(3) { width: 8%; }
.print-area .summary-table th:nth-child(4) { width: 9%; }
.print-area .summary-table th:nth-child(5),
.print-area .summary-table th:nth-child(6) { width: 17%; }

/* Five: the skill or unit, then marks, percentage, Edexcel, relative. The name
   takes what the other four do not, being the only prose on the sheet. Each set
   adds to 100 on purpose -- `table-layout: fixed` makes them exact. */
.print-area .ranked-table th:nth-child(1) { width: 44%; }
.print-area .ranked-table th:nth-child(2) { width: 12%; }
.print-area .ranked-table th:nth-child(3) { width: 11%; }
.print-area .ranked-table th:nth-child(4),
.print-area .ranked-table th:nth-child(5) { width: 16.5%; }

/* A CHILD'S SHEET IS MEANT TO BE ONE SHEET, which is the developer's ask and is
   why the rows are tight: a 25-question paper is 25 rows, and at this size a
   portrait page holds well over that. A longer paper will still run to a second
   sheet rather than being squeezed to illegibility. */
.print-area .summary-table th,
.print-area .summary-table td,
.print-area .ranked-table th,
.print-area .ranked-table td { padding: 0.5px 3px; }

/* THE PRINT BUTTON SITS AT THE FOOT OF THE PANEL IT PRINTS, and is small on
   purpose (the developer, 2026-08-18): it is furniture at the bottom of a panel
   rather than the thing the panel is for, and a full-size button there competes
   with the table above it.

   A panel that FOLDS takes its button with it, which is the point of putting it
   inside rather than above -- a shut panel must not offer to print something
   nobody can see. */
.print-foot {
  margin: 0.6rem 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.print-button {
  padding: 0.2rem 0.6rem;
  font-size: 0.78rem;
  font-weight: 500;
}

/* The foot is a control, so it is never on the paper -- printing it would be
   printing the button that was just pressed. */
.print-area .print-foot { display: none; }

/* SENTENCES THAT ONLY MAKE SENSE ON A SCREEN. "Click a student for their own
   sheet" is an instruction nobody can follow on paper, and the line about what
   the columns mean is a caption for a control that is not on the sheet either.
   Marked where they are WRITTEN rather than guessed at here by matching
   `.muted`, which also carries text that is worth printing. */
.print-area .screen-only { display: none; }

/* The panels a printed table arrives wrapped in are screen furniture: a card
   with a shadow inside a printed page is a box drawn round nothing. */
.print-area .panel {
  padding: 0;
  margin: 0 0 0.4rem;
  border: 0;
  box-shadow: none;
  background: none;
}

  /* The masthead: the logo and the site name, then what this sheet is about. */
  .print-masthead {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    border-bottom: 2px solid var(--lesson-cyan-deep);
    padding-bottom: 0.3rem;
    margin-bottom: 0.5rem;
  }

  .print-masthead img { width: 28px; height: 28px; }

  .print-masthead h1 {
    margin: 0;
    font-size: 1rem;
    letter-spacing: 0.01em;
  }

  .print-title { margin: 0 0 0.5rem; }
  .print-title h2 { margin: 0 0 0.15rem; font-size: 1.05rem; }

  /* The facts about this sheet -- assessment, date, class, student, grade,
     target -- on one line, separated rather than listed, so the eye takes them
     in as a caption and not as a table. */
  .print-facts {
    margin: 0;
    font-size: 0.8rem;
    color: #333;
  }

  .print-facts span + span::before {
    content: " · ";
    color: #888;
  }

  .print-footer {
    margin-top: 0.5rem;
    font-size: 0.7rem;
    color: #555;
    border-top: 1px solid var(--line);
    padding-top: 0.2rem;
  }

  /* STICKY IS UNDONE EVERYWHERE, and it has to be before anything else works.
     A sticky box on paper has nothing to scroll within: the pinned name column
     and the two footer rows print ON TOP of the grid, which this stylesheet's
     own comments record happening once already. */
  .print-area .rag-grid thead th,
  .print-area .rag-grid tbody .student-name,
  .print-area .rag-grid tfoot .student-name,
  .print-area .rag-grid .student-name,
  .print-area .rag-grid .row-total,
  .print-area .rag-grid .row-grade,
  .print-area .rag-grid tfoot tr,
  .print-area .rag-grid .rag-labels th,
  .print-area table th,
  .print-area table td {
    position: static;
  }

  /* HEADINGS REPEAT ON EVERY SHEET, which is what makes a forty-student grid
     readable past page one -- AND IT ONLY WORKS ONCE THE STICKY ABOVE IS GONE,
     because a sticky thead is taken out of the normal table layout the browser
     would otherwise repeat. */
  .print-area thead { display: table-header-group; }
  .print-area tr { break-inside: avoid; }

  /* The scroll boxes are a screen idea. `max-height: 70vh` on .grid-scroll clips
     a class to one screenful, which on paper is a class cut in half. */
  .print-area .table-scroll,
  .print-area .grid-scroll {
    overflow: visible;
    max-height: none;
    border: 0;
    /* THE NEGATIVE MARGIN IS THE ONE THAT MATTERED. `.table-scroll` carries
       `margin: 0 -4px` so the scroller bleeds to the panel's edge on screen, and
       on paper those 8px made every table 2.1mm WIDER THAN THE SHEET -- which is
       a whole extra page holding one sliver of a column. Measured against the
       container rather than guessed at: the width came out 188.1mm on a 186mm
       sheet and stayed there through two other fixes aimed at the wrong thing. */
    margin: 0;
  }

  /* THE ROTATED HEADINGS PRINT, NAMES AND ALL (2026-08-18). The developer asked
     why the curriculum grid could not print the short names it shows on screen,
     and the answer was that it can -- what cannot be done is UNROTATING them, a
     column then having to be as wide as its heading, which turns a 46-column
     grid into six sheets. Dropping the text was a decision made in the shadow of
     that one and never separately justified.

     WRAPPING IS WHAT MAKES IT WORK, and the direction of it is counter-intuitive
     enough to write down: a TALLER box is a LONGER line and therefore FEWER
     lines, so the column gets NARROWER. Measured on A4 landscape (277mm across):

         heading   25 columns   46 columns
         25mm      511mm        890mm
         35mm      373mm        634mm
         45mm      306mm        508mm    <- every name shown in full

     45mm it is. A 25-column grid comes to 306mm, so it shrinks by a tenth and
     fits one sheet across. THE COST IS PAGE HEIGHT -- 45 of the 190mm a
     landscape sheet has -- so a full class of thirty runs to a second sheet
     where a code-only heading kept it on one. That is the trade, and it was the
     developer's to make.

     THE HEIGHT HAS TO BE IN MILLIMETRES HERE. On screen it is in `rem`, which is
     root-relative and so takes no notice of the print type size -- the trap this
     stylesheet has now met five times. Left alone, an 18.5rem heading is 78mm of
     a 190mm page.

     `.rag-head` and `.q-head` are not rotated themselves; the SPAN inside each
     is. So these rules give the box a size and leave the rotation alone. */
  .print-area .rag-grid .rag-head,
  .print-area .qla-grid .q-head {
    height: 45mm;
    max-height: none;
    vertical-align: bottom;
  }

  .print-area .rag-grid .rag-head-name,
  .print-area .qla-grid .q-desc {
    display: block;
    max-height: 39mm;
    font-size: 6pt;
  }

  /* The corner cell's labels for those stacked lines: they name a heading layout
     that the printed sheet does not need spelling out. */
  .print-area .rag-name-label,
  .print-area .q-desc-label { display: none; }

  /* Controls have no meaning on paper. */
  .print-area select,
  .print-area button,
  .print-area input,
  .print-area .analysis-controls,
  .print-area .sort-controls,
  .print-area .filters { display: none; }

  /* A folded panel prints open -- see openAllDetails() in print.js, which sets
     the attribute; this hides the twisty the attribute leaves behind. */
  .print-area details > summary { list-style: none; font-weight: 600; }
  .print-area details > summary::-webkit-details-marker { display: none; }

  /* Type small enough to fit a wide grid and large enough to read. Measured
     against a 30-student, 25-question QLA sheet on A4 landscape. */
  .print-area { font-size: 8.5pt; }
  .print-area th, .print-area td { padding: 2px 4px; }

  /* THE GRID'S OWN SIZES ARE IN `rem` AND SO ARE DEAF TO THE LINE ABOVE, which
     is the trap this stylesheet has now met four times: a size measured against
     something fixed elsewhere. `rem` is root-relative, not inherited, so
     `font-size` on `.print-area` leaves `.rag-grid`'s 0.9rem type and its four
     `min-width`s exactly as they were -- and MEASURING rather than looking is
     what found it: a 25-question sheet came out 474mm wide against the 277mm a
     landscape A4 has, so the commonest printout in the product would have been
     split over two sheets for no reason.

     Relaxed rather than overridden wherever the content can decide for itself.
     The three pinned right-hand columns keep a width, because on screen their
     widths and their `right` offsets are one fact written twice -- but the
     offsets are gone here (nothing is sticky on paper), so these are free to be
     whatever the content needs. */
  .print-area .rag-grid { font-size: 7.5pt; }

  .print-area .rag-grid th,
  .print-area .rag-grid td { padding: 1px 3px; }

  .print-area .rag-grid .rag-head {
    min-width: 0;
    height: auto;
    padding: 1px 2px;
  }

  /* THE DATA CELLS CARRY A MIN-WIDTH TOO, and it is the one that was actually
     driving the width: `.rag-grid td.rag` is 3.4rem and `.qla-grid td.qla`
     2.4rem, both root-relative, so a cell holding the digit 3 was 54px wide at
     any print size. Found by measuring a cell rather than by reading the file --
     the heading looked like the obvious culprit and was not. */
  .print-area .rag-grid td.rag,
  .print-area .qla-grid td.qla { min-width: 0; }

  .print-area .rag-grid .student-name { min-width: 0; }
  .print-area .rag-grid tbody .student-name,
  .print-area .rag-grid tfoot .student-name { font-size: 7.5pt; }
  .print-area .rag-grid .row-total { min-width: 0; }
  .print-area .rag-grid .row-grade { min-width: 0; }

  /* A grid is sized by its content on paper, not stretched to the sheet: a
     25-question sheet that fits should not be spread thin to fill the width. */
  .print-area .rag-grid { width: auto; }

/* ---- Report a problem (2026-08-30) ----------------------------------------
   The fixed link every signed-in page carries, and its dialog. Injected by
   js/report-problem.js; pages themselves declare nothing. The link is
   deliberately quiet -- present when needed, invisible in the mind's eye
   otherwise. */
#report-problem-link {
  position: fixed; right: 0.9rem; bottom: 0.9rem; z-index: 40;
  font-size: 0.8rem; padding: 0.35rem 0.7rem;
  background: var(--panel-bg, #fff); color: var(--ink-soft);
  border: 1px solid var(--line, #d8dee6); border-radius: 999px;
  cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
#report-problem-link:hover { color: var(--ink); }
#report-problem-overlay {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(20, 26, 34, 0.45);
  display: flex; align-items: center; justify-content: center; padding: 1rem;
}
#report-problem-overlay[hidden] { display: none; }
.report-problem-dialog {
  position: relative;
  background: var(--panel-bg, #fff); border-radius: 10px;
  padding: 1.25rem 1.5rem; max-width: 30rem; width: 100%;
  box-shadow: 0 8px 30px rgba(0,0,0,0.25);
}
/* The corner close (2026-08-30). Deliberately NOT `button.icon-button` -- that
   class is a bordered control that belongs in a toolbar, and a boxed square in
   the corner of a dialog reads as an action rather than a dismissal. The dialog
   is `position: relative` solely to anchor this. */
.report-problem-close {
  position: absolute; top: 0.4rem; right: 0.5rem;
  padding: 0.1rem 0.45rem;
  background: none; color: var(--ink-soft);
  font-size: 1.5rem; font-weight: 400; line-height: 1.2;
}
.report-problem-close:hover:not(:disabled) { background: none; color: var(--ink); }
/* The heading stops short of the ×, so a wider dialog never runs its title
   under the control. */
.report-problem-dialog h2 { margin: 0 0 0.5rem; padding-right: 1.75rem; }
.report-problem-dialog textarea {
  width: 100%; box-sizing: border-box; resize: vertical;
  font: inherit; padding: 0.5rem; margin-top: 0.5rem;
}
.report-problem-buttons { display: flex; gap: 0.6rem; margin: 0.9rem 0 0; }
