/* ── The set is live ───────────────────────────────────────────────────────
   The settings page looked identical whether nobody had opened the paper or
   five students had finished it, so "can I still change this?" — the question
   a tutor arrives with — had no answer on the screen that offers the switches.
   Said once at the top, and again on the cards it bears on, because a note at
   the top of a page ten cards tall is not beside the switch it is about. */
.set-live {
  grid-column: 1 / -1; display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 13px 18px; background: var(--state-warn-bg); border: 1px solid var(--flag-gold);
  border-radius: var(--radius-md); font-size: var(--text-md); line-height: 1.55; color: var(--brown-ink);
}
.set-live strong { color: var(--deep-ink); }
.set-live > span:first-child { flex: 1 1 100%; }
.card-live-note {
  display: flex; gap: 9px; align-items: baseline; margin-top: 14px;
  padding: 10px 13px; background: var(--state-warn-bg); border: 1px solid var(--flag-gold);
  border-radius: 9px; font-size: 13px; line-height: 1.5; color: var(--brown-ink);
}
.card-live-note::before { content: "!"; font-weight: 800; color: var(--warm-tan); }
.card-live-note strong { color: var(--deep-ink); }
/* A card whose settings the sittings depend on. The tint IS the signal — no
   disabled grey, because grey reads as broken and these are working exactly as
   intended. It warns rather than closes: whether they should be closed once a
   paper has been sat is a decision about how John works, not one to make
   silently inside a restyle. */
.settings-card.is-fixed { background: var(--sand-soft); border-color: var(--border-card); }

/* Save, where it is needed rather than only where it was built. */
.save-bar {
  position: sticky; bottom: 0; z-index: 15; margin-top: 22px; padding: 14px 20px;
  background: var(--warm-white); border-top: 1px solid var(--border-card);
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}
.save-bar-note { font-size: var(--text-md); color: var(--text-secondary); }
.save-bar-note strong { color: var(--deep-ink); }
.save-bar .btn-primary { margin-left: auto; }

/* ── Item analysis, in the design's grammar ────────────────────────────────
   FOUR COLUMNS: which question, what it says, how the cohort spread across its
   options, and the verdict. It was a stack, so comparing the spread against
   the verdict meant reading down and holding both in your head — and those two
   things ARE the comparison the screen exists to make. Side by side, a
   distractor beating the key is a shape rather than a pair of numbers. */
/* ── The heading over each group of faults ────────────────────────────────
   Four faults, four jobs — re-read the key, remove an ambiguity, replace the
   question, or change nothing and just know. Sorted into one worst-first
   column the reader has to infer which job a row is, and the row that needs
   the key checked looks like a milder version of the one above rather than a
   different kind of thing. Set at the weight of a section heading, not a card:
   these divide the list, they are not items in it. */
.ia-head { margin: 22px 0 10px; }
.ia-head:first-child { margin-top: 4px; }
.ia-head h3 {
  margin: 0 0 4px; font-family: var(--font-display);
  font-size: 15px; font-weight: 700; color: var(--text-primary);
}
.ia-head p {
  margin: 0; max-width: 78ch;
  font-size: 12.5px; line-height: 1.55; color: var(--text-secondary);
}
/* The sound remainder is a boundary, not a finding — it says the problems
   have ended, so it must not read as a fifth one. */
.ia-head-quiet { border-top: 1px solid var(--border-card); padding-top: 14px; }
.ia-head-quiet h3 { font-size: 13.5px; color: var(--text-secondary); }

.ia-row {
  display: grid; grid-template-columns: 44px minmax(0, 1fr) 190px auto;
  gap: 6px 16px; align-items: start;
}
.ia-q { font-weight: 700; font-size: 14px; color: var(--text-secondary); }
.ia-stem { font-family: var(--exam-font); font-size: 14.5px; line-height: 1.5; }
.ia-meta { grid-column: 2; font-size: var(--text-sm); color: var(--text-secondary); margin-top: 3px; }
.ia-diag { grid-column: 4; display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.ia-tag.broken { background: var(--state-bad-bg); border: 1px solid var(--state-bad-border); color: var(--state-bad-text); }
.ia-tag.check { background: var(--state-warn-bg); border: 1px solid var(--flag-gold); color: var(--brown-ink); }
.ia-tag.waste { background: var(--sand-soft); border: 1px solid var(--border-card); color: var(--text-secondary); }
.ia-tag.slow { background: var(--partial-bg); border: 1px solid var(--partial-border); color: #3A4A73; }
/* A question that did its job says so, quietly — it must not read as a flag. */
.ia-tag.sound { background: #EFF6F0; border: 1px solid #C6DDCB; color: var(--correct-text); }
.ia-tag.quiet { background: var(--warm-white); border: 1px solid var(--border-card); color: var(--text-secondary); }
/* The option spread, in its own column and spanning the rows beside it. */
.ia-opts { grid-column: 3; grid-row: 1 / 4; display: grid; gap: 3px; align-content: start; margin: 0; }
.ia-opt {
  display: grid; grid-template-columns: 14px minmax(0, 1fr) 38px;
  gap: 6px; align-items: center; font-size: 11.5px;
}
.ia-opt-k { font-weight: 700; color: var(--text-secondary); min-width: 0; }
.ia-opt-bar { height: 11px; border-radius: 3px; background: var(--sand-soft); overflow: hidden; }
.ia-opt-bar i { display: block; height: 100%; background: var(--warm-stone); }
.ia-opt.is-key .ia-opt-k { color: var(--deep-ink); }
.ia-opt.is-key .ia-opt-bar i { background: var(--correct-border); }
.ia-opt.is-trap .ia-opt-bar i { background: var(--warm-tan); }
.ia-opt-n { text-align: right; color: var(--text-secondary); }
.ia-why { grid-column: 1 / -1; }
.ia-acts { grid-column: 1 / -1; }
@media (max-width: 1000px) {
  .ia-row { grid-template-columns: 40px minmax(0, 1fr); }
  .ia-opts, .ia-diag { grid-column: 1 / -1; grid-row: auto; }
  .ia-diag { justify-content: flex-start; }
}

/* ── Horizon: a cohort figure with its field attached ──────────────────────
   Ported from the design's horizon-cohort screen. Only the part of it that
   our data supports: the field a figure was computed on. The rest of that
   screen — matching a sheet's names to students across ten weeks, pinned
   snapshot versions, declared comparability — describes a feature we have
   not built, and drawing its chrome over data we don't hold would be a
   screen that lies quietly. */
.hz-panel {
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-lg); overflow: hidden;
}
.hz-panel-head {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 14px 20px; background: var(--sand-soft); border-bottom: 1px solid var(--hairline);
}
.hz-panel-title { font-family: var(--font-display); font-weight: 700; font-size: 15.5px; }
.hz-panel-count { margin-left: auto; font-size: 13px; color: var(--text-secondary); }
.hz-panel-note { flex: 1 1 100%; font-size: 13px; line-height: 1.55; color: var(--text-secondary); margin: 0; }
/* One figure per paper, because a rank is per paper: Reading 6 of 47 and
   Writing 20 of 41 are different numbers about different fields. */
.hz-figures {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 10px; padding: 14px 20px;
}
.hz-figure {
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-md); padding: 14px 16px;
}
.hz-figure-n { font-family: var(--font-ui); font-weight: 700; font-size: 20px; }
.hz-figure-l { font-size: 13px; color: var(--text-secondary); margin: 2px 0 8px; }
.hz-figure.is-limited { background: var(--sand-soft); }
.hz-field {
  display: inline-flex; align-items: baseline; gap: 7px; flex-wrap: wrap;
  font-size: var(--text-sm); color: var(--text-secondary);
}
.hz-field b { font-weight: 700; color: var(--text-primary); }
.hz-can { display: grid; gap: 0; border-top: 1px solid var(--hairline); }
.hz-can-row {
  display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 12px; align-items: baseline;
  padding: 11px 20px; border-bottom: 1px solid var(--hairline); font-size: var(--text-md); line-height: 1.5;
}
.hz-can-row:last-child { border-bottom: 0; }
.hz-can-row b { font-weight: 800; font-size: 14px; }
.hz-can-row.is-yes b { color: var(--correct-text); }
.hz-can-row.is-no b { color: var(--warm-tan); }
.hz-can-row.is-cond b { color: var(--brown-ink); }
.hz-can-row.is-cond { background: var(--state-attention-bg); }
.hz-can-row.is-no { color: var(--text-secondary); }
.hz-can-row strong { color: var(--deep-ink); }

/* The one week that is yours. On ten near-identical rows the question the
   screen is opened with — where am I up to — was answerable only by reading
   all ten and comparing bars. Exactly one row carries this, because two
   highlights is none. */
#horizon-weeks > .pw-row.is-next {
  border-color: var(--flag-gold); background: var(--state-attention-bg);
  box-shadow: inset 3px 0 0 var(--flag-gold);
}
.pw-done {
  display: inline-flex; align-items: center; font-size: 12px; font-weight: 700;
  color: var(--correct-text); white-space: nowrap; margin-right: 2px;
}

/* ── Plus reports: the state is a column, not a tint ───────────────────────
   Three tints and a paragraph made the state something you inferred from a
   colour and then confirmed by reading. Twelve families down the list, that
   is twelve inferences. No "sent" state: reports go out by hand, so the
   platform does not know and will not pretend to. */
.pr-row {
  display: grid; grid-template-columns: minmax(0, 1.2fr) 150px minmax(0, 1fr) auto;
  gap: 14px; align-items: center;
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-md); padding: 12px 16px; margin-bottom: 8px;
}
.pr-row.is-fail { border-color: var(--incorrect-border); background: var(--state-bad-bg); }
.pr-row.is-blocked { background: var(--state-attention-bg); border-color: var(--flag-gold); }
.pr-who { font-size: var(--text-lg); font-weight: 600; }
.pr-who small { display: block; font-size: var(--text-sm); color: var(--text-secondary); font-weight: 400; margin-top: 2px; }
.pr-state { font-size: var(--text-sm); font-weight: 700; border-radius: 999px; padding: 3px 11px; text-align: center; white-space: nowrap; }
.pr-state.ready { background: var(--sand-soft); border: 1px solid var(--border-card); color: var(--text-primary); }
.pr-state.blocked { background: var(--state-warn-bg); border: 1px solid var(--flag-gold); color: var(--brown-ink); }
/* Text in --incorrect-border on --incorrect-bg measured 4.38:1 — under the
   4.5 AA needs for 12.5px bold, and the only chip in the set that failed. The
   border keeps the original red; only the text darkens, to --state-bad-text,
   which measures 6.04:1 on the same ground. */
.pr-state.fail { background: var(--incorrect-bg); border: 1px solid var(--incorrect-border); color: var(--state-bad-text); }
.pr-note { font-size: 13px; line-height: 1.5; color: var(--text-secondary); }
.pr-note strong { color: var(--deep-ink); }
@media (max-width: 900px) {
  .pr-row { grid-template-columns: minmax(0, 1fr) auto; }
  .pr-note { grid-column: 1 / -1; }
}

/* ── The export dialog: what goes in, beside who it is for ─────────────────
   The rows used to sit behind a "Customise what's included" fold, which made
   the presets' one job invisible — you chose "For the parent" and either
   trusted the label or opened the fold and read eleven rows to find the two
   that moved. */
/* Was `.dl-card:has(.ex)`, which could never fail to match: there is exactly
   one download card in the markup and the example block is static inside it.
   So the condition described nothing, cost a modern browser, and gave an older
   one a card 220px too narrow for a layout it was perfectly able to draw. */
.dl-card { max-width: 940px; }
.ex { display: grid; grid-template-columns: minmax(0, 330px) minmax(0, 1fr); gap: 22px; align-items: start; }
.ex-rows {
  border: 1px solid var(--border-card); border-radius: var(--radius-md);
  background: var(--warm-white); overflow: hidden;
}
.ex-head {
  display: flex; align-items: baseline; gap: 12px;
  padding: 10px 14px; background: var(--sand-soft); border-bottom: 1px solid var(--hairline);
}
.ex-head-t { font-family: var(--font-display); font-weight: 700; font-size: 14.5px; }
.ex-count { margin-left: auto; font-size: var(--text-sm); color: var(--text-secondary); }
.ex-rows .report-field-checks { min-height: 0; gap: 0; padding: 4px 14px 12px; max-height: 52vh; overflow-y: auto; }
@media (max-width: 860px) {
  .ex { grid-template-columns: minmax(0, 1fr); }
  .ex-rows .report-field-checks { max-height: none; }
}

/* ── Marking: which rubric, and what the bands add up to ───────────────────*/
.mk-top {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 9px 12px; margin: 0 0 10px;
  background: var(--sand-soft); border: 1px solid var(--border-card);
  border-radius: var(--radius-md); font-size: var(--text-md);
}
.mk-top-label { font-size: 11.5px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-secondary); }
.mk-top-note { flex: 1 1 100%; font-size: var(--text-sm); line-height: 1.5; color: var(--text-secondary); }
/* What the rubric says about ITSELF, kept visually apart from the fixed note
   above it — one is the platform explaining how marking works and is the same
   on every paper, the other is this rubric's own words and is not. Run
   together they read as one paragraph from the same voice. */
.mk-top-about {
  flex: 1 1 100%; font-size: var(--text-sm); line-height: 1.5;
  color: var(--text-secondary); padding-top: 6px;
  border-top: 1px dashed var(--hairline);
}
.mk-total {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 12px 14px; margin-top: 12px;
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-md);
}
.mk-total-n { font-family: var(--font-ui); font-weight: 700; font-size: var(--text-2xl); }
.mk-total-l { flex: 1; min-width: 200px; font-size: 13px; line-height: 1.5; color: var(--text-secondary); }
.mk-total-l strong { color: var(--deep-ink); }
.mk-wordcount {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin: 8px 0 0; font-size: var(--text-sm); color: var(--text-secondary);
}
.mk-wordcount strong { color: var(--text-primary); }

/* A bank question item analysis has marked broken. Rare by design — if this
   tint is everywhere it has stopped meaning anything. */
.bank-row.is-broken { background: var(--state-bad-bg); border-color: var(--state-bad-border); }

/* ── The neighbours, in their own words ────────────────────────────────────*/
.qe-neighbours { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 8px 0 0; }
.qe-nb {
  display: grid; gap: 2px; text-align: left; min-width: 0;
  /* Rows at the top. The two cards share a grid row, so a card with a
     tall neighbour is stretched to match; its two spans were then spread
     down the stretched height and Q38's excerpt sat centred in a box
     three times its size. */
  align-content: start;
  padding: 8px 11px; background: var(--sand-soft);
  border: 1px solid var(--border-card); border-radius: var(--radius-md);
  font: inherit; color: inherit; cursor: pointer;
}
.qe-nb:hover:not(:disabled) { border-color: var(--brown-ink); background: var(--parchment); }
.qe-nb-next { text-align: right; }
.qe-nb-n { font-size: 11.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-secondary); }
/* TWO LINES OF WORDS, NOT A CLAMPED BOX. This was a -webkit-box with
   -webkit-line-clamp: 2, which clamps INLINE content and nothing else:
   a stem that carries a list or a picture hands the box block children,
   the box lays those out as its own items, and the clamp draws the
   inline text over the top of them. On the free test's Q38 the three
   bullet lines and "Which one of the following must also be true?"
   printed through each other, and Q40's picture filled the card at full
   size. So the excerpt is a plain block two lines tall: every child is
   made inline, a list item keeps a bullet as text, and a picture becomes
   a thumbnail the height of a line. Nothing here is the question; it is
   the neighbour's words, which the design asked for. */
.qe-nb-stem {
  font-size: var(--text-sm); line-height: 1.4; color: var(--text-secondary);
  display: block; max-height: 2.8em; overflow: hidden;
}
.qe-nb-stem :is(p, div, ul, ol, li, figure, h1, h2, h3, table, tr, td, th) {
  margin: 0; padding: 0; display: inline; border: 0;
}
.qe-nb-stem li::before { content: '\2022 '; }
.qe-nb-stem li::after { content: ' '; }
.qe-nb-stem img { display: inline; max-height: 1.4em; width: auto; margin: 0 4px 0 0; vertical-align: middle; border-radius: 2px; }
.qe-nb.is-empty { background: none; border-style: dashed; cursor: default; }
@media (max-width: 700px) { .qe-neighbours { grid-template-columns: 1fr; } .qe-nb-next { text-align: left; } }

/* ── Readiness as a checklist ──────────────────────────────────────────────
   Failures only reads as a shorter list the closer a paper gets to ready, and
   at the end as nothing at all — "Ready to go out" over an empty space does
   not say what was verified. Ticks are the other half of the sentence. */
.ready-panel {
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-md); overflow: hidden; margin: 8px 0;
}
.ready-head {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px; background: var(--sand-soft); border-bottom: 1px solid var(--hairline);
}
.ready-title { font-family: var(--font-display); font-weight: 700; font-size: 14.5px; }
.ready-count { margin-left: auto; font-size: var(--text-sm); color: var(--text-secondary); }
.ready-row {
  display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; gap: 10px;
  align-items: baseline; padding: 9px 14px;
  border-bottom: 1px solid var(--hairline); font-size: var(--text-md); line-height: 1.5;
}
.ready-row:last-child { border-bottom: 0; }
.ready-mark { font-weight: 800; text-align: center; }
.ready-row.is-done { color: var(--text-secondary); }
.ready-row.is-done .ready-mark { color: var(--correct-text); }
.ready-row.is-todo { background: var(--state-attention-bg); }
.ready-row.is-todo .ready-mark { color: var(--brown-ink); }
.ready-what { min-width: 0; }
.ready-why { display: block; margin-top: 3px; font-size: var(--text-sm); color: var(--text-secondary); }
.ready-row.is-todo .ready-why { color: var(--text-primary); }
.ready-foot {
  margin: 0; padding: 10px 14px; border-top: 1px solid var(--hairline);
  background: var(--sand-soft); font-size: var(--text-sm); line-height: 1.55; color: var(--text-secondary);
}
.ready-foot strong { color: var(--deep-ink); }

/* ── What the student sees afterwards, as one choice ───────────────────────
   Two independent checkboxes could express four combinations; the server
   permits three. Named policies cannot ask for the one it would refuse. */
.policy-set { display: grid; gap: 10px; margin: 4px 0 16px; }
.policy {
  display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; align-items: start;
  padding: 14px 16px; border: 1px solid var(--border-card); border-radius: var(--radius-md);
  background: var(--warm-white); cursor: pointer;
}
.policy:hover { border-color: var(--warm-tan); }
.policy.is-on { background: var(--sand-soft); border-color: var(--deep-ink); }
.policy input { width: 17px; height: 17px; accent-color: var(--deep-ink); margin-top: 2px; }
.policy-name { font-size: var(--text-lg); font-weight: 700; display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.policy-when { font-size: 12px; font-weight: 700; border-radius: 999px; padding: 2px 9px; background: var(--parchment); border: 1px solid var(--border-card); color: var(--text-secondary); }
.policy-what { display: block; font-size: var(--text-md); line-height: 1.55; color: var(--text-secondary); margin-top: 4px; }
.policy-shows { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.pshow { border: 1px solid var(--border-card); background: var(--parchment); }
.pshow.yes { background: #EDF3EA; border-color: #A8C49C; color: #2F5A26; }
.pshow.no { color: var(--text-secondary); text-decoration: line-through; text-decoration-color: var(--warm-tan); }
/* Settled: the same card with nothing to click. What a student gets back is
   decided by the practice tick now, so this states the outcome rather than
   offering it — which means dropping the 20px gutter the radio used to sit in,
   and the pointer and hover border that promise something will happen. A card
   that looks pressable and is not is a worse control than no control. */
.policy.is-settled { grid-template-columns: minmax(0, 1fr); cursor: default; }
.policy.is-settled:hover { border-color: var(--deep-ink); }

/* The column worth looking at, stated as a fact and nothing more. A grid of
   numbers answers "how did they do"; it does not answer "where is this cohort
   coming apart", because that is a property of a column and a column is the
   one thing a table of rows makes you compute in your head. */
.results-colnote {
  margin: 12px 0 0; padding: 11px 14px;
  background: var(--sand-soft); border: 1px solid var(--border-card);
  border-radius: var(--radius-md);
  font-size: 13px; line-height: 1.55; color: var(--text-secondary);
}
.results-colnote strong { color: var(--deep-ink); }

/* The one failure on the import screen that leaves no trace anywhere else: a
   dropped line keeps every count intact, so the questions still look whole.
   Pulled out of the run of warnings, because in a stack of sentences it read
   as one more remark about a paper that looks fine. */
.hint-text.is-silent {
  padding: 12px 14px; margin: 6px 0 10px;
  background: var(--state-bad-bg); border: 1px solid var(--state-bad-border); border-radius: var(--radius-md);
  color: var(--text-primary); line-height: 1.55;
}
.hint-text.is-silent strong { color: var(--state-bad-text); }
.imp-lines { margin: 8px 0 0; padding-left: 18px; }
.imp-lines li {
  font-family: var(--exam-font); font-size: var(--text-sm); line-height: 1.5;
  color: var(--text-secondary); margin: 2px 0;
}
.imp-lines-more { display: block; margin-top: 4px; font-size: var(--text-sm); color: var(--text-secondary); }

/* ── What each week's marks carry ──────────────────────────────────────────
   Every week ranks the same way. What differs is whether the marks arrived as
   ANSWERS or as totals — and a week of totals has nothing for item analysis or
   marks by skill. Invisible one week at a time; this is the only place the
   unevenness can be seen before it turns up as a hole in a report. */
#horizon-coverage { margin-top: 16px; }
.hz-weeks { display: flex; gap: 5px; flex-wrap: wrap; padding: 14px 20px; }
.hz-week {
  display: grid; gap: 2px; justify-items: center; min-width: 76px;
  padding: 8px 6px; border: 1px solid var(--border-card); border-radius: 9px;
  background: var(--warm-white); font-size: 12px; color: var(--text-secondary);
  text-align: center;
}
.hz-week b { font-family: var(--font-ui); font-size: 13px; font-weight: 700; color: var(--text-primary); }
.hz-week.is-full { background: var(--correct-bg); border-color: var(--correct-border); }
/* Ours needs a state the design's did not: a week where SOME rows carry
   answers and some are totals. That is the common case here, and folding it
   into either neighbour would be the misreport this strip exists to prevent. */
.hz-week.is-mixed { background: var(--state-attention-bg); border-color: var(--flag-gold); }
.hz-week.is-totals { background: var(--sand-soft); }
.hz-week.is-empty { opacity: 0.55; }
.hz-week-key { display: flex; gap: 16px; flex-wrap: wrap; padding: 0 20px 14px; font-size: var(--text-sm); color: var(--text-secondary); }
.hz-week-key span { display: flex; align-items: center; gap: 6px; }
/* A BAR, NOT A BOX. An 11px rounded square with a border, sitting to the left
   of a label, is a checkbox — every other one on this page is exactly that,
   and the pale "totals only" swatch read as an unticked one. These are inert:
   they say what a colour on the strip above means. So they are short bars,
   which nothing on this platform asks you to tick, and which echo the FILL of
   the week chip rather than its outline. */
.hz-week-key i { width: 18px; height: 7px; border-radius: 999px; border: 1px solid var(--border-card); flex: none; }
.hz-week-key i.is-full { background: var(--correct-bg); border-color: var(--correct-border); }
.hz-week-key i.is-mixed { background: var(--state-attention-bg); border-color: var(--flag-gold); }
.hz-week-key i.is-totals { background: var(--sand-soft); }

/* An option is a drop target and a paste target, not only a file-picker
   button. A diagram goes from the paper to the option without becoming a file:
   on a Mac, Cmd+Ctrl+Shift+4 puts a selection straight on the clipboard. */
.import-option[data-drop] { outline-offset: 2px; }
.import-option[data-drop]:focus-visible {
  outline: 2px solid var(--deep-ink);
}
.import-option[data-drop]:focus-visible::after {
  content: "drop or paste a picture here";
  margin-left: auto; font-size: 11.5px; font-weight: 600; color: var(--text-secondary);
  white-space: nowrap;
}
.import-option.is-dropping {
  background: var(--parchment); outline: 2px dashed var(--brown-ink);
}

/* Where the questions land, said before the press that creates them. */
.imp-next {
  flex: 1 1 100%; order: -1;
  padding: 10px 13px; margin-bottom: 8px;
  background: var(--sand-soft); border: 1px solid var(--border-card);
  border-radius: var(--radius-md);
  font-size: 13px; line-height: 1.55; color: var(--text-secondary);
}
.imp-next strong { color: var(--deep-ink); }

/* ── Keyboard focus, once, for the whole tutor app ─────────────────────────
   The student surfaces have carried :focus-visible rules since their own
   ports; this app had exactly one, on the import drop rows added today. So a
   tutor working by keyboard got the browser default where it survived and
   nothing where a rule had cleared it.

   :where() keeps specificity at zero, so this never fights a component that
   wants its own focus treatment — it only fills the gap where none exists. */
:where(button, [role="button"], a[href], summary, label[tabindex], [tabindex="0"],
       [contenteditable="true"]):focus-visible {
  outline: 2px solid var(--brown-ink);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* A contenteditable div is where every stem, option and passage is written,
   and it matches none of the selectors above on its own — not an input, not a
   button. It also carries `outline: none` in 02-editor.css, which is the same
   specificity as this rule (a :where() is zero, a pseudo-class is one), so
   this file loading last is what settles it. Tab into the question editor
   before this and nothing happened at all.

   Text fields signal focus by border rather than outline — deliberate, and
   left alone — but they must still signal it. */
:where(input, textarea, select):focus-visible {
  outline: 2px solid var(--brown-ink);
  outline-offset: 1px;
}

/* Visible to assistive tech only. Defined here because this app never had it —
   portal.css has carried its own copy since the portal port, and the readiness
   checklist needed one the moment its ticks became decoration. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── Live now, as columns ──────────────────────────────────────────────────
   The one screen read under time pressure. As stacked cards, "who is running
   out of time" and "whose work has stopped reaching us" could only be answered
   by reading every row top to bottom; both are properties of a column. */
.lv-list { display: grid; gap: 6px; }
.lv-head, .lv-row {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) 190px 150px;
  gap: 14px 14px;
  /* start, not center: a row that grows a second line for an integrity note
     would otherwise float its clock to the middle of the taller row, away
     from the name it belongs to. */
  align-items: start;
}
.lv-head {
  padding: 0 16px 2px; font-size: 11.5px; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--text-secondary);
}
.lv-row {
  padding: 11px 16px; background: var(--warm-white);
  border: 1px solid var(--border-card); border-radius: var(--radius-md);
}
.lv-who { min-width: 0; }
.lv-name { font-weight: 700; }
.lv-what { font-size: var(--text-sm); color: var(--text-secondary); margin-top: 2px; }
.lv-prog { min-width: 0; }
.lv-clock-cell { justify-self: end; text-align: right; }
/* The integrity line is prose about the row, not a fact in a column, so it
   spans — and it only appears when something actually happened. */
.lv-breach { grid-column: 1 / -1; }
@media (max-width: 1200px) {
  .lv-head { display: none; }
  .lv-row { grid-template-columns: minmax(0, 1fr) auto; }
  .lv-prog { grid-column: 1 / -1; }
}

/* ── THE STATE CHIP, SHARED ────────────────────────────────────────────────
   A small pill that names a state inside a row: "2 to mark", "Assessed",
   "yours", "You were last here", "Too few to call". Six of them had grown
   separately and disagreed by a pixel or two in every direction — 11 vs 11.5
   vs 12px, three paddings, two font weights — which is exactly the kind of
   difference nobody can name and everybody feels.
   Geometry once, here; each chip keeps only its COLOURS, which are the part
   that carries meaning.

   NOT folded in, and deliberately:
     · .category-badge is square-cornered — a LABEL (a skill tag, a category),
       not a state. The shape is the distinction.
     · .tag-chip is interactive and lives in a filter bar, so it is bigger on
       purpose: it has to be clickable.
     · .status-pill is the lifecycle marker, uppercase and heavier, and is
       meant to out-shout everything here.
     · .pr-state sits in its own column as the row's verdict rather than
       beside something, and is sized for that. */
.job-chip, .tr-lasthere, .gp-state, .ia-tag, .pshow, .sd-owner, .sw-chip {
  display: inline-block; font-size: var(--text-xs); font-weight: 700;
  border-radius: 999px; padding: 2px 9px; white-space: nowrap;
}

/* ── THE PAGE IT CAME OFF, in the question editor ──────────────────────────
   Folded away, not open. In import review the page is beside every question
   because checking the parse IS the task; here the task is authoring, and a
   full page of somebody else's paper permanently open beside the editor would
   push the fields you came for off the screen. One click when the note above
   makes you want to look, closed the rest of the time.

   Only ever drawn for a question that still carries import notes, and the file
   is deleted when they are cleared. */
.imp-page-peek { margin-top: 10px; }
.imp-page-peek > summary {
  cursor: pointer; font-size: var(--text-sm); font-weight: 700;
  color: var(--state-warn-text); list-style: none; display: inline-block;
  padding: 3px 9px; border-radius: 999px; border: 1px solid var(--state-warn-border);
  background: var(--warm-white);
}
.imp-page-peek > summary::-webkit-details-marker { display: none; }
.imp-page-peek > summary:hover { background: var(--sand-soft); }
.imp-page-peek img {
  display: block; margin-top: 10px; max-width: 100%; max-height: 60vh;
  border: 1px solid var(--border-card); border-radius: var(--radius-sm);
  background: var(--warm-white);
}

/* ── THE CAPTURED DIAGRAM, in import review ────────────────────────────────
   Sits between the question's words and its options, which is where it will
   sit in the paper. Bounded in height: a full-page figure would otherwise push
   the options off screen and turn a review list into a scroll. */
.imp-figs { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; padding: 0 16px 10px 40px; }
.imp-fig { margin: 0; max-width: 320px; }
.imp-fig img {
  display: block; max-width: 100%; max-height: 220px;
  border: 1px solid var(--border-card); border-radius: var(--radius-sm); background: var(--warm-white);
}
.imp-fig figcaption {
  margin-top: 4px; font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-secondary);
}
/* A diagram the importer KNOWS is there and could not take. Warn-tinted rather
   than bad-tinted: it is a job, not a breakage, and the question still imports. */
.imp-fig-missing {
  flex: 1 1 100%; margin: 0; padding: 8px 12px; border-radius: var(--radius-sm);
  background: var(--state-warn-bg); border: 1px solid var(--state-warn-border);
  color: var(--state-warn-text); font-size: var(--text-sm); line-height: 1.5;
}

/* ── SET THE WORK, on a Horizon week ───────────────────────────────────────
   The only forward-looking panel in the Horizon area. It borrows the panel
   bones from .sd-panel rather than inventing a seventh card, and adds only
   the row: a checkbox, a name, where they have been, and what that means. */
.sw-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 12px 16px; background: var(--state-warn-bg);
  border: 1px solid var(--border-card); border-bottom: 0;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}
.sw-head h3 { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: var(--text-xl); }
.sw-count { margin-left: auto; font-size: var(--text-sm); color: var(--text-secondary); }
.sw-count b { color: var(--state-warn-text); }
.sw-body {
  border: 1px solid var(--border-card); border-radius: 0 0 var(--radius-md) var(--radius-md);
  background: var(--warm-white); padding: 4px 16px 14px;
}
.sw-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center; gap: 10px; padding: 9px 0;
  border-top: 1px solid var(--hairline); cursor: pointer;
}
.sw-list > .sw-row:first-child { border-top: 0; }
.sw-row:hover { background: var(--parchment); }
.sw-name { font-size: var(--text-md); font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.sw-when { font-size: var(--text-sm); color: var(--text-secondary); }
/* Someone who stopped before last week reads quieter, because the row is a
   question rather than an oversight — and it starts unticked to match. */
.sw-row.is-drifted .sw-name { font-weight: 400; color: var(--text-secondary); }
.sw-chip { background: var(--sand-soft); color: var(--text-secondary); }
.sw-chip.is-recent {
  background: var(--state-warn-bg); border: 1px solid var(--state-warn-border);
  color: var(--state-warn-text);
}
.sw-actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 14px; }
.sw-note { margin: 0; flex: 1 1 240px; font-size: var(--text-sm); line-height: 1.5; color: var(--text-secondary); }
@media (max-width: 700px) {
  .sw-row { grid-template-columns: auto minmax(0, 1fr); }
  .sw-when, .sw-chip { grid-column: 2; }
}

/* ── THE TABLE, SHARED ─────────────────────────────────────────────────────
   Five screens draw the same object: one bordered card, a sand header naming
   each column once, hairline-separated rows, a hover. They were five copies of
   that, and had already drifted — 13px padding on two of them and 14px on
   three, one row bordered in --sand where the rest used --hairline, and two
   tables with no hover at all, so an Overview row felt dead while the Tests row
   above it lit up.

   Declared ONCE here. A screen now adds only its own column tracks, which is
   the promise ".gp-*" below already made and this family had quietly stopped
   keeping.

   NOT merged with .gp-*, which is a different object and should stay one: that
   is a list of separate rounded cards with gaps between them, this is a single
   table with ruled rows. Two legitimate patterns, not one pattern written
   twice. Whether the .gp-* screens should BECOME tables is a design question,
   not a refactor.

   Two traps preserved from the originals:
     · align-items is START where a row can grow a second line (Tests,
       Students) — otherwise the last column floats into the middle of the
       extra space. Rows that are always one line stay centred.
     · the head is END-aligned so its uppercase labels sit on the baseline of
       the tallest column heading. */
.tr-table, .roster-table, .sit-table, .ov-table, .hz-sk-table,
.cs-table {
  border: 1px solid var(--border-card); border-radius: var(--radius-md);
  background: var(--warm-white); overflow: hidden;
}
.tr-head, .tr-row, .roster-head, .roster-row, .sit-head, .sit-row,
.ov-head, .ov-row, .hz-sk-head, .hz-sk-row, .cs-head, .cs-row {
  display: grid; gap: 16px; padding: 14px 18px; align-items: center;
}
/* START where a row can grow a second line — Tests carries a shape line under
   the set name, Students a parent-and-cohort line under the student's. Centred,
   their last column floats into the middle of the extra space.
   Declared HERE rather than beside each screen's tracks: this file loads last,
   so an override written in 04-lists.css loses to the rule above it. */
.tr-row, .roster-row, .cs-row { align-items: start; }
.tr-head, .roster-head, .sit-head, .ov-head, .hz-sk-head, .cs-head {
  align-items: end; padding: 10px 18px;
  background: var(--sand-soft); border-bottom: 1px solid var(--border-card);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--text-secondary);
}
.tr-row, .roster-row, .sit-row, .ov-row, .hz-sk-row,
.cs-row { border-top: 1px solid var(--hairline); }
.tr-body > .tr-row:first-child, .roster-body > .roster-row:first-child,
.sit-body > .sit-row:first-child, .ov-row:first-child, .hz-sk-row:first-child,
.cs-body > .cs-row:first-child { border-top: 0; }
.tr-row:hover, .roster-row:hover, .sit-row:hover,
.ov-row:hover, .hz-sk-row:hover, .cs-row:hover { background: var(--parchment); }
/* A tinted row keeps its tint through hover — the tint is a state, not a
   hover affordance, and letting hover wash it out reads as the state clearing. */
.tr-row.is-lasthere:hover, .roster-row.is-job:hover,
.hz-sk-row.is-weak:hover, .cs-row.is-ready:hover { background: var(--state-attention-bg); }

/* The stacking rules live HERE, not beside each screen's tracks, for the same
   reason the alignment does: a media query adds no specificity, so a
   `display: none` written in an earlier stylesheet loses to the plain
   `display: grid` in the shared bones above. Tests and Students spent a while
   with their headers never hiding and their columns running off a 768 screen
   because of exactly that. One grammar, one home. */
@media (max-width: 1200px) {
  /* The header cannot survive here — five named tracks in 700px gives the set
     name nine characters. Rows stack, and every cell keeps its own words,
     which is why the state was written as a word in the first place. */
  .tr-head { display: none; }
  .tr-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; }
  .tr-cat { grid-column: 1; font-size: var(--text-sm); color: var(--text-secondary); }
  .tr-state { grid-column: 1 / -1; }
  .tr-when { grid-column: 1 / -1; }
  .tr-actions { grid-column: 1 / -1; justify-content: flex-start; flex-wrap: wrap; }
}

@media (max-width: 1200px) {
  .roster-head { display: none; }
  .roster-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; }
  .roster-job, .roster-todo, .roster-sat { grid-column: 1 / -1; }
  .roster-sat::before { content: "Papers sat: "; color: var(--text-secondary); }
  .roster-todo::before { content: "Their to-do: "; color: var(--text-secondary); }
  .roster-actions { grid-column: 1 / -1; justify-content: flex-start; flex-wrap: wrap; }
}

/* ── The grammar, shared ───────────────────────────────────────────────────
   Live now, the question list and the Plus list each grew their own copy of
   the same idea: fixed tracks, a header naming them once, a state as a word in
   its own column. That is three copies of one rule and the reason the app grew
   two names for an action bar. These are the shared bones; a screen adds only
   its own column widths.

   Two traps, both paid for once already:
     · align-items must be START. A row that grows a second line floats its
       last column into the middle of the extra space otherwise.
     · a full-width note must be appended AFTER the last column, or it takes
       the next grid row and pushes that column down out of its own row. */
.gp-list { display: grid; gap: 6px; }
.gp-head {
  padding: 0 16px 2px; font-size: 11.5px; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--text-secondary);
}
.gp-row {
  padding: 11px 16px; background: var(--warm-white);
  border: 1px solid var(--border-card); border-radius: var(--radius-md);
}
.gp-head, .gp-row { display: grid; gap: 14px; align-items: start; }
.gp-name { font-weight: 700; min-width: 0; }
.gp-muted { font-size: 13px; color: var(--text-secondary); min-width: 0; overflow-wrap: anywhere; }
/* A number column reads right-aligned, and its heading has to agree with it —
   a left-aligned "OUT OF" over right-aligned figures is two columns pretending
   to be one. */
.gp-num { text-align: right; }
/* A state is a word, not a tint alone — the tint is the second signal. */
.gp-state { margin-left: 8px; background: var(--sand-soft); border: 1px solid var(--border-card); color: var(--text-secondary); }
.gp-row.is-off { background: var(--parchment); }
.gp-row.is-off .gp-name { color: var(--text-secondary); }

/* Staff: name, email, role, last seen, actions. */
/* A floor on the actions track, for the reason written under .gp-rubric
   below: the head's action cell is empty, so `auto` made it 0 there and 143px
   on the rows, and "Last signed in" sat 144px right of the dates beneath it.
   Measured at 1400px, and 150px is the widest that cell gets (Edit plus
   Switch off). */
/* A FIXED actions column. It was minmax(150px, auto), and each row is its
   own grid, so a row with three buttons (Edit, Switch off, Delete) sized
   its column wider than a row with two and wider than the header's empty
   cell: the columns stopped lining up and Delete ran past the card's edge
   the day it was added (20 Sep 2026). 236px holds the three at their
   widest; .band-actions wraps if a fourth ever arrives. */
.gp-staff { grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr) 90px 150px 236px; }
@media (max-width: 1200px) {
  .gp-head { display: none; }
  .gp-staff { grid-template-columns: minmax(0, 1fr) auto; }
  .gp-staff .gp-muted { grid-column: 1 / -1; }
}

/* Rubrics: name, criteria, total, whether the criteria carry bands. */
/* The last track is the actions, and it CANNOT be `auto`. The header row's
   action cell is empty, so auto sized it to 0 there and to 213px on the data
   rows — two separate grids disagreeing by the width of three buttons, which
   put "Out of" 214px away from the number underneath it. Measured at 1400px:
   header tracks 373/435/80/160/0, row tracks 274/319/80/160/213. A floor, so
   both agree and the buttons can still grow if one is ever added. */
.gp-rubric { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.4fr) 80px 160px minmax(220px, auto); }
.gp-sub { display: block; font-size: var(--text-sm); font-weight: 400; color: var(--text-secondary); margin-top: 2px; }
@media (max-width: 1000px) {
  .gp-rubric { grid-template-columns: minmax(0, 1fr) auto; }
  .gp-rubric .gp-muted { grid-column: 1 / -1; }
}

/* Import log: file, where it went, how many of how many, what it left behind.
   The "what the importer said" fold spans, and is appended AFTER the last
   column so it cannot steal that column's row. */
.gp-import { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1.2fr) 140px minmax(0, 220px); }
.gp-flags { display: flex; gap: 5px; flex-wrap: wrap; justify-content: flex-end; }
.gp-detail { grid-column: 1 / -1; margin-top: 6px; }
@media (max-width: 1000px) {
  .gp-import { grid-template-columns: minmax(0, 1fr) auto; }
  .gp-import .gp-muted { grid-column: 1 / -1; }
}

/* The page a dropped line was printed on. Without it, "a sentence went
   missing" is a search of the whole PDF. */
.imp-line-page {
  display: inline-block; min-width: 26px; margin-right: 8px;
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 700;
  color: var(--text-secondary); background: var(--sand-soft);
  border: 1px solid var(--border-card); border-radius: 4px;
  padding: 0 5px; text-align: center;
}

/* ── Student detail, ordered for the phone call ────────────────────────────
   Trajectory, then what is outstanding on both sides, then how to reach them,
   then the record. The old page led with the portal mirror and the link card
   — two things you FETCH — and put the only question the call opens with
   ("how is she going?") below both, unanswered, because there was no
   trajectory at all. */
.sd-panel {
  border: 1px solid var(--border-card); border-radius: var(--radius-md);
  background: var(--warm-white); overflow: hidden; margin-bottom: 20px;
}
.sd-panel-head {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 14px 20px; border-bottom: 1px solid var(--sand);
}
.sd-panel-head h3 { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: var(--text-xl); }
/* The head of a panel that is about work waiting, tinted the same as the rows
   the roster gives a job. */
.sd-panel-head.is-attention { background: var(--state-attention-bg); }
.sd-panel-sub { font-size: var(--text-sm); color: var(--text-secondary); }
.sd-latest { margin-left: auto; font-size: var(--text-sm); font-weight: 700; color: var(--brown-ink); }
.sd-count {
  font-size: 13px; font-weight: 700; color: var(--state-warn-text);
}

/* One control, two states, and only ever one view on screen. Disabled rather
   than hidden when a percentile cannot be computed: a control that vanishes
   leaves you wondering whether the screen has it, and the title says why. */
.sd-toggle { display: flex; border: 1px solid var(--border-card); border-radius: 8px; overflow: hidden; }
.sd-toggle button {
  font-family: var(--font-ui); font-size: 12px; font-weight: 700;
  color: var(--brown-ink); background: transparent; border: 0;
  border-left: 1px solid var(--border-card); padding: 7px 12px; cursor: pointer;
}
.sd-toggle button:first-child { border-left: 0; }
.sd-toggle button.is-on { color: var(--parchment); background: var(--deep-ink); }
.sd-toggle button:disabled { color: var(--text-muted); cursor: not-allowed; }

.sd-traj-body { display: grid; grid-template-columns: minmax(0, 1fr) 300px; }
.sd-traj-main { padding: 18px 20px 12px; min-width: 0; }
.sd-chart { width: 100%; height: 200px; display: block; }
.sd-chart-note { font-size: 12px; line-height: 1.6; color: var(--text-muted); margin: 6px 0 0; }
.sd-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 6px; font-size: 12px; color: var(--brown-ink); }
.sd-legend span { display: flex; align-items: center; gap: 7px; }
.sd-key-line { width: 22px; height: 2.5px; background: var(--correct-border); display: inline-block; }
.sd-key-square { width: 9px; height: 9px; border: 2px solid var(--flag-gold); display: inline-block; }

/* The per-paper bars, which work from the FIRST sitting — so this side is
   never empty even when the chart beside it cannot be drawn. */
.sd-bypaper { border-left: 1px solid var(--sand); padding: 18px 20px; min-width: 0; }
.sd-label {
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--text-muted);
}
.sd-bars { display: grid; gap: 11px; margin: 12px 0 0; }
.sd-bar-row { display: grid; grid-template-columns: minmax(0, 120px) 1fr 46px; gap: 10px; align-items: center; }
.sd-bar-name { font-size: 13px; min-width: 0; overflow-wrap: anywhere; }
.sd-bar { height: 8px; background: var(--sand); border-radius: 4px; overflow: hidden; }
.sd-bar i { display: block; height: 8px; }
.sd-bar-num { font-size: var(--text-sm); color: var(--brown-ink); text-align: right; }

/* Under a floor: the sittings listed, with the reason. */
.sd-thin { display: grid; gap: 0; }
.sd-thin-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 150px 90px 90px;
  gap: 16px; align-items: center; padding: 10px 2px; border-top: 1px solid var(--sand);
  font-size: 13px; color: var(--brown-ink);
}
.sd-thin-row > div:first-child { color: var(--text-primary); min-width: 0; overflow-wrap: anywhere; }
.sd-thin-head {
  border-top: 0; padding-top: 0; font-size: var(--text-xs); font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--text-muted);
}
.sd-thin-head > div:first-child { color: var(--text-muted); }

/* Outstanding beside How to reach them. */
.sd-cols { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 20px; align-items: start; }
.sd-cols > * { min-width: 0; }
.sd-out-head, .sd-out-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 150px 128px; gap: 16px; padding: 12px 20px;
}
.sd-out-head {
  padding-bottom: 6px; font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--text-muted);
}
.sd-out-row { align-items: center; border-top: 1px solid var(--sand); }
.sd-out-what { font-size: 14px; font-weight: 600; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.sd-out-detail { font-size: var(--text-sm); color: var(--text-secondary); margin-top: 2px; }
.sd-out-since { font-size: 13px; color: var(--brown-ink); }
.sd-out-act { display: flex; justify-content: flex-end; }
/* WHOSE job it is, as a word. "She owes me a paper" and "I owe her a mark"
   were levelled into one undifferentiated run of chips before this. */
.sd-owner.is-you { color: var(--state-warn-text); background: var(--state-warn-bg); border: 1px solid var(--flag-gold); }
.sd-owner.is-them { color: var(--text-secondary); background: var(--sand-soft); border: 1px solid var(--border-card); }

.sd-reach-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 14px; }
.sd-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.sd-rule { height: 1px; background: var(--sand); }
.sd-value { font-size: 14px; }
.sd-sub { font-size: var(--text-sm); line-height: 1.55; color: var(--brown-ink); overflow-wrap: anywhere; }
.sd-sub.is-missing { color: var(--state-warn-text); }
.sd-reach-body .portal-link-card { margin: 0; padding: 0; border: 0; background: none; }
.sd-reach-body .portal-link-head { margin: 0; }
.sd-reach-body .portal-link-row code { flex: 1 1 100%; font-size: 11.5px; }

/* A button that is a sentence's worth of link, not a control. */
.link-btn {
  background: none; border: 0; padding: 0; font: inherit; font-weight: 700;
  color: inherit; text-decoration: underline; cursor: pointer;
}
.btn-small { padding: 5px 10px; font-size: 12px; }

/* ── Every sitting ────────────────────────────────────────────────────────
   A record, not a task, so it sits below the three panels that are. Six
   columns, and the behavioural read of each sitting — pacing, flags, changes,
   the end-of-paper rush — folds open under its row rather than being lost:
   the table is what you scan, that is what you read when one row stops you. */
.sd-sittings-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin: 4px 0 10px; }
.sd-sittings-head h3 { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: var(--text-xl); }
.sd-sittings-head .hint-text { margin: 0; flex: 1 1 320px; }
.sit-head, .sit-row { grid-template-columns: minmax(0, 1fr) 124px 100px 84px 96px 96px; }
/* The only row in this family that opens something on click. */
.sit-row { cursor: pointer; }
.sit-paper { font-size: 14px; font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.sit-kind, .sit-marks, .sit-time, .sit-when { font-size: 13px; color: var(--brown-ink); }
.sit-score { font-size: 13px; font-weight: 700; }
/* Appended AFTER the last column so it cannot steal that column's grid row —
   the same trap .gp-detail is commented for. */
.sit-detail {
  grid-column: 1 / -1; margin-top: 4px; padding-top: 8px;
  border-top: 1px dashed var(--border); display: flex; gap: 12px;
  align-items: baseline; flex-wrap: wrap;
}

/* TABLES, at the table breakpoint: a multi-column table needs about 200px for
   the name it is sorted by, and below that it should already be stacked rows
   rather than a table pretending it still fits. */
@media (max-width: 1200px) {
  .sit-head { display: none; }
  .sit-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 6px; }
  .sit-kind, .sit-marks, .sit-score, .sit-time, .sit-when { grid-column: auto; }
  .sd-out-head { display: none; }
  .sd-out-row { grid-template-columns: minmax(0, 1fr); row-gap: 8px; }
  .sd-out-act { justify-content: flex-start; }
}
/* PANES, at the pane breakpoint — two columns of content side by side survive
   narrower than a five-column table does, so they collapse later. */
@media (max-width: 1000px) {
  .sd-traj-body { grid-template-columns: minmax(0, 1fr); }
  .sd-bypaper { border-left: 0; border-top: 1px solid var(--sand); }
  .sd-cols { grid-template-columns: minmax(0, 1fr); }
}

/* ── Marks by skill tag ────────────────────────────────────────────────────
   Two columns that must never become one number: practice papers are
   self-chosen and re-sittable, assessed papers are one attempt under exam
   conditions, and an average across both describes the choosing as much as
   the child. Side by side they can be compared without being blended. */
.sk-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 0; }
.sk-col { padding: 16px 20px; border-left: 1px solid var(--sand); min-width: 0; }
.sk-col:first-child { border-left: 0; }
.sk-rows { display: grid; gap: 10px; }
.sk-row { display: grid; grid-template-columns: minmax(0, 1fr) 90px 74px; gap: 10px; align-items: center; }
.sk-name { font-size: 13px; min-width: 0; overflow-wrap: anywhere; }
/* Which paper the skill was met on. A tag is not owned by one subject —
   "inference" turns up in Reading and in Thinking Skills — so a weak tag
   without this sends a tutor to the wrong paper. */
.sk-where { display: block; font-size: 11.5px; color: var(--text-secondary); margin-top: 1px; }
.sk-bar { height: 8px; background: var(--sand); border-radius: 4px; overflow: hidden; }
.sk-bar i { display: block; height: 8px; }
/* A COUNT, never a bare percentage. "24 of 31 right" cannot be misread; "77%"
   over four questions can. */
.sk-num { font-size: var(--text-sm); color: var(--brown-ink); text-align: right; }
@media (max-width: 700px) {
  .sk-col { border-left: 0; border-top: 1px solid var(--sand); }
  .sk-col:first-child { border-top: 0; }
}

/* ── Horizon: one student's skills on one exam ─────────────────────────────
   The bar and the cohort mean share ONE track. Two separate bars would make
   the reader do the subtraction; a tick on the same rail makes "below the
   middle" a shape you see before you read a number. */
.hz-sk-head, .hz-sk-row { grid-template-columns: minmax(0, 1fr) 108px minmax(160px, 300px) 148px 150px; }
/* The row carrying the result. Tinted, and the verdict beside it says the
   size — the tint alone would only say "this one", not "by how much". */
.hz-sk-row.is-weak { background: var(--state-attention-bg); }
.hz-sk-tag { font-size: 14px; font-weight: 700; }
.hz-sk-where { font-size: var(--text-sm); color: var(--text-secondary); margin-top: 2px; }
.hz-sk-mine { font-size: var(--text-md); font-weight: 700; }
.hz-sk-track { position: relative; height: 22px; }
.hz-sk-bar { position: absolute; inset: 7px 0; background: var(--sand); border-radius: 4px; overflow: hidden; }
.hz-sk-bar i { display: block; height: 8px; border-radius: 4px; }
.hz-sk-tick { position: absolute; top: 0; width: 2px; height: 22px; background: var(--brown-ink); }
.hz-sk-mean { font-size: 13px; color: var(--brown-ink); }
.hz-sk-verdict { font-size: var(--text-md); }
.hz-sk-verdict.is-below { font-weight: 700; color: var(--state-warn-text); }
.hz-sk-verdict.is-above { font-weight: 700; color: var(--correct-text); }
.hz-sk-verdict.is-with { color: var(--brown-ink); }
.hz-sk-verdict.is-quiet { color: var(--text-muted); }
.hz-sk-key {
  display: flex; gap: 18px; flex-wrap: wrap; align-items: center;
  padding: 10px 18px 0; font-size: 12px; color: var(--brown-ink);
}
.hz-sk-key span { display: flex; align-items: center; gap: 7px; }
.hz-sk-key-bar { width: 22px; height: 8px; border-radius: 4px; background: var(--correct-border); display: inline-block; }
.hz-sk-key-tick { width: 2px; height: 16px; background: var(--brown-ink); display: inline-block; }

@media (max-width: 1200px) {
  .hz-sk-head { display: none; }
  .hz-sk-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; }
  .hz-sk-track { grid-column: 1 / -1; }
}

/* ── The hub's Overview tab ────────────────────────────────────────────────
   Four counts, the papers, and what stands between the set and a report going
   home. Every count NAMES THE FIELD IT COUNTED — "4 of 6" wants a footnote,
   "4 of 6 granted students" is a sentence. */
.ov-counts { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; margin-bottom: 22px; }
.ov-count {
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-md); padding: 14px 16px;
}
/* A count that IS a job, tinted like every other job on this platform. */
.ov-count.is-bad { background: var(--state-attention-bg); border-color: var(--flag-gold); }
.ov-count-n { font-family: var(--font-ui); font-weight: 700; font-size: 24px; }
.ov-count-l { font-size: var(--text-sm); font-weight: 700; margin-top: 2px; }
.ov-count-s { font-size: var(--text-sm); color: var(--text-secondary); margin-top: 3px; line-height: 1.5; }

.ov-head, .ov-row { grid-template-columns: minmax(0, 1fr) 96px 150px 150px 130px; }
.ov-paper { font-size: 14px; font-weight: 600; }
.ov-paper-sub { font-size: var(--text-sm); color: var(--text-secondary); margin-top: 2px; }
/* Where the paper came from. Quieter than the line above it — provenance is
   something you go looking for, not something to read past on the way to the
   figures. Lossy imports get the attention tint rather than the warn one: "30
   of 35 read" is a fact about how this paper was built, and if it were a
   problem it would be a problem for the person who finished it months ago. */
.ov-paper-src {
  font-size: var(--text-xs); color: var(--text-muted);
  margin-top: 3px; line-height: 1.45;
}
.ov-paper-src.is-lossy {
  color: var(--brown-ink);
  border-left: 2px solid var(--flag-gold); padding-left: 7px;
}
/* Bar THEN figure, and the figure is the carrier — the bar is a second
   reading of a number that is already written beside it, which is why it is
   aria-hidden. */
.ov-cell { font-size: 13px; color: var(--brown-ink); }
.ov-cell span { display: block; margin-top: 4px; }
.ov-bar { height: 8px; background: var(--sand); border-radius: 4px; overflow: hidden; }
.ov-bar i { display: block; height: 8px; background: var(--correct-border); border-radius: 4px; }
.ov-act { display: flex; justify-content: flex-end; }

/* Each line says what to DO, not only what is wrong. A checklist that reports
   faults and stops leaves the reader to invent every remedy. */
.ov-check {
  display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; gap: 10px;
  align-items: baseline; padding: 12px 20px; border-top: 1px solid var(--sand);
  font-size: var(--text-md); line-height: 1.55;
}
.ov-check:first-of-type { border-top: 0; }
.ov-check.is-todo { background: var(--state-attention-bg); }
.ov-tick { font-weight: 800; text-align: center; }
.ov-check.is-ok .ov-tick { color: var(--correct-text); }
.ov-check.is-todo .ov-tick { color: var(--state-warn-text); }
/* Nothing to check yet: neither green nor amber, because it is neither done
   nor outstanding. Quiet on purpose — this row is waiting for the work that
   will make it answerable, and dressing it as either of its neighbours is the
   misreport it was added to fix. */
.ov-check.is-waiting { background: transparent; color: var(--text-secondary); }
.ov-check.is-waiting .ov-tick { color: var(--warm-tan); }
.ov-check-detail { color: var(--text-secondary); }
.ov-check.is-todo .ov-check-detail { color: var(--text-primary); }

@media (max-width: 1200px) {
  .ov-head { display: none; }
  .ov-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; }
  .ov-check { grid-template-columns: 22px minmax(0, 1fr); }
  .ov-check > .tr-go { grid-column: 2; justify-self: start; }
}

/* ── A THUMB IS NOT A MOUSE POINTER ────────────────────────────────────────
   This app had no touch sizing at all: every control was drawn for a cursor
   that lands where it is aimed. The exam player already does this (see
   player/shell.css); the tutor's side never did, and the tutor is the one
   holding a phone on a train.

   44px is the floor Apple and Google both publish, and it is a MINIMUM rather
   than a size — `min-height` leaves a control that is already bigger alone,
   so nothing on a desktop moves by a pixel.

   Scoped to `pointer: coarse` and not to a width, because the question is what
   is doing the pointing. A touchscreen laptop at 1400px still has a thumb; a
   narrow window on a desktop does not. */
@media (pointer: coarse) {
  .tr-go,
  .btn-primary, .btn-secondary, .btn-ghost, .btn-small,
  /* .sidebar-link, NOT `.sidebar-nav a`. The nav is ten <button>s, so the
     descendant-`a` selector matched nothing and the whole rail — every screen
     in the app, and Sign out — sat at 40px on a touchscreen while this block
     looked like it had covered it. Second selector in here written from memory
     and found by measuring; see .tag-chip below. */
  .sidebar-link, .sidebar-theme-toggle,
  .hub-tab, .chip-count, .row-menu > summary,
  /* .tag-chip is the real class the filter chips carry — "All 1", "Live 1",
     "Archived (0)". Written as .filter-chip first, which matches nothing, and
     found by measuring rather than by reading: they were the one control left
     at 28px after everything else cleared. */
  .menu-item, .tag-chip, .test-sort select, .filter-select,
  /* Every typed field in the app, measured at 39px. A text box is a target
     before it is a box: missing it puts the caret in the one above. */
  .text-input {
    min-height: 44px;
  }
  /* NOT .tr-name, and not by oversight. It is a link inside a row that is
     itself the click target — measured at 229px tall — so a thumb has the
     whole row. Giving the link 44px would only pad a hit area that is already
     five times that. */
  /* Padding, not just height: a 44px-tall link whose text sits at the top is
     44px of box with a 20px target in it. */
  .sidebar-link, .hub-tab, .menu-item {
    display: flex; align-items: center;
  }
  /* ── Marking, where a mis-tap is a wrong mark ────────────────────────────
     Marking is the one job on this side of the app that is genuinely done on
     a tablet — an essay is read, not scanned — and it was the least sized for
     one. The band is the judgement, so .mk-band-pick is the most important
     control here; it was 41px.

     .mk-num is the one that matters most, though, and it was the smallest:
     34-37px square, six pixels apart. Tapping 11 instead of 12 is a mark a
     child keeps, and NOTHING catches it — there is no descriptor to check it
     against the way the band has, no confirm step, and the number looks just
     as deliberate either way. Every other mis-tap on this screen announces
     itself; this one does not.

     Both dimensions, not just height: these are the only square controls in
     the app, so a min-height alone would still leave a 34px-wide target. */
  .mk-band-pick { min-height: 44px; }
  .mk-num { min-width: 44px; min-height: 44px; }
  /* Wrapping to a second row is the right answer when a band spans more marks
     than fit — a wrapped row of honest targets beats one row of small ones. */
  .mk-within { gap: 10px; }
  /* SPACE BETWEEN, so the wrong one is not hit. Two 44px controls touching
     each other are one 88px control as far as a thumb is concerned, and the
     row-menu sits right beside the action a tutor actually wants. */
  .tr-actions, .roster-actions, .row-actions, .view-header-actions {
    gap: 12px;
  }
  /* A checkbox is the smallest thing on any of these screens and the easiest
     to miss. The label is the target, so it gets the height. */
  .checkbox-label { min-height: 44px; display: flex; align-items: center; gap: 10px; }
}

/* ── The student's page on a phone ─────────────────────────────────────────
   A tutor on a phone is asking one of two things: what has this child got on,
   and how do I reach them. Everything else here is a COMPARISON — a trajectory
   chart, a skill map, a table of every sitting — and comparison needs width it
   cannot have. Shown at 375px they become a column of unreadable slivers that
   push the two useful panels off the screen.

   Hidden rather than reflowed, deliberately: there is no honest way to draw a
   percentile chart 300px wide, and a chart that cannot be read is worse than
   one that admits it is not here. The mirror, Outstanding and the contact
   details stay, which is the whole of the glance. */
@media (max-width: 700px) and (pointer: coarse) {
  #student-trajectory,
  #student-skills,
  #student-pattern-panel,
  .sd-sittings-head,
  #student-history-list,
  #student-history-empty { display: none; }
}

/* ── What a student has never opened, on the tutor's map ───────────────────
   Same statement as the "Not practised yet" block on the student's own page,
   and deliberately the same shape as .skill-thin over there: this is a fact
   the figures above cannot speak for, not a warning. The attention tint, not
   warn — an untouched subject is a gap in what has been SET, which is a
   decision for the person reading this screen. */
.sk-untouched {
  margin: 0 20px 14px; padding: 12px 14px;
  background: var(--state-attention-bg); border: 1px solid var(--border-card);
  border-radius: var(--radius-md);
  display: flex; flex-direction: column; gap: 4px;
  font-size: var(--text-md); line-height: 1.6;
}
.sk-untouched .muted { font-size: var(--text-sm); color: var(--text-secondary); }

/* ── The consultation screen ───────────────────────────────────────────────
   A screen SHARED, not sent: John talks over it on a call while a family who
   are not customers yet watch. Everything here is sized for that — larger
   reading type than the rest of the app, generous stops, and one idea per
   screenful, because the person looking at it is listening at the same time.

   The reading face rather than the UI face on every sentence, because on this
   screen the sentences ARE the product. Everywhere else in the app the words
   label a control; here they are the thing being read out loud. */
.call-nav {
  position: sticky; top: 0; z-index: 3;
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 10px 0 12px; margin-bottom: 4px;
  background: var(--parchment);
  border-bottom: 1px solid var(--hairline);
}
.call-nav-a {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 12px 6px 8px; border-radius: var(--radius-pill);
  background: var(--warm-white); border: 1px solid var(--border);
  font-family: var(--font-ui); font-size: var(--text-sm); font-weight: 600;
  color: var(--text-secondary); text-decoration: none;
}
.call-nav-a:hover { border-color: var(--border-card); color: var(--text-primary); }
.call-nav-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--sand); color: var(--brown-ink);
  font-size: 10.5px; font-weight: 700;
}

/* scroll-margin, so an anchor does not land UNDER the sticky nav — the stop
   the family were sent to would otherwise be the one line they cannot see. */
.call-sec { scroll-margin-top: 64px; padding: 26px 0 30px; }
.call-sec + .call-sec { border-top: 1px solid var(--hairline); }
.call-num {
  display: block; font-family: var(--font-ui);
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--warm-tan); margin-bottom: 6px;
}
.call-h {
  margin: 0 0 10px; font-family: var(--font-reading);
  font-size: 25px; font-weight: 700; line-height: 1.25;
  color: var(--text-primary);
}
.call-lede {
  margin: 0 0 20px; max-width: 62ch;
  font-family: var(--font-reading); font-size: 16px; line-height: 1.65;
  color: var(--text-secondary);
}
.call-lede strong { color: var(--text-primary); font-weight: 600; }

.call-figs { display: flex; flex-wrap: wrap; gap: 10px; }
/* A lone paper's card must not stretch the width of the screen: one figure in
   a 1400px box reads as a banner rather than a fact. */
.call-fig {
  flex: 1 1 150px; max-width: 340px; padding: 14px 16px;
  background: var(--warm-white); border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.call-fig-n {
  display: block; font-family: var(--font-reading);
  font-size: 30px; font-weight: 700; line-height: 1;
  color: var(--text-primary); font-variant-numeric: tabular-nums;
}
.call-fig-l {
  display: block; margin-top: 6px;
  font-size: var(--text-sm); line-height: 1.45; color: var(--text-secondary);
}

/* The four papers, strongest first. The track carries quarter ticks and
   NOTHING ELSE: no band, no threshold, no cohort line. A tick at 25/50/75 is
   a ruler; a line labelled "the mark you need" would be a number we made up. */
.paper-row {
  display: grid;
  grid-template-columns: minmax(120px, 1.1fr) 108px minmax(180px, 2fr);
  gap: 4px 18px; align-items: center;
  padding: 13px 0; border-bottom: 1px solid var(--hairline);
}
.paper-name {
  font-family: var(--font-reading); font-size: 16px; font-weight: 600;
  color: var(--text-primary);
}
.paper-score {
  font-family: var(--font-reading); font-size: 21px; font-weight: 700;
  color: var(--text-primary); font-variant-numeric: tabular-nums;
  text-align: right;
}
.paper-score small {
  display: block; margin-top: 2px;
  font-size: var(--text-sm); font-weight: 400; color: var(--text-secondary);
}
.paper-track {
  position: relative; display: block; height: 12px;
  background: var(--sand-soft); border: 1px solid var(--hairline);
  border-radius: var(--radius-pill); overflow: hidden;
}
.paper-fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  background: var(--warm-stone); border-radius: var(--radius-pill);
}
.paper-fill.is-top { background: var(--sage); }
.paper-tick {
  position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--parchment); opacity: 0.85;
}
.paper-say {
  grid-column: 3; font-size: var(--text-sm); color: var(--text-secondary);
}
.paper-say:empty { display: none; }

.call-panel {
  margin-top: 20px; padding: 18px 20px;
  background: var(--sand-soft); border: 1px solid var(--border-card);
  border-radius: var(--radius-md);
}
.call-panel h4 {
  margin: 0 0 8px; font-family: var(--font-reading);
  font-size: var(--text-xl); font-weight: 700; color: var(--text-primary);
}
.call-panel p {
  margin: 0 0 10px; max-width: 62ch;
  font-family: var(--font-reading); font-size: 15px; line-height: 1.65;
  color: var(--text-secondary);
}
.call-panel p:last-child { margin-bottom: 0; }
.call-panel strong { color: var(--text-primary); font-weight: 600; }

.call-item {
  display: grid; grid-template-columns: minmax(0, 1.3fr) 170px minmax(0, 1.4fr);
  gap: 4px 18px; align-items: baseline;
  padding: 11px 0; border-bottom: 1px solid var(--hairline);
}
.call-item-n {
  font-family: var(--font-reading); font-size: 15.5px; font-weight: 600;
  color: var(--text-primary);
}
.call-item-t {
  font-size: var(--text-md); color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.call-item-d { font-size: var(--text-sm); color: var(--text-secondary); }

.call-plan {
  display: grid; grid-template-columns: 32px minmax(0, 1fr);
  gap: 14px; align-items: start;
  padding: 16px 0; border-bottom: 1px solid var(--hairline);
}
.call-plan-n {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--sand); color: var(--brown-ink);
  font-family: var(--font-reading); font-size: 15px; font-weight: 700;
}
.call-plan-where {
  display: inline-block; margin-bottom: 5px; padding: 2px 9px;
  border-radius: var(--radius-pill); background: var(--warm-white);
  border: 1px solid var(--border);
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-secondary);
}
.call-plan-t {
  font-family: var(--font-reading); font-size: 17px; font-weight: 700;
  line-height: 1.35; color: var(--text-primary);
}
.call-plan-d {
  margin: 5px 0 0; max-width: 62ch;
  font-family: var(--font-reading); font-size: 15px; line-height: 1.65;
  color: var(--text-secondary);
}

/* Shared on a call, which is a window that is rarely full width. Below this
   the three-column rows stack rather than crushing the sentence that explains
   the figure into two words a column. */
@media (max-width: 900px) {
  .paper-row, .call-item {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .paper-track, .paper-say, .call-item-d { grid-column: 1 / -1; }
  .call-h { font-size: 22px; }
}


/* ── Consultations ─────────────────────────────────────────────────────────
   The packs list, on the shared table bones above. A pack whose papers are all
   back is a JOB — a call to book — so it carries the attention tint the roster
   uses for the same meaning, rather than a colour of its own. */
.cs-head, .cs-row {
  grid-template-columns: minmax(140px, 1.2fr) minmax(0, 1.6fr) 90px 110px 150px;
  cursor: pointer;
}
.cs-head { cursor: default; }
.cs-row.is-ready { background: var(--state-attention-bg); }
.cs-name {
  display: block; font-family: var(--font-reading);
  font-size: var(--text-lg); font-weight: 600; color: var(--text-primary);
  text-decoration: none;
}
/* It is a link, so it says so on hover and takes a focus ring like every
   other one. Underlined only on hover: a table of names all underlined reads
   as a list of warnings. */
.cs-name:hover { text-decoration: underline; }
.cs-sub { font-size: var(--text-sm); line-height: 1.45; color: var(--text-secondary); }
.cs-count { font-size: var(--text-md); font-variant-numeric: tabular-nums; }

/* The paper picker in the new-consultation dialog. Scrolls rather than growing
   the card past the screen: there are forty tests and there will be more. */
.pick-list {
  max-height: 260px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--warm-white);
}
.pick-row {
  display: grid; grid-template-columns: 20px minmax(0, 1fr) auto;
  gap: 10px; align-items: baseline; padding: 9px 12px; cursor: pointer;
}
.pick-row + .pick-row { border-top: 1px solid var(--hairline); }
.pick-row:hover { background: var(--parchment); }
.pick-name { font-size: var(--text-md); color: var(--text-primary); }
.pick-meta { font-size: var(--text-xs); color: var(--text-muted); }

/* WHAT IS NOT BACK YET, on the screen where you noticed. The alternative is a
   hunt through a test's Access tab for which of thirty codes belongs to this
   family, which is why the links are here rather than a note telling you to go
   and find them. */
.call-waiting {
  margin-top: 20px; padding: 16px 18px;
  background: var(--state-attention-bg); border: 1px solid var(--border-card);
  border-radius: var(--radius-md);
}
.call-waiting h4 {
  margin: 0 0 6px; font-family: var(--font-reading);
  font-size: var(--text-lg); font-weight: 700; color: var(--text-primary);
}
.call-waiting > p {
  margin: 0 0 12px; max-width: 62ch;
  font-family: var(--font-reading); font-size: 14.5px; line-height: 1.6;
  color: var(--text-secondary);
}
.call-wait-row {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) auto;
  gap: 12px; align-items: center; padding: 8px 0;
  border-top: 1px solid var(--hairline);
}
.call-wait-n { font-size: var(--text-md); font-weight: 600; color: var(--text-primary); }
.btn-sm { padding: 4px 10px; font-size: var(--text-sm); }
/* Stands in for the button when the browser refuses the clipboard: the link
   itself, selected, where the button was. */
.copy-fallback { min-width: 220px; padding: 4px 8px; font-size: var(--text-sm); }

/* A quiet link, because it is an aside: the report is the document that gets
   sent, and this screen is the conversation about it. */
.paper-report {
  display: inline-block; margin-left: 10px;
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: 0.03em; color: var(--warm-tan); text-decoration: none;
  border-bottom: 1px solid var(--border-card);
}
.paper-report:hover { color: var(--text-primary); border-bottom-color: var(--warm-tan); }

@media (max-width: 900px) {
  .cs-head { display: none; }
  .cs-row { grid-template-columns: minmax(0, 1fr) auto; }
  .call-wait-row { grid-template-columns: minmax(0, 1fr) auto; }
}

/* ── Prep: the private side of a consultation ──────────────────────────────
   Filled in DURING a phone call, so it is laid out to be typed into rather
   than read: short labelled boxes in a grid, the notes beside them, and no
   Save button to remember at the end of a call. */
.prep-grid {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 16px; margin-bottom: 16px;
}
.prep-card {
  padding: 18px 20px; margin-bottom: 16px;
  background: var(--warm-white); border: 1px solid var(--border-card);
  border-radius: var(--radius-md);
}
.prep-grid > .prep-card { margin-bottom: 0; }
.prep-card h3 {
  margin: 0 0 4px; font-family: var(--font-reading);
  font-size: var(--text-xl); font-weight: 700; color: var(--text-primary);
  display: flex; align-items: baseline; gap: 10px;
}
.prep-saved {
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--warm-tan);
}
.prep-fields {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px 14px; margin-top: 12px;
}
.prep-field { display: flex; flex-direction: column; gap: 4px; }
.prep-field .field-label { margin: 0; }
/* A field the server refused. Marked where it was typed rather than announced
   in a dialog — the box is both the message and the place to fix it. */
.field-bad { border-color: var(--state-bad-border); background: var(--state-bad-bg); }
.prep-notes {
  width: 100%; margin-top: 10px; resize: vertical;
  font-family: var(--font-reading); font-size: 14.5px; line-height: 1.6;
}
.prep-row-acts { display: flex; gap: 6px; justify-content: flex-end; }
.prep-add {
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--hairline);
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
}
.prep-add .pick-list, .prep-add .text-input { width: 100%; }

@media (max-width: 900px) {
  .prep-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ── A screen's introduction ───────────────────────────────────────────────
   UNDER THE HEADING, AND THE FULL WIDTH OF THE PAGE. John, 21 Aug, after
   three attempts at putting it beside the title: "the position of the test
   should always be under the heading and not aligned left. it should stretch
   and go to the width of the page, right now, it is limited."

   The history, because it was expensive and the wrong turn is instructive.
   These paragraphs used to run the full width of a 1500px window — 200
   characters a line — so a max-width of 78ch was added. At 12px that is
   465px, which on a 1016px column left 551px of void beside a paragraph with
   a full-width table underneath it: a correctly-set measure reading as an
   accident. The fix attempted was to give it a partner — two columns, then
   three so a primary button could sit on the row. Each version looked more
   deliberate and none of them was what was wanted, because the thing being
   solved was never the measure. It was that the paragraph did not line up
   with anything.

   So it lines up with everything: same left edge as the heading above it and
   the table below, and the same width. Lines are long — about 180 characters
   at the widest, since .main caps at 1280px — and that is the accepted cost
   of a column that agrees with the rest of the screen. Nothing else on these
   screens sets its own width either.

   The type stays where it was put: --text-md rather than the hardcoded 12px
   that matched no token, for prose that explains a screen rather than
   captions a row. */
.view > .hint-text {
  font-size: var(--text-md);
  line-height: 1.6;
  color: var(--text-secondary);
}

/* A bullet list explaining a screen is the same prose in another shape, so it
   gets the same treatment. Scoped to a direct child of .view: a hint-list
   inside a panel keeps the width its panel gives it. */
.view > .hint-list {
  font-size: var(--text-md);
}

/* ── Question library: what this list is, beside how to narrow it ──────────
   The rule above deliberately skips this screen, because its paragraph is not
   the page's introduction — it describes the SELECTED TAB, which is why it
   sits below the tabs rather than beside the title. Correct, and it left the
   screen still reading as a left column with air next to it.

   The pairing here is different and better than a header would have been:
   the prose says what you are looking at, and the filters say how to narrow
   it. Those are the same question asked twice, so they belong on one band.

   Placement is by COLUMN only, never by row. Auto-placement then pairs them
   in DOM order — prose beside the state chips, the state heading beside the
   search bar — and, crucially, it stays correct when #bank-state-head is
   hidden, which it is until a filter is pressed. Pinning rows would have left
   a hole exactly then. */
/* Question library once put its paragraph in a column with the filters
   beside it. Same rule as every other screen now: the text explaining what
   you are looking at runs the full width under the tabs, and the controls
   for narrowing it sit underneath. */

/* ── The verdict on a question ─────────────────────────────────────────────
   A row of item analysis says what the numbers found; this says what a person
   concluded. Below the figures, because it is the answer to them — and quiet
   until there is one, so a screen of unjudged questions is not a wall of
   controls shouting to be used. */
.ia-verdict {
  grid-column: 1 / -1; margin: 10px 0 2px; padding-top: 10px;
  border-top: 1px dashed var(--border);
  display: flex; flex-wrap: wrap; gap: 8px 18px;
  align-items: baseline; justify-content: space-between;
}
.ia-verdict-said { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: baseline; }
.ia-verdict-none { font-size: var(--text-sm); color: var(--text-muted); }
.ia-verdict-who { font-size: var(--text-xs); color: var(--text-muted); }
.ia-verdict-note {
  flex-basis: 100%; font-family: var(--font-reading);
  font-size: var(--text-sm); line-height: 1.5; color: var(--text-secondary);
}
.ia-verdict-acts { display: flex; flex-wrap: wrap; gap: 6px; }
/* The one in force. A pressed state, not a colour: which verdict is set is
   already said in words beside it, and two ways of saying it is one too many. */
.ia-verdict-acts .btn-sm.is-on {
  background: var(--deep-ink); border-color: var(--deep-ink); color: var(--warm-white);
}

/* ── Skip to the main content ──────────────────────────────────────────────
   Off-screen until it takes focus, then the first thing on the page. It is a
   real link to a real region, not a scroll handler: `main` carries
   tabindex="-1" so focus actually lands there and the next Tab continues INTO
   the work rather than back at the top of the sidebar. */
.skip-link {
  position: absolute; left: 8px; top: -60px; z-index: 400;
  padding: 10px 16px; border-radius: var(--radius-sm);
  background: var(--deep-ink); color: var(--warm-white);
  font-family: var(--font-ui); font-size: var(--text-md); font-weight: 600;
  text-decoration: none; transition: top 120ms ease;
}
.skip-link:focus { top: 8px; }
/* The target must not draw a focus ring of its own when reached this way —
   it is a region, and the eye is already where it needs to be. */
.main:focus { outline: none; }
@media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }
