/* public/styles.css — shared contract §19.
 *
 * Flat BEM: block__element--modifier, lowercase-hyphen, no utility classes, no prefix.
 * The class vocabulary below is complete — groups use NO class outside it. If a group needs
 * something new it reuses an existing class or wraps the thing in .card.
 *
 * System-UI font stack only: zero webfont requests, so the panel renders instantly and works
 * with no network at all. No @import, no CDN, no framework: the CSP is default-src 'self' and
 * every one of those would be a blocked request in a candidate's browser.
 *
 * TWO SIDES, ONE FILE. Everything from "candidate side" downwards is scoped to `body.candidate`,
 * which views/layout.js puts on every nav:'portal' page and nowhere else. The admin screens are
 * untouched by it on purpose — the operator's own tool is used on a laptop and he did not ask
 * for it to change.
 *
 * THE CANDIDATE SIDE IS A PHONE. Its candidates come out of a Discord server and do the whole
 * process, including recording and uploading a screen capture, on the device in their hand. The
 * rules that follow from that, applied throughout:
 *   * nothing may scroll sideways at 320px — flex children carry min-width:0, long words wrap,
 *     and body.candidate clips as a backstop;
 *   * form controls are 16px, because iOS Safari zooms the whole page when a control smaller
 *     than that takes focus, and a page that zooms on the first tap feels broken;
 *   * anything tappable is at least 44px tall with real separation;
 *   * layout() sets viewport-fit=cover, so the safe-area insets below are load-bearing rather
 *     than decorative: without them the header sits under the notch in landscape and the quiz
 *     bar sits under the home indicator.
 */

:root {
  --bg: #f6f7f9;
  --surface: #ffffff;
  --text: #16181d;
  --muted: #666d7a;
  --border: #dfe3e9;
  --accent: #2f5fe0;
  --ok: #17794a;
  --warn: #9a6100;
  --danger: #c02626;

  --radius: 8px;

  --gap-1: 4px;
  --gap-2: 8px;
  --gap-3: 12px;
  --gap-4: 16px;
  --gap-5: 24px;
  --gap-6: 32px;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14161a;
    --surface: #1c1f25;
    --text: #e8eaee;
    --muted: #9aa2b1;
    --border: #2c313a;
    --accent: #6f95f5;
    --ok: #4cc38a;
    --warn: #e0a33a;
    --danger: #f06a6a;
  }
}

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
}

a { color: var(--accent); }

/* ---------------------------------------------------------------- layout */

.app-shell { min-height: 100vh; }

/* max() against the safe-area insets, not a bare env(): on a device with no notch every inset
   is 0 and the padding is exactly what it always was. viewport-fit=cover is what makes them
   non-zero, and it is set in views/layout.js. */
.topnav {
  display: flex;
  align-items: center;
  gap: var(--gap-4);
  flex-wrap: wrap;
  padding: var(--gap-3) var(--gap-4);
  padding-top: max(var(--gap-3), env(safe-area-inset-top));
  padding-left: max(var(--gap-4), env(safe-area-inset-left));
  padding-right: max(var(--gap-4), env(safe-area-inset-right));
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.topnav__brand {
  font-weight: 650;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

.topnav__links {
  display: flex;
  gap: var(--gap-3);
  flex-wrap: wrap;
  align-items: center;
}

.topnav__link {
  color: var(--muted);
  text-decoration: none;
  padding: var(--gap-1) var(--gap-2);
  border-radius: var(--radius);
  white-space: nowrap;
}
.topnav__link:hover { color: var(--text); background: var(--bg); }
.topnav__link--active { color: var(--text); background: var(--bg); font-weight: 600; }

.topnav__right { margin-left: auto; }

.page {
  max-width: 1400px;
  margin: 0 auto;
  padding: var(--gap-5) var(--gap-4) var(--gap-6);
  padding-left: max(var(--gap-4), env(safe-area-inset-left));
  padding-right: max(var(--gap-4), env(safe-area-inset-right));
  padding-bottom: calc(var(--gap-6) + env(safe-area-inset-bottom));
}

.page__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-4);
  flex-wrap: wrap;
  margin-bottom: var(--gap-4);
}

.page__title { font-size: 22px; margin: 0; font-weight: 650; }

.page__actions { display: flex; gap: var(--gap-2); flex-wrap: wrap; }

.flash {
  max-width: 1400px;
  margin: var(--gap-2) auto 0;
  padding: var(--gap-2) var(--gap-3);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  font-size: 13px;
}
.flash--ok { border-color: var(--ok); color: var(--ok); background: color-mix(in srgb, var(--ok) 10%, var(--surface)); }
.flash--error { border-color: var(--danger); color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, var(--surface)); }

/* ------------------------------------------------------------ primitives */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: var(--gap-4);
}

.card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-3);
  flex-wrap: wrap;
  padding: var(--gap-3) var(--gap-4);
  border-bottom: 1px solid var(--border);
}

.card__title { margin: 0; font-size: 15px; font-weight: 650; }
.card__body { padding: var(--gap-4); }
.card__actions { display: flex; gap: var(--gap-2); flex-wrap: wrap; }

.stack { display: flex; flex-direction: column; gap: var(--gap-3); }
.row { display: flex; gap: var(--gap-3); align-items: center; }
.row--wrap { flex-wrap: wrap; }
.row--between { justify-content: space-between; }

.muted { color: var(--muted); }
.small { font-size: 13px; }
.mono { font-family: var(--mono); font-size: 13px; word-break: break-all; }

/* A <pre> keeps its author's line breaks AND refuses to add any of its own, so the candidate
   write-ups on /admin/applicants/:id — which arrive as one long paragraph per line — were
   4125px wide inside a 254px card, and the whole ADMIN page scrolled sideways to read one of
   them. `pre-wrap` keeps every newline the candidate typed (which is the only reason these are
   <pre> and not <p>) while letting a line that is too long for the box wrap like ordinary
   text. Nothing changes at a width where the line already fits. */
pre.mono {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin: 0 0 var(--gap-3);
  /* ...but NOT `word-break: break-all`, which .mono sets and this undoes.
     Every pre.mono in the app holds something a PERSON TYPED as prose: a candidate's write-up,
     a trainee's practice take, the draft DM on a drill step. break-all chops those mid-word at
     the edge of the box, so on a 375px phone a take read "I was about to send a search part /
     y. I decided your job had finally eat / en you" — three broken words in one sentence, in
     the one place a trainee re-reads their own answer against the coach's verdict.
     `overflow-wrap: anywhere` above already handles the case break-all was there for — a
     single unbreakable 4000px token — without touching ordinary words. Inline `.mono` keeps
     break-all, because that one holds ids, tokens and filenames, which have no words to break. */
  word-break: normal;
}
.hidden { display: none !important; }

.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;
}

.divider { height: 1px; background: var(--border); border: 0; margin: var(--gap-4) 0; }

/* -------------------------------------------------------------- controls */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-4);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  font-weight: 550;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover { border-color: var(--muted); }
.btn--primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn--primary:hover { filter: brightness(1.08); }
.btn--ghost { background: transparent; }
.btn--danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn--danger:hover { filter: brightness(1.08); }
.btn--sm { padding: var(--gap-1) var(--gap-2); font-size: 13px; }

.field { display: flex; flex-direction: column; gap: var(--gap-1); margin-bottom: var(--gap-3); }
.field__label { font-size: 13px; font-weight: 600; }

.field__input,
.textarea,
.select {
  width: 100%;
  padding: var(--gap-2) var(--gap-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 14px;
}
.field__input:focus,
.textarea:focus,
.select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.textarea { resize: vertical; min-height: 90px; line-height: 1.5; }

.field__error { font-size: 13px; color: var(--danger); }
.field__hint { font-size: 13px; color: var(--muted); }

.progress {
  width: 100%; height: 8px;
  background: var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.progress__bar { height: 100%; width: 0%; background: var(--accent); transition: width .15s linear; }

.copy-btn { font-size: 13px; }

/* Up and Down are one form with two buttons, so they need to sit together and tighter than the
   gap the surrounding .row uses — they are one control, not two. */
.tpl-move { display: flex; gap: var(--gap-1); }

/* ------------------------------------------------------------------ data */

.badge {
  display: inline-block;
  padding: 1px var(--gap-2);
  border-radius: 999px;
  font-size: 12px;
  font-weight: 650;
  border: 1px solid var(--border);
  color: var(--muted);
}
.badge--fresh { color: var(--ok); border-color: var(--ok); }
.badge--warm { color: var(--warn); border-color: var(--warn); }
.badge--stale { color: var(--danger); border-color: var(--danger); }
.badge--info { color: var(--accent); border-color: var(--accent); }

.pill {
  display: inline-block;
  padding: 1px var(--gap-2);
  border-radius: var(--radius);
  font-size: 12px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--muted);
}
.pill--stage { color: var(--text); }
.pill--active { color: var(--accent); border-color: var(--accent); }
.pill--rejected { color: var(--danger); border-color: var(--danger); }
.pill--withdrawn { color: var(--muted); }
.pill--hired { color: var(--ok); border-color: var(--ok); }

.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th,
.table td {
  text-align: left;
  padding: var(--gap-2) var(--gap-3);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.table th { font-size: 13px; color: var(--muted); font-weight: 600; white-space: nowrap; }
.table--fixed { table-layout: fixed; }
.table__num { text-align: right; font-variant-numeric: tabular-nums; }

.empty { color: var(--muted); font-size: 14px; margin: 0; padding: var(--gap-4) 0; text-align: center; }

.timeline { list-style: none; margin: 0; padding: 0; }
.timeline__item {
  display: flex;
  gap: var(--gap-3);
  padding: var(--gap-2) 0;
  border-bottom: 1px solid var(--border);
}
.timeline__item:last-child { border-bottom: 0; }
.timeline__time {
  flex: 0 0 auto;
  width: 150px;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.timeline__body { flex: 1 1 auto; font-size: 14px; min-width: 0; }

/* ----------------------------------------------------------------- board */

.board {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(240px, 1fr);
  gap: var(--gap-3);
  overflow-x: auto;
  padding-bottom: var(--gap-4);
  align-items: start;
}

.board__col {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-height: 120px;
  display: flex;
  flex-direction: column;
}

.board__col-head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  font-size: 13px;
  font-weight: 650;
}

.board__count { color: var(--muted); font-weight: 550; font-variant-numeric: tabular-nums; }

/* The Experience column, and ONLY when somebody in it has actually sent their proof. It is the
   one column where the candidate cannot move themselves on, so a card sitting in it is a job of
   his rather than a queue running itself. The colour is --danger because that is already what
   this app means by "this one is on you and it has been a while" — the stale badge, the stale
   card — and a second colour for the same idea is a second thing to learn. */
.board__col--owed { border-color: var(--danger); }
.board__col--owed .board__col-head {
  border-bottom-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 8%, var(--surface));
}
.board__owed { color: var(--danger); font-weight: 600; white-space: nowrap; }

.board__body { padding: var(--gap-2); display: flex; flex-direction: column; gap: var(--gap-2); flex: 1 1 auto; }

.cardlet {
  display: block;
  padding: var(--gap-2) var(--gap-3);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  text-decoration: none;
  cursor: grab;
}
.cardlet:hover { border-color: var(--muted); }
.cardlet--fresh { border-left-color: var(--ok); }
.cardlet--warm { border-left-color: var(--warn); }
.cardlet--stale { border-left-color: var(--danger); }
.cardlet--dragging { opacity: .45; }

.cardlet__name { font-weight: 600; font-size: 14px; word-break: break-word; }
.cardlet__meta { display: flex; gap: var(--gap-2); align-items: center; flex-wrap: wrap; margin-top: var(--gap-1); font-size: 12px; color: var(--muted); }
.cardlet__actions { margin-top: var(--gap-2); }

.dropzone { min-height: 40px; border-radius: var(--radius); }
.dropzone--over { outline: 2px dashed var(--accent); outline-offset: -2px; background: color-mix(in srgb, var(--accent) 8%, transparent); }

@media (max-width: 900px) {
  .board {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    overflow-x: visible;
  }
  .board__col-head { position: static; }
}

/* ---------------------------------------------------------------- detail */

.detail { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: var(--gap-4); align-items: start; }
.detail__rail { min-width: 0; }
.detail__main { min-width: 0; }

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

.section { margin-bottom: var(--gap-5); }
.section__title { font-size: 15px; font-weight: 650; margin: 0 0 var(--gap-3); }

.sensitive { border: 1px solid var(--danger); border-radius: var(--radius); }
.sensitive__warn { color: var(--danger); font-size: 13px; padding: var(--gap-3) var(--gap-4); border-bottom: 1px solid var(--border); }
.sensitive__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-3);
  flex-wrap: wrap;
  padding: var(--gap-2) var(--gap-4);
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}
.sensitive__row:last-child { border-bottom: 0; }

/* Experience proof, on the applicant page. Screenshots are a LOOKING job — six of them against
   one claim — so they render, side by side, at a size he can actually read a chat log in. The
   grid is auto-fill so one screenshot is not stretched across the card and eight do not become
   thumbnails. Clicking one opens it full size through the same admin-only route. */
.proofs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--gap-3);
  padding: var(--gap-3) var(--gap-4);
}
.proof { margin: 0; display: flex; flex-direction: column; gap: var(--gap-1); min-width: 0; }
.proof__img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 420px;
  object-fit: contain;
  object-position: top left;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}
.proof__file { font-size: 14px; word-break: break-all; }
.proof figcaption { display: flex; align-items: center; gap: var(--gap-2); flex-wrap: wrap; }
.proof figcaption form { margin: 0; }

/* ---------------------------------------------------------------- portal */

.portal {
  max-width: 720px;
  width: 100%;
  margin: 0 auto;
  padding: var(--gap-5) var(--gap-4) var(--gap-6);
  padding-left: max(var(--gap-4), env(safe-area-inset-left));
  padding-right: max(var(--gap-4), env(safe-area-inset-right));
  padding-bottom: calc(var(--gap-6) + env(safe-area-inset-bottom));
}
.portal__head { margin-bottom: var(--gap-5); }
.portal__heading { font-size: 22px; font-weight: 650; margin: 0 0 var(--gap-2); }
.portal__instruction { color: var(--muted); margin: 0; }

/* THE YES/NO QUESTION — the first screen a candidate sees, and the only thing on it.
   Two targets, each at least a quarter of a phone screen tall, so there is nothing to miss and
   nothing to read twice. They stack on a phone and sit side by side anywhere wider; neither is
   styled as the "right" answer, because both are. */
.bigchoice {
  display: grid;
  /* auto-fit, not a media query: at 320px two 180px columns cannot fit beside each other so
     they stack, and anywhere wider they sit side by side. No breakpoint to keep in step with
     anything, and no fixed pixel width that a 320px screen has to scroll sideways past. */
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--gap-3);
  margin-top: var(--gap-5);
}
.bigchoice form { margin: 0; }
.bigchoice__btn {
  width: 100%;
  min-height: 92px;
  padding: var(--gap-4);
  font-family: inherit;
  font-size: 22px;
  font-weight: 650;
  color: var(--text);
  background: var(--surface);
  border: 2px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}
.bigchoice__btn:hover { border-color: var(--accent); }
.bigchoice__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.uploader { display: flex; flex-direction: column; gap: var(--gap-3); }
.uploader__drop {
  border: 2px dashed var(--border);
  border-radius: var(--radius);
  padding: var(--gap-5);
  text-align: center;
  color: var(--muted);
  font-size: 14px;
  cursor: pointer;
}
.uploader__drop:hover { border-color: var(--accent); color: var(--text); }
.uploader__status { font-size: 13px; color: var(--muted); }

/* The signed-in candidate's Discord account, in the portal header. The sign-out beside it is a
   same-origin POST, so nav:'portal' still emits no outbound link and the token in the Referer
   goes nowhere but back to us. */
.topnav__user {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--gap-2);
}
.topnav__who {
  font-size: 13px;
  color: var(--muted);
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topnav__user form { margin: 0; }

/* ================================================================================ candidate
 *
 * body.candidate — every nav:'portal' page, and nothing else. See the file header.
 * ========================================================================================== */

/* ------------------------------------------------------- the "Chatter Hiring" header bar
 *
 * The bar is brand + username + Sign out and it must survive a 32-character Discord name at
 * 320px. It does NOT wrap: wrapping put Sign out on a third line and pushed the page down.
 * Instead the brand holds its width, the name takes what is left and truncates, and Sign out
 * keeps its full tap target hard against the right edge. */
.topnav--portal {
  flex-wrap: nowrap;
  gap: var(--gap-3);
  padding-top: max(var(--gap-2), env(safe-area-inset-top));
  padding-bottom: var(--gap-2);
  min-height: 56px;
}
/* The brand does not shrink. It is the one thing on this bar the operator asked for by name, and
   at 320px a shrinkable brand was the first thing to go: "Chatter ...". The username shrinks
   instead — it is the candidate's own name and they can read it truncated. */
.topnav--portal .topnav__brand {
  flex: 0 0 auto;
  font-size: 16px;
}
.topnav--portal .topnav__user {
  flex: 1 1 auto;
  min-width: 0;
  justify-content: flex-end;
}
/* flex: 0 1 auto + min-width: 0 is what lets the ellipsis actually happen. Without min-width a
   flex item refuses to shrink below its content, so a long name pushes the button off-screen
   instead of truncating — the exact failure this block exists to prevent. */
.topnav--portal .topnav__who {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 14px;
}
.topnav--portal .topnav__user form { flex: 0 0 auto; }

/* --------------------------------------------------------------------------- type scale
 *
 * 16px body on the candidate side. Not a preference: a form control under 16px makes iOS
 * Safari zoom the whole page the moment it takes focus, and the candidate is then typing into
 * a page they have to pinch back out of. */
body.candidate {
  font-size: 16px;
  /* Backstop for sideways scroll. `clip` rather than `hidden` because hidden would make this a
     scroll container and break the sticky quiz bar. */
  overflow-x: clip;
}
.candidate .small,
.candidate .field__hint,
.candidate .field__error,
.candidate .field__label,
.candidate .uploader__status,
.candidate .empty { font-size: 14px; }
.candidate .card__title { font-size: 16px; }
.candidate .portal__heading { font-size: clamp(20px, 5.2vw, 24px); line-height: 1.25; }
.candidate .portal__instruction { font-size: 16px; }
.candidate .mono { font-size: 14px; }

/* Long unbroken things — a pasted URL, a Discord name, a filename — must wrap rather than widen
   the page. .mono already breaks anywhere; ordinary copy breaks at the word. */
.candidate p,
.candidate li,
.candidate .cardlet__name,
.candidate .card__title { overflow-wrap: break-word; }

/* A flex/grid child will not shrink past its content unless it is told it may. Every sideways
   scrollbar on this side of the app traced back to a missing one of these. */
.candidate .stack > *,
.candidate .row > *,
.candidate .card__actions > * { min-width: 0; }

.candidate img,
.candidate video { max-width: 100%; }

/* ------------------------------------------------------------------------- tap targets
 *
 * 44px is the floor, on the candidate side at every width and on every screen at phone widths.
 * .btn--sm keeps its smaller TEXT — it is still a secondary control — but not a smaller target. */
.candidate .btn { min-height: 44px; font-size: 16px; }
/* --sm keeps the smaller LABEL — it is still a secondary control — but not a smaller target. */
.candidate .btn--sm { min-height: 44px; padding: var(--gap-2) var(--gap-3); font-size: 14px; }

/* The text inputs and the stage <select> were the two controls the 44px floor missed: measured
   at 320px they came out 42px and 39px, because their height came from padding + a 16px line
   box rather than from a rule. A select is the worst of the two — it is the one control on the
   candidate side where a mis-tap silently changes an answer instead of doing nothing. */
.candidate .field__input,
.candidate .textarea,
.candidate .select { min-height: 44px; }

/* Controls are 16px on every phone-sized screen, admin included: this one is about iOS
   zooming, not about how the page looks, and the operator reads his own board on a phone too.
   The same goes for the 44px floor: it is about a thumb, and the operator has one. */
@media (max-width: 900px) {
  .field__input,
  .textarea,
  .select { font-size: 16px; min-height: 44px; }
  .btn { min-height: 44px; }
  .btn--sm { min-height: 44px; padding: var(--gap-2) var(--gap-3); }
}

/* The Replace / Send-a-different-version disclosures in views/portal.js. The text is small and
   muted on purpose; the target around it is not. The default list-item display is kept so the
   disclosure triangle survives — it is the only affordance the control has. */
.candidate summary.toggle {
  min-height: 44px;
  padding: var(--gap-3) 0;
}

/* --------------------------------------------------------------------------- uploading
 *
 * The one part of this the candidate watches for minutes at a time, on a phone, on mobile data,
 * on a file that came out of their own screen recorder. */
.candidate .uploader__drop {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 96px;
  padding: var(--gap-4);
  font-size: 16px;
  line-height: 1.4;
}
.candidate .progress { height: 10px; }
/* RESERVED, NOT SIZED TO THE TEXT. public/upload.js swaps this line roughly twice a second
   between "Uploading 41% of 912 MB", "Received" and a two-line error. Letting the box follow the
   text moved every control under it on every tick; two lines are reserved up front so the only
   thing that changes is the words. Tabular figures stop the percentage jittering as digits
   change width. */
.candidate .uploader__status {
  margin: 0;
  min-height: 2.8em;
  font-variant-numeric: tabular-nums;
}

/* The candidate's own video played back to them. `aspect-ratio: auto 16 / 9` reserves a sane box
   before the metadata arrives and then yields to the real ratio — most of these are portrait
   phone recordings, and without the cap a 9:16 video at full card width ran several screens
   tall. */
.candidate .video-preview,
.video-preview {
  display: block;
  width: 100%;
  max-width: 100%;
  max-height: 60vh;
  aspect-ratio: auto 16 / 9;
  object-fit: contain;
  border-radius: var(--radius);
  background: #000;
}

/* ------------------------------------------------------------------------- quiz, shared
 *
 * Used by views/quiz.js (group PAGINATE). The image band and the option rows are the two things
 * that decide whether a question is answerable on a phone without scrolling, so they live here
 * rather than in inline styles. */

:root { --quiz-media-h: min(42vh, 340px); }

/* A FIXED band, not a max-height, and that is the point: the height is the same before and after
   the picture loads, so nothing under it moves when it arrives — no reflow under a thumb that is
   already reaching for an answer.
 *
 * THE BAND ITSELF IS INVISIBLE: no background and no border, so a picture shorter than the band
 * leaves what looks like ordinary spacing rather than an empty grey box. The frame belongs to the
 * image, which is also what keeps it from reading as part of the card when it is a dark
 * screenshot on a dark theme.
 *
 * The height is deliberately under half the viewport: the question, the picture and the first
 * answer have to be on screen together on a 667px-tall phone, and the cap is what buys that. */
.quiz-media {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--quiz-media-h);
  margin: 0 0 var(--gap-3);
  overflow: hidden;
}
.quiz-media__img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* The question card itself. It is a `.card` first — this class only exists so the flagged
   modifier below has something to hang off and so the prompt/options block can be found in the
   markup. Integration note: views/quiz.js emits `class="card quiz-q"`, and the smoke suite
   asserts that every class the candidate pages emit is defined here, so the bare `.quiz-q` rule
   is NOT dead code — deleting it turns that check red. `scroll-margin-top` is what makes an
   in-page jump to a flagged question land below the sticky header rather than under it. */
.quiz-q { scroll-margin-top: var(--gap-4); }
.quiz-q__meta { margin: 0 0 var(--gap-2); font-size: 14px; color: var(--muted); }
.quiz-q__prompt { margin: 0 0 var(--gap-3); font-size: 17px; font-weight: 650; line-height: 1.4; }
.quiz-q__extra { margin: 0 0 var(--gap-3); color: var(--muted); }
.quiz-q--flagged { border-color: var(--danger); }

.quiz-opts {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* 52px, not 44: these sit directly above and below each other, so the gap between two targets
   matters as much as the size of one. */
.quiz-opt {
  display: flex;
  gap: var(--gap-3);
  align-items: flex-start;
  min-height: 52px;
  padding: var(--gap-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: 16px;
  line-height: 1.4;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.quiz-opt:hover { border-color: var(--muted); }
.quiz-opt input[type="radio"] {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  margin: 1px 0 0;
  accent-color: var(--accent);
}
.quiz-opt__letter { font-weight: 650; }
.quiz-opt__text { min-width: 0; overflow-wrap: break-word; }
/* Two ways to show the chosen row, and the page should set both: :has() covers the instant the
   finger lands, --picked covers a server re-render on a browser without :has(). */
.quiz-opt--picked,
.quiz-opt:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
}
.quiz-opt:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }

/* "Question 4 of 12" with a bar beside it. The fill's width is the one value that has to be an
   inline style, which style-src 'unsafe-inline' allows. */
.quiz-progress {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  margin: 0 0 var(--gap-3);
}
.quiz-progress__text {
  flex: 0 0 auto;
  font-size: 14px;
  color: var(--muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.quiz-progress__track {
  flex: 1 1 auto;
  min-width: 0;
  height: 6px;
  background: var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.quiz-progress__fill { display: block; height: 100%; background: var(--accent); border-radius: 999px; }

.quiz-dots { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
.quiz-dots__dot { width: 10px; height: 10px; border-radius: 999px; background: var(--border); }
.quiz-dots__dot--done { background: var(--ok); }
.quiz-dots__dot--current { background: var(--accent); }
.quiz-dots__dot--locked { background: var(--border); opacity: 0.45; }

/* Back / Next, pinned to the bottom of the viewport so the candidate never has to scroll past
   the options to find the way forward. The bottom padding clears the iPhone home indicator. */
.quiz-nav {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  margin-top: var(--gap-4);
  padding: var(--gap-3) 0;
  padding-bottom: calc(var(--gap-3) + env(safe-area-inset-bottom));
  background: var(--bg);
  border-top: 1px solid var(--border);
}
.quiz-nav .btn { flex: 1 1 0; min-width: 0; }
.quiz-nav__spacer { flex: 1 1 0; }
/* Back is written AFTER Next in the DOM on a question step, on purpose: a form's default button
   — the one Enter presses — is the first submit button in tree order, and with Back first a
   candidate who chose an option with the arrow keys and pressed Enter was walked BACKWARDS.
   Both have to be submit buttons because Back must SAVE. So the DOM order is Next-then-Back and
   this rule puts Back back on the left where a reader expects it. Do not "fix" the markup. */
.quiz-nav__back { order: -1; }

/* The unit list. Name on the left, badges and Start on the right; below ~420px the right-hand
   group drops to its own line rather than squeezing the name to three characters. */
.unit-row {
  display: flex;
  gap: var(--gap-3);
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  min-height: 60px;
  padding: var(--gap-3) 0;
  border-top: 1px solid var(--border);
}
.unit-row:first-child { border-top: 0; }
.unit-row__main { flex: 1 1 200px; min-width: 0; }
.unit-row__name { font-size: 16px; overflow-wrap: break-word; }
.unit-row__name--locked { color: var(--muted); }
.unit-row__meta { font-size: 14px; color: var(--muted); }
.unit-row__side { display: flex; align-items: center; gap: var(--gap-2); flex-wrap: wrap; flex: 0 0 auto; }
a.unit-row { color: inherit; text-decoration: none; }
a.unit-row:hover .unit-row__name { text-decoration: underline; }

/* Any table that has to stay a table on a phone goes in one of these instead of widening the
   page. There are none on the candidate side today; there is one the moment somebody adds it. */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ------------------------------------------------------------------------ phone widths */

@media (max-width: 560px) {
  :root { --quiz-media-h: min(34vh, 240px); }

  /* The flash banner had `margin: 8px auto 0` and no side margin, so on a phone it ran edge to
     edge with its border off the screen. */
  .flash {
    margin-left: max(var(--gap-3), env(safe-area-inset-left));
    margin-right: max(var(--gap-3), env(safe-area-inset-right));
  }

  .row { flex-wrap: wrap; }

  /* The applicant timeline's time column is a fixed 150px, which is nearly half of a 320px
     screen and left 92px for the sentence beside it. Below 560px the stamp goes on its own
     line instead. Admin-only, and only at a width the operator reads on his phone. */
  .timeline__item { flex-direction: column; gap: var(--gap-1); }
  .timeline__time { width: auto; }

  .candidate .portal { padding-top: var(--gap-4); }
  .candidate .portal__head { margin-bottom: var(--gap-4); }
  .candidate .card__body { padding: var(--gap-3); }
  .candidate .card__head { padding: var(--gap-3); }
  .candidate .small,
  .candidate .field__hint,
  .candidate .uploader__status { font-size: 15px; }

  /* One full-width primary action per card reads as one thing to press. The header's Sign out
     is explicitly exempt — it is not a card action and must stay small and off to the side. */
  .candidate .card__actions {
    flex-direction: column;
    align-items: stretch;
  }
  .candidate .card__actions > .btn { width: 100%; }
  .candidate .topnav__user .btn { width: auto; }

  .candidate .quiz-q__prompt { font-size: 17px; }
  .candidate .uploader__drop { min-height: 88px; padding: var(--gap-3); }
}

@media (max-width: 380px) {
  /* 320px is the floor (an iPhone SE in a split view, or a phone with the text size turned up).
     Everything gets a little tighter; nothing gets a horizontal scrollbar. */
  .topnav--portal { gap: var(--gap-2); }
  .topnav--portal .topnav__brand { font-size: 15px; }
  .candidate .quiz-opt { padding: var(--gap-2) var(--gap-3); gap: var(--gap-2); }
  .unit-row__side { width: 100%; }
}

/* ------------------------------------------------------------------- reduced motion
 *
 * Honoured globally rather than per-transition, so a transition added later is covered by
 * default instead of being forgotten. The progress bar then steps instead of easing, which is
 * exactly what somebody who asked for less movement wants. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* A button that cannot act stays in place and reads as unavailable, rather than vanishing and
   shuffling everything beside it. The first message's Up and the last message's Down are the
   only ones this applies to. */
.btn[disabled] {
  opacity: 0.35;
  cursor: default;
  pointer-events: none;
}

/* THE LAST SCREEN BEFORE THE CAMERA PERMISSION PROMPT.
   Deliberately the loudest block in the app. The candidates are juniors on phones and recording
   begins the instant permission is granted - someone who taps through without reading burns a
   take and one of their two goes. Big type, one fact per line, no paragraph. */
.rec-intro {
  border: 2px solid var(--accent);
  border-radius: var(--radius);
  padding: var(--gap-4);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}
.rec-intro__title {
  margin: 0 0 var(--gap-3);
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--text);
}
.rec-intro__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--gap-3);
}
.rec-intro__list li {
  font-size: 19px;
  line-height: 1.45;
  color: var(--text);
  padding-left: 1.6em;
  position: relative;
}
.rec-intro__list li::before {
  content: "→";
  position: absolute;
  left: 0;
  color: var(--accent);
  font-weight: 700;
}
.rec-intro__list b { font-weight: 800; }
.rec-intro__note {
  margin: var(--gap-4) 0 0;
  font-size: 16px;
  color: var(--muted);
}
@media (max-width: 560px) {
  .rec-intro { padding: var(--gap-3); }
  .rec-intro__title { font-size: 23px; }
  .rec-intro__list li { font-size: 18px; }
}

/* THE FIRST SCREEN: Telegram handle and the experience question, one form, one submit. */
.firstask { display: grid; gap: var(--gap-5); }
.firstask__field { display: grid; gap: var(--gap-2); }
.firstask__label { font-size: 17px; font-weight: 650; color: var(--text); }
/* 17px, not 14: iOS Safari zooms the whole page when a focused control is under 16px, which is
   the commonest way a form feels broken on an iPhone. */
.firstask__input { font-size: 17px; min-height: 52px; }
.firstask__q { margin: 0; font-size: 19px; font-weight: 650; color: var(--text); }

/* A Discord account only days old, next to one years old. Informational - nothing is blocked. */
.pill--warn {
  color: var(--danger);
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

/* ================================================================================== training
 *
 * The training platform's whole class vocabulary, in one block at the end of the file so it can
 * be read — and removed — as one thing. It ships behind TRAINING_ENABLED, so with the flag off
 * nothing below is ever emitted by any page; it is dead CSS rather than a trace, which is why it
 * costs nothing to leave here while the feature is switched off.
 *
 * CLASSROOM (Skool structure + Onyx dark aesthetic) adds .classroom, .curric, .course-hero,
 * .chapter, .lesson-row, .practice, .exam-row. Forced dark tokens live under
 * body.candidate.classroom so admin and non-training portal pages are untouched.
 *
 * SHARED, DELIBERATELY. The three groups building the trainee runner, the operator editor and
 * this shell all draw from these names. `npm run smoke` asserts that every class a candidate
 * page emits is defined in the SERVED stylesheet, so a name one side invents and the other never
 * styles turns that check red rather than shipping as an unstyled column on somebody's phone.
 * If a screen needs something that is not here, reuse a name or wrap the thing in .card — do not
 * add a one-off.
 *
 * A PHONE IS THE DEFAULT, NOT THE BREAKPOINT. Trainees do the whole course on the device in
 * their hand: every rule here is written for 375px first and widened at a min-width, tap targets
 * are 44px or more, and form controls are 16px because iOS Safari zooms the page when a focused
 * control is smaller than that. His lessons are long, so .prose is real typography and not a
 * default <p>.
 */

/* --------------------------------------------------------------- the pacing line
 *
 * "~17h 37m of training left - at ~90 min/day you finish in ~12 days", which is the one sentence
 * he asked for, and under it the caveat when some remaining lesson has no length typed.
 *
 * THE CAVEAT IS THE SAME SIZE AS THE SENTENCE. It says the number above it is a floor rather
 * than an estimate; putting that in smaller, greyer type would be the polite way of hiding the
 * one thing a reader needs in order to not be misled. It is a block, so it is on its own line at
 * every width. */
.pacing {
  margin: 0;
  color: var(--muted);
  font-size: 15px;
  line-height: 1.5;
}

.pacing__caveat {
  display: block;
  margin-top: var(--gap-1);
  color: var(--warn);
}

/* ------------------------------------------------------------------ "Your numbers"
 *
 * Four tiles. auto-fit with a 140px floor rather than a media query: two per row on a 375px
 * phone, four on a laptop, and nothing to keep in sync when a fifth tile is added or the chapter
 * tile is absent on a course with no chapters.
 *
 * tabular-nums on the value is what stops "9%" and "41%" sitting at different widths and making
 * the row twitch when the number changes. */
.numbers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--gap-2);
}

.numbers__tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: var(--gap-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.numbers__label {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}

.numbers__value {
  font-size: 22px;
  font-weight: 650;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

/* The fraction under the percentage - "12/29 drills right". It wraps rather than truncating,
   because "Unlocks at 11/11 lessons" is the whole content of one of these tiles. */
.numbers__note {
  font-size: 13px;
  color: var(--muted);
  overflow-wrap: break-word;
}

/* ------------------------------------------------------------------- the quick lookup
 *
 * One row: a search field that takes the space, and a button that does not shrink. The button
 * keeps its 44px target from .btn; the field is 16px from the controls block above. */
.lookup {
  display: flex;
  gap: var(--gap-2);
  align-items: center;
}

.lookup__field { flex: 1 1 auto; min-width: 0; }

/* ======================================================================== the lesson runner
 *
 * A LESSON IS AN ORDERED SEQUENCE OF TYPED STEPS and these are the names its screens are built
 * from. The progress readout ("7/16") reuses .quiz-progress, the sticky Continue reuses
 * .quiz-nav, and a quick-check's options reuse .quiz-opt — they are the same controls with a
 * different noun, and a second set would be a second thing to keep working on a phone.
 */

/* ONE STEP, AND IT IS A .card FIRST. This class exists so a step can be found in the markup and
   so an in-page jump lands below the header rather than under it; the box, the border and the
   padding all come from .card. Integration note: the view emits `class="card step"`, and the
   smoke suite asserts every class a candidate page emits is defined here — so this bare rule is
   not dead code. */
.step { scroll-margin-top: var(--gap-4); }

/* One step's own frame inside the card. The kicker is the type's shout line — "QUICK CALL - SEND
   OR TRASH?", "PUT IT INTO PRACTICE · REQUIRED" - and it is small, spaced capitals rather than a
   second heading, so the step's real heading stays the biggest thing on the screen. */
.step__kicker {
  margin: 0 0 var(--gap-1);
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
}

.step__heading {
  margin: 0 0 var(--gap-3);
  font-size: 19px;
  font-weight: 650;
  line-height: 1.3;
  overflow-wrap: break-word;
}

/* --------------------------------------------------------------------------- prose
 *
 * THE LESSON TEXT, AND IT IS THE SCREEN TRAINEES SPEND THE MOST TIME ON. Nine of Lesson 1.1's
 * sixteen steps are prose and they are read on a phone, so this is typography rather than
 * styling: 17px (comfortable at arm's length without triggering iOS's zoom), 1.65 line height,
 * and a 68-character measure on a wide screen because a 1200px-wide line is unreadable however
 * big the font is.
 *
 * ELEMENT SELECTORS, NOT CLASSES, AND THAT IS THE CONTRACT. The prose renderer emits ONE class —
 * .prose — and inside it plain <p>, <strong>, <em>, <blockquote>, <ul>, <ol>, <li>, <h3>, <code>
 * from its allowlist. So the renderer cannot invent a class the stylesheet has never heard of,
 * there is nothing to keep in sync, and the allowlist is the only thing that decides what may
 * appear. Anything outside it is escaped text by the time it gets here.
 *
 * The first child loses its top margin and the last its bottom, so a prose block sits flush in a
 * .card__body instead of adding a second layer of padding. */
.prose {
  font-size: 17px;
  line-height: 1.65;
  max-width: 68ch;
  overflow-wrap: break-word;
}

.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose p { margin: 0 0 var(--gap-4); }
.prose strong { font-weight: 700; }
.prose em { font-style: italic; }

/* A coloured sub-heading inside a long body. h3 and not h2: the step's own heading is the h2 on
   that screen, and a sub-heading that outranks it reads as a new step. */
.prose h3 {
  margin: var(--gap-5) 0 var(--gap-2);
  font-size: 16px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--accent);
}

/* A LEFT BAR AND NO QUOTE MARKS. His blockquotes are the line he wants remembered, not a
   citation, and a 42px curly quote on a 375px screen takes the space the sentence needed. */
.prose blockquote {
  margin: 0 0 var(--gap-4);
  padding: var(--gap-2) 0 var(--gap-2) var(--gap-4);
  border-left: 3px solid var(--accent);
  color: var(--text);
  font-style: italic;
}

.prose blockquote > :last-child { margin-bottom: 0; }

/* padding-left in a rem-independent unit: the marker has to sit INSIDE the card at 320px, and a
   default list indent plus the card padding put it under the edge. */
.prose ul,
.prose ol {
  margin: 0 0 var(--gap-4);
  padding-left: 22px;
}

.prose li { margin: 0 0 var(--gap-2); }
.prose li > :last-child { margin-bottom: 0; }

.prose code {
  font-family: var(--mono);
  font-size: 0.9em;
  padding: 1px 4px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--muted) 16%, transparent);
  overflow-wrap: anywhere;
}

.prose hr { height: 1px; background: var(--border); border: 0; margin: var(--gap-5) 0; }

/* ------------------------------------------------------------------------- the drill
 *
 * "QUICK CALL - SEND OR TRASH?" A scenario, the draft message they are judging, two buttons, then
 * a verdict with the reason.
 *
 * THE DRAFT IS SET APART FROM THE SCENARIO because they are two different voices and confusing
 * them is the whole way to get the drill wrong: the scenario is the situation, the draft is the
 * message somebody wants to send. It keeps the line breaks it was typed with. */
/* The drill's own block inside the card. Same note as .step: a hook and a scroll anchor, not a
   box. */
.drill { scroll-margin-top: var(--gap-4); }

.drill__draft {
  margin: 0 0 var(--gap-3);
  padding: var(--gap-3);
  border: 1px solid var(--border);
  border-left: 3px solid var(--muted);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--muted) 8%, var(--surface));
  font-size: 16px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* TWO FULL-WIDTH BUTTONS SIDE BY SIDE, 52px tall. This is a snap judgement — the point of the
   drill is the first instinct — so both calls are equally easy to reach with one thumb and
   neither is styled as the expected answer. They stack below 360px rather than becoming two
   22-pixel-wide targets. */
.drill__calls {
  display: flex;
  gap: var(--gap-3);
}

.drill__calls .btn {
  flex: 1 1 0;
  min-width: 0;
  min-height: 52px;
  font-size: 16px;
  font-weight: 650;
}

/* The verdict, after the call. The colour is the ANSWER and the sentence under it is the reason —
   "Recycled generic caption she didn't write - breaks the rule." A wrong call is still a finished
   drill, so this is information and not a scolding: --wrong borders in the danger colour and does
   not fill the block with it. */
.drill__verdict {
  margin: var(--gap-3) 0 0;
  padding: var(--gap-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 16px;
  line-height: 1.5;
}

.drill__verdict--right {
  border-color: var(--ok);
  background: color-mix(in srgb, var(--ok) 10%, var(--surface));
}

.drill__verdict--wrong {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 10%, var(--surface));
}

/* ----------------------------------------------------------- the quick check's explanation
 *
 * One green line under a correct pick, one red line under a wrong one — "Wrong picks show you
 * why - try again." A line and not a card: it belongs to the option above it, and a box would
 * push the next question off a 667px screen. */
.explain {
  margin: var(--gap-2) 0 0;
  padding-left: var(--gap-3);
  border-left: 3px solid var(--border);
  font-size: 15px;
  line-height: 1.5;
}

.explain--right { border-left-color: var(--ok); color: var(--ok); }
.explain--wrong { border-left-color: var(--danger); color: var(--danger); }

/* -------------------------------------------------------------------------- the coach
 *
 * The practice step's reply. Two parts, and the SPLIT IS THE POINT: the verdict says what the
 * answer did against the rubric, and the upgrade is the one concrete thing to change. A trainee
 * on their second take reads the upgrade and nothing else, so it is visually the louder of the
 * two even though the verdict is longer.
 *
 * A COACH BLOCK ONLY EXISTS WHEN THERE IS A GROUNDED VERDICT TO SHOW. src/coach.js withholds a
 * verdict whose quotes do not appear in the trainee's own text, and the screen then shows the
 * take with no coach block rather than a hedged one — a half-shown verdict is the hallucination
 * this whole feature was rebuilt to stop. */
.coach {
  padding: var(--gap-3);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
}

.coach__verdict {
  margin: 0;
  font-size: 16px;
  line-height: 1.6;
  overflow-wrap: break-word;
}

.coach__upgrade {
  margin: var(--gap-3) 0 0;
  padding-top: var(--gap-3);
  border-top: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  font-size: 16px;
  font-weight: 650;
  line-height: 1.6;
  overflow-wrap: break-word;
}

/* "The coach is reading your answer..." — up to ten seconds of waiting on a model, so the line
   has to be there and has to be honest. RESERVED HEIGHT: it swaps between the waiting line, an
   error and nothing at all, and a line that appears from nowhere shifts the button the trainee is
   about to press. No spinner: the CSP allows no animation library and a pulsing box on a slow
   phone is worse than a sentence. */
.coach__status {
  display: block;
  min-height: 22px;
  margin-top: var(--gap-2);
  font-size: 15px;
  color: var(--muted);
}

.coach__status--error { color: var(--danger); }

/* ------------------------------------------------------------------- the finish checklist
 *
 * "Videos watched 1/1", "Put it into practice - coach feedback received", "Written questions
 * 2/2". A list with a mark on each row, derived at render time from the lesson's own steps.
 *
 * THE MARK IS A CHARACTER AND A COLOUR, NOT A COLOUR ALONE. Red/green with no symbol is
 * unreadable to a red-green colourblind trainee and says nothing in a screenshot, so the view
 * writes the tick or the dash into .checklist__mark and this only sizes it. */
.checklist {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
}

.checklist__item {
  display: flex;
  gap: var(--gap-3);
  align-items: flex-start;
  min-height: 44px;
  padding: var(--gap-2) 0;
  border-top: 1px solid var(--border);
  font-size: 16px;
  line-height: 1.45;
}

.checklist__item:first-child { border-top: 0; }
.checklist__item--done { color: var(--text); }
.checklist__item--todo { color: var(--muted); }

.checklist__mark {
  flex: 0 0 auto;
  width: 22px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.checklist__item--done .checklist__mark { color: var(--ok); }
.checklist__item--todo .checklist__mark { color: var(--muted); }

/* ===================================================================== the operator's screen
 *
 * /admin/training/people. One screen: who is in training, who has gone quiet, and their practice
 * answers with take one beside take two. Admin-only, so it is NOT scoped to body.candidate — but
 * he reads his own panel on a phone too, which is why the roster goes in a .table-wrap and the
 * take pair stacks before it squeezes.
 */

/* THE ROSTER TABLE. Cells align to the TOP, not the middle: "Becoming Her for 8 Hours:
   Immersion" wraps to two lines in the "On now" column and a vertically centred badge beside it
   then floats in the middle of the row with nothing to line up with. Integration note: the view
   emits `class="table roster"`, and the smoke suite asserts every class a page emits is defined
   here — so this bare rule is not dead code. */
.roster th,
.roster td { vertical-align: top; }

/* The name is the filter: it narrows the practice answers below without leaving the page. */
.roster__who {
  color: var(--accent);
  font-weight: 650;
  text-decoration: none;
  overflow-wrap: break-word;
}

.roster__who:hover { text-decoration: underline; }

.roster__progress { font-weight: 650; font-variant-numeric: tabular-nums; }

/* ONE PRACTICE EXERCISE'S CARD. The accent edge is what separates the stack of trainee writing
   from the roster and the settings card above it: on a screen that is otherwise five identical
   .card blocks, the thing he came to read should be findable by its shape. scroll-margin-top is
   what makes a filtered jump land below the header rather than under it. */
.takes {
  border-left: 3px solid var(--accent);
  scroll-margin-top: var(--gap-4);
}

/* TAKE ONE BESIDE TAKE TWO, and side by side is the whole value of this screen: the comparison is
   what tells him whether somebody can be coached.
 *
 * auto-fit with a 280px floor and NO MEDIA QUERY, for two reasons. A 375px screen cannot show two
 * readable columns of free text, so it stacks — and stacked-but-adjacent still reads in the right
 * order. And the column count then follows the CARD's width rather than the VIEWPORT's, which is
 * the correct dependency: a third take on a step with required_takes 3 lands in the row without a
 * breakpoint being added for it. (A `min-width:` media query would also trip the smoke suite's
 * fixed-width grep, which scans the whole stylesheet for a width over 320px.) */
.takes__pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--gap-3);
}

/* The scenario they were answering, collapsed. It is the same paragraph on every take of that
   step, so it is context he opens once rather than something he scrolls past on every card. */
.takes__scenario summary {
  cursor: pointer;
  font-size: 13px;
  color: var(--muted);
  min-height: 44px;
  display: flex;
  align-items: center;
}

.take {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  min-width: 0;
  padding: var(--gap-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--muted) 5%, var(--surface));
}

.take__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-2);
  flex-wrap: wrap;
}

.take__number {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  font-weight: 650;
}

/* WHAT THE TRAINEE TYPED, WITH THEIR LINE BREAKS. pre-wrap and not <p>: a reply written as three
   short lines is a different answer from the same words as one paragraph, and the shape of it is
   part of what he is judging. overflow-wrap keeps a pasted URL from widening the card. */
.take__body {
  margin: 0;
  max-width: 72ch;
  font-size: 15px;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The coach's verdict, under the answer and visibly not part of it. */
/* A measure, on both, because a take that has no partner yet gets the full width of the card on a
   laptop — 1000px of 15px prose, which is a wall. The card stays wide and the words stay
   readable; when the second take arrives they sit side by side and this does nothing. */
.take__verdict {
  margin: 0;
  max-width: 72ch;
  padding-top: var(--gap-2);
  border-top: 1px solid var(--border);
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
}

/* NOT GRADED IS NOT A BAD ANSWER. A take whose verdict was withheld — the API did not answer, or
   the model quoted words the trainee never wrote — says so in the warning colour, because on the
   screen he makes decisions from, "we never graded this" and "this person wrote nothing good"
   must never look the same. */
.take__verdict--missing { color: var(--warn); }

/* -------------------------------------------------------------------- training, phone widths */

@media (max-width: 560px) {
  /* Two tiles per row at 375px rather than four 80px ones. */
  .numbers { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .numbers__value { font-size: 20px; }

  .candidate .prose { font-size: 17px; }
  .candidate .step__heading { font-size: 18px; }

  /* The lookup stays ONE ROW at every phone width. It was wrapped to two — field above, a
     full-width Find under it — and that put 100px of search box between the pacing line and the
     first chapter on a screen where the chapters are the point. The field is ~190px at 320px and
     the button keeps its own 44px target, so neither is squeezed. */
  .lookup__field { min-width: 120px; }
}

@media (max-width: 380px) {
  /* 320px is the floor (an iPhone SE in a split view, or a phone with the text size turned up).
   *
   * THE TILES STAY TWO ACROSS, and this is a correction rather than a preference: they were one
   * per row below 380px, which is BELOW THE 375px IPHONE WIDTH THIS COURSE IS READ ON — so the
   * commonest phone in the agency got four full-width tiles and the first chapter started 400px
   * down the page. Two columns at 320px is a 140px tile, which holds "DRILL ACCURACY" over two
   * lines and the number under it.
   *
   * The two drill buttons DO stack: SEND and TRASH are 52px targets taking a snap judgement, and
   * a pair of 150px slivers side by side is the one place a mis-tap costs something. */
  .drill__calls { flex-direction: column; }
  .prose ul,
  .prose ol { padding-left: 18px; }
}

/* ── The Onyx games (views/training-extend.js) ─────────────────────────────────────────────── */
.game-line { margin: 0 0 1rem; padding: .75rem 1rem; border-left: 3px solid var(--accent); font-size: 1.05rem; }
.game-result--ok { border-color: var(--ok); }
.game-result--bad { border-color: var(--danger); }

/* ADMIN PREVIEW strip (views/layout.js previewBanner) — the operator's unlocked walk-through. */
.preview-banner {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
  padding: 10px 16px; background: #fff4d6; color: #5a3d00;
  border-bottom: 2px solid #e0a800; font-size: 14px; line-height: 1.4;
}
.preview-banner a { color: inherit; text-decoration: underline; }
.preview-banner__form { margin: 0 0 0 auto; }
.quiz-dots__link { display: block; width: 100%; height: 100%; border-radius: inherit; }


/* ============================================================================== classroom
 *
 * Skool.com structure + Onyx Academy dark UI. Forced on body.candidate.classroom (training
 * pages only) — does NOT wait for prefers-color-scheme, and does NOT touch admin.
 * Blue accent for completion checks, progress bars, Start buttons and links.
 * ======================================================================================== */

body.candidate.classroom {
  --bg: #0b0c0e;
  --surface: #16181c;
  --surface-2: #1c1f25;
  --text: #f4f5f7;
  --muted: #9aa2b1;
  --border: #2a2e36;
  --accent: #3b82f6;
  --ok: #4ade80;
  --warn: #e0a33a;
  --danger: #f06a6a;
  --lime: #3b82f6;
  --lime-dim: color-mix(in srgb, #3b82f6 22%, transparent);
  background: var(--bg);
  color: var(--text);
}

body.candidate.classroom .app-shell { background: var(--bg); }

body.candidate.classroom .topnav--portal {
  background: #0f1114;
  border-bottom: 1px solid var(--border);
}

body.candidate.classroom .card {
  background: var(--surface);
  border-color: var(--border);
}

body.candidate.classroom .btn--primary {
  background: var(--lime);
  border-color: var(--lime);
  color: #0b0c0e;
}

body.candidate.classroom .btn--primary:hover {
  background: #60a5fa;
  border-color: #60a5fa;
}

body.candidate.classroom .btn--ghost {
  color: var(--text);
  border-color: var(--border);
}

body.candidate.classroom .field__input,
body.candidate.classroom .textarea,
body.candidate.classroom .select {
  background: var(--surface-2);
  border-color: var(--border);
  color: var(--text);
}

body.candidate.classroom .badge--fresh {
  background: var(--lime-dim);
  color: var(--lime);
}

body.candidate.classroom .quiz-progress__fill {
  background: var(--lime);
}

body.candidate.classroom .quiz-dots__dot--done {
  background: var(--lime);
  border-color: var(--lime);
}

body.candidate.classroom .quiz-dots__dot--current {
  border-color: var(--lime);
  box-shadow: 0 0 0 2px var(--lime-dim);
}

body.candidate.classroom .preview-banner {
  background: #2a2410;
  color: #f0d78c;
  border-bottom-color: #e0a800;
}

body.candidate.classroom .flash--ok {
  background: var(--lime-dim);
  color: var(--lime);
  border-color: color-mix(in srgb, var(--lime) 40%, transparent);
}

/* ---- course index -------------------------------------------------------- */

/* Index uses the same two-column shell as lesson pages (sidebar + main). */
.classroom--index,
.classroom--lesson {
  display: grid;
  grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
  gap: var(--gap-5);
  align-items: start;
  max-width: 1120px;
}

.classroom__main {
  display: flex;
  flex-direction: column;
  gap: var(--gap-4);
  min-width: 0;
  flex: 1 1 auto;
}

.course-hero {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  padding: var(--gap-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.course-hero__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-3);
}

.course-hero__title {
  margin: 0;
  font-size: clamp(22px, 5vw, 28px);
  font-weight: 700;
  line-height: 1.2;
}

.course-hero__pct {
  font-size: 22px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--lime);
}

.course-hero__meta {
  margin: 0;
  font-size: 14px;
  color: var(--muted);
}

.classroom__chapters {
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

/* ---- chapter accordion --------------------------------------------------- */

.chapter {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.chapter__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-3);
  min-height: 52px;
  padding: var(--gap-3) var(--gap-4);
  cursor: pointer;
  list-style: none;
  user-select: none;
}

.chapter__summary::-webkit-details-marker { display: none; }

.chapter__summary::before {
  content: "";
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(-45deg);
  flex: 0 0 auto;
  transition: transform 0.15s ease;
}

.chapter[open] > .chapter__summary::before {
  transform: rotate(45deg);
}

.chapter__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 16px;
  font-weight: 650;
  overflow-wrap: break-word;
}

.chapter__stat {
  flex: 0 0 auto;
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.chapter__body {
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
  padding: 0 var(--gap-4) var(--gap-4);
  border-top: 1px solid var(--border);
  padding-top: var(--gap-3);
}

.chapter__lessons {
  display: flex;
  flex-direction: column;
}

/* ---- lesson rows (index) ------------------------------------------------- */

.lesson-row {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  min-height: 48px;
  padding: var(--gap-2) 0;
  border-bottom: 1px solid var(--border);
  color: inherit;
  text-decoration: none;
}

.lesson-row:last-child { border-bottom: 0; }

a.lesson-row:hover { background: color-mix(in srgb, var(--lime) 6%, transparent); }

.lesson-row--locked { color: var(--muted); }

.lesson-row__mark {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}

.lesson-row__mark--done {
  background: var(--lime);
  border-color: var(--lime);
  color: #0b0c0e;
}

.lesson-row__mark--lock {
  border-color: transparent;
  background: transparent;
  font-size: 14px;
}

.lesson-row__main { flex: 1 1 auto; min-width: 0; }

.lesson-row__name {
  font-size: 15px;
  font-weight: 500;
  overflow-wrap: break-word;
}

.lesson-row__meta {
  font-size: 13px;
  color: var(--muted);
}

.lesson-row__dur {
  flex: 0 0 auto;
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ---- practice + exams ---------------------------------------------------- */

.practice {
  padding: var(--gap-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.practice__title {
  margin: 0 0 var(--gap-3);
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}

.practice__list {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
}

.practice__item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 48px;
  padding: var(--gap-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: inherit;
  text-decoration: none;
}

a.practice__item:hover { border-color: var(--lime); }

.practice__item--locked { color: var(--muted); }

.practice__name { font-size: 15px; font-weight: 600; }
.practice__meta { font-size: 13px; color: var(--muted); }

.exam-row {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  min-height: 52px;
  padding: var(--gap-3) var(--gap-4);
  margin: var(--gap-2) 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.exam-row--locked { color: var(--muted); }
.exam-row--done { border-color: color-mix(in srgb, var(--lime) 45%, var(--border)); }

.exam-row__mark {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid var(--border);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
}

.exam-row__mark--done {
  background: var(--lime);
  border-color: var(--lime);
  color: #0b0c0e;
}

.exam-row__main { flex: 1 1 auto; min-width: 0; }
.exam-row__name { font-size: 15px; font-weight: 650; }
.exam-row__meta { font-size: 13px; color: var(--muted); }

/* ---- progress bar (shared) ----------------------------------------------- */

.curric__bar {
  height: 8px;
  background: var(--surface-2, #1c1f25);
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
}

.curric__bar-fill {
  display: block;
  height: 100%;
  background: var(--lime, #3b82f6);
  border-radius: 999px;
}

/* ---- two-column shell (index + lesson) -----------------------------------
 *
 * Desktop-first for the sidebar (Skool LEFT curriculum). Phone override uses
 * max-width — never min-width:NNNpx — because smoke forbids fixed min-widths
 * over 320px even inside media queries. Grid is defined above with
 * .classroom--index / .classroom--lesson.
 */

.classroom__sidebar {
  display: block;
  position: sticky;
  top: var(--gap-4);
  max-height: calc(100vh - 80px);
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap-3);
}

.classroom__lesson-head { margin-bottom: var(--gap-1); }

.classroom__lesson-title-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-3);
}

.classroom__lesson-title {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-2);
  margin: 0;
}

.classroom__lesson-check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--lime);
  color: #0b0c0e;
  font-size: 12px;
  font-weight: 700;
  flex: 0 0 auto;
}

.classroom__lesson-dur {
  font-size: 14px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Mobile curriculum drawer — details/summary, no JS. Hidden on desktop. */
.curric-drawer {
  display: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.curric-drawer__toggle {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: var(--gap-3) var(--gap-4);
  cursor: pointer;
  list-style: none;
  font-weight: 650;
  font-size: 15px;
}

.curric-drawer__toggle::-webkit-details-marker { display: none; }

.curric-drawer__body {
  padding: 0 var(--gap-3) var(--gap-3);
  border-top: 1px solid var(--border);
}

@media (max-width: 899px) {
  .classroom--index,
  .classroom--lesson {
    display: flex;
    flex-direction: column;
    gap: var(--gap-4);
  }

  .classroom__sidebar { display: none; }

  .curric-drawer { display: block; }
}

/* ---- curriculum panel ---------------------------------------------------- */

.curric {
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

.curric__head {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
}

.curric__chapter {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.3;
}

.curric__meta {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
}

.curric__test { color: var(--muted); }
.curric__test--passed { color: var(--lime); }
.curric__test--locked { color: var(--muted); }

.curric__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.curric__item {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  min-height: 40px;
  padding: 6px 8px;
  border-radius: 6px;
  border: 1px solid transparent;
}

.curric__item--current {
  background: var(--lime-dim);
  border-color: color-mix(in srgb, var(--lime) 50%, transparent);
}

.curric__item--locked { color: var(--muted); }

.curric__link {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  flex: 1 1 auto;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

.curric__link:hover .curric__title { color: var(--lime); }

.curric__check {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--lime);
  color: #0b0c0e;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
}

.curric__lock {
  flex: 0 0 auto;
  width: 18px;
  text-align: center;
  font-size: 12px;
}

.curric__dot {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid var(--border);
}

.curric__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 13px;
  overflow-wrap: break-word;
}

.curric__dur {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.curric__preview {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--accent);
  text-decoration: underline;
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  padding: 0 4px;
}

.curric__test-link {
  display: block;
  padding: var(--gap-2) var(--gap-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 13px;
  font-weight: 600;
  color: inherit;
  text-decoration: none;
  text-align: center;
  min-height: 40px;
  line-height: 24px;
}

a.curric__test-link:hover { border-color: var(--lime); color: var(--lime); }

.curric__test-link--locked {
  color: var(--muted);
  font-weight: 500;
}

.curric__back {
  font-size: 13px;
  color: var(--muted);
  text-decoration: none;
  min-height: 36px;
  display: inline-flex;
  align-items: center;
}

.curric__back:hover { color: var(--lime); }

/* Course-wide sidebar chapters (index) */
.curric--course { gap: var(--gap-2); }

.curric__ch {
  border-top: 1px solid var(--border);
  padding-top: var(--gap-2);
}

.curric__ch-sum {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-2);
  min-height: 36px;
  cursor: pointer;
  list-style: none;
  user-select: none;
  font-size: 13px;
  font-weight: 650;
}

.curric__ch-sum::-webkit-details-marker { display: none; }

.curric__ch-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: break-word;
}

.curric__ch-stat {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.curric__ch .curric__list { margin-top: var(--gap-1); }

.curric__ch .curric__test-link,
.curric__ch .curric__test-link--locked {
  margin-top: var(--gap-1);
  font-size: 12px;
}

/* Subtle lookup on classroom index */
body.candidate.classroom .lookup {
  opacity: 0.92;
}

body.candidate.classroom .lookup__field {
  background: var(--surface);
}

@media (max-width: 560px) {
  .course-hero { padding: var(--gap-3); }
  .chapter__stat { font-size: 12px; }
  .classroom__lesson-title-row { flex-wrap: wrap; }
}


/* ============================================================================== apply page
 *
 * body.candidate.apply-page — the public front door (/apply) and its dead ends, views/apply.js.
 * Same family as the classroom: forced dark, blue accent (#3b82f6 / #60a5fa), never green. It
 * does NOT wait for prefers-color-scheme and touches nothing outside apply-page.
 *
 * Phone first: one column at 390px with the Discord button above the fold; at 960px+ the hero
 * and the card sit side by side and "How it works" becomes a four-step strip under them.
 *
 * No webfonts, no images, no data: URIs (img-src is 'self'): the glow is gradients, the icons
 * are inline <svg> in the markup, and the brand mark is a pseudo-element.
 * ======================================================================================== */

body.candidate.apply-page {
  --bg: #0a0b0e;
  --surface: #14161b;
  --surface-2: #1b1e25;
  --text: #f4f5f7;
  --muted: #9aa2b1;
  --border: #262a33;
  --accent: #60a5fa;
  --blue: #3b82f6;
  --blue-soft: #60a5fa;
  --blurple: #5865f2;
  --ok: #4ade80;
  --warn: #e0a33a;
  --danger: #f06a6a;
  color-scheme: dark;
  color: var(--text);
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse 70% 42% at 50% -6%, rgba(59, 130, 246, 0.30), transparent 70%),
    radial-gradient(ellipse 45% 35% at 92% 18%, rgba(96, 165, 250, 0.10), transparent 70%),
    radial-gradient(ellipse 50% 40% at 0% 70%, rgba(59, 130, 246, 0.07), transparent 70%);
  background-repeat: no-repeat;
  background-attachment: fixed;
  min-height: 100vh;
}
body.candidate.apply-page .app-shell { background: transparent; }

/* The header: transparent over the glow, brand with a small chat-bubble mark. The brand text
   never truncates or clips (flex: 0 0 auto from the portal rules, plus nowrap and no overflow). */
body.candidate.apply-page .topnav--portal {
  background: rgba(10, 11, 14, 0.55);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  padding-left: max(var(--gap-5), env(safe-area-inset-left));
  padding-right: max(var(--gap-5), env(safe-area-inset-right));
}
body.candidate.apply-page .topnav__brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
  overflow: visible;
  white-space: nowrap;
}
body.candidate.apply-page .topnav__brand::before {
  content: "";
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border-radius: 9px 9px 9px 3px;
  /* A chat bubble with three dots: four gradients, no image request. */
  background:
    radial-gradient(circle at 30% 50%, #fff 0 1.6px, transparent 2.2px),
    radial-gradient(circle at 50% 50%, #fff 0 1.6px, transparent 2.2px),
    radial-gradient(circle at 70% 50%, #fff 0 1.6px, transparent 2.2px),
    linear-gradient(135deg, var(--blue-soft), var(--blue) 60%, #2563eb);
  box-shadow: 0 4px 14px -4px rgba(59, 130, 246, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.25);
}

/* The dead-end pages (closed, sent, cancelled, expired) keep the .portal column but get the
   same card and type as the front door. */
body.candidate.apply-page .portal {
  padding-top: var(--gap-6);
  padding-left: max(var(--gap-5), env(safe-area-inset-left));
  padding-right: max(var(--gap-5), env(safe-area-inset-right));
}
body.candidate.apply-page .card .stack > p { margin: 0; }
body.candidate.apply-page .card__body { padding: var(--gap-5); }
body.candidate.apply-page .portal__heading {
  font-size: clamp(24px, 6vw, 32px);
  font-weight: 750;
  letter-spacing: -0.02em;
}
body.candidate.apply-page .card {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.015)), var(--surface);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 16px;
  box-shadow: 0 24px 60px -28px rgba(0, 0, 0, 0.8);
}
body.candidate.apply-page .btn--primary {
  background: var(--blue);
  border-color: var(--blue);
  color: #fff;
  font-weight: 600;
}
body.candidate.apply-page .btn--primary:hover { background: #2f74e6; border-color: #2f74e6; }
body.candidate.apply-page .field__input {
  background: var(--surface-2);
  border-color: var(--border);
  color: var(--text);
}

/* ---- layout ----------------------------------------------------------------------------- */

.apply {
  max-width: 1120px;
  margin: 0 auto;
  padding: var(--gap-6) var(--gap-5) calc(var(--gap-6) + env(safe-area-inset-bottom));
  padding-left: max(var(--gap-5), env(safe-area-inset-left));
  padding-right: max(var(--gap-5), env(safe-area-inset-right));
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gap-5);
}

/* ---- hero ------------------------------------------------------------------------------- */

.apply__hero { min-width: 0; }
.apply__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-2);
  margin: 0 0 var(--gap-4);
  padding: 5px 12px 5px 10px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #bfdbfe;
  background: rgba(59, 130, 246, 0.12);
  border: 1px solid rgba(96, 165, 250, 0.28);
  border-radius: 999px;
}
.apply__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--blue-soft);
  box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.22);
}
.apply__title {
  margin: 0 0 var(--gap-3);
  font-size: clamp(34px, 10.4vw, 60px);
  line-height: 1.04;
  font-weight: 800;
  letter-spacing: -0.035em;
  color: #fff;
}
.apply__title-accent {
  /* One unit: "chat with us" moves to its own line rather than leaving "us" alone on one. */
  display: inline-block;
  white-space: nowrap;
  padding-bottom: 0.06em;
  background: linear-gradient(100deg, #93c5fd 0%, #60a5fa 45%, #3b82f6 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.candidate .apply__lede {
  margin: 0;
  max-width: 34em;
  text-wrap: pretty;
  font-size: clamp(16px, 4.2vw, 19px);
  line-height: 1.55;
  color: #b4bccb;
}

/* ---- the card --------------------------------------------------------------------------- */

.apply-card {
  position: relative;
  min-width: 0;
  padding: var(--gap-5) var(--gap-4);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.015)),
    var(--surface);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 20px;
  box-shadow:
    0 30px 80px -30px rgba(0, 0, 0, 0.85),
    0 0 0 1px rgba(59, 130, 246, 0.05),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
/* A thin blue highlight along the top edge of the card. */
.apply-card::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 18%;
  right: 18%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(96, 165, 250, 0.8), transparent);
}

/* The button. Discord blurple, full width, 56px tall — a thumb cannot miss it. */
.candidate .btn--discord {
  display: flex;
  width: 100%;
  min-height: 56px;
  gap: 12px;
  padding: 14px 20px;
  font-size: 17px;
  font-weight: 650;
  letter-spacing: 0.005em;
  color: #fff;
  background: var(--blurple);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 14px;
  box-shadow: 0 12px 28px -10px rgba(88, 101, 242, 0.75), inset 0 1px 0 rgba(255, 255, 255, 0.18);
  text-decoration: none;
  transition: background-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.candidate .btn--discord:hover {
  background: #4752c4;
  box-shadow: 0 14px 32px -10px rgba(88, 101, 242, 0.85), inset 0 1px 0 rgba(255, 255, 255, 0.18);
  transform: translateY(-1px);
}
.candidate .btn--discord:active { transform: translateY(0); }
.candidate .btn--discord:focus-visible { outline: 3px solid #93c5fd; outline-offset: 3px; }
.btn--discord svg { flex: 0 0 auto; }

.candidate .apply-card__hint {
  margin: var(--gap-3) 0 0;
  text-wrap: balance;
  font-size: 13.5px;
  line-height: 1.45;
  text-align: center;
  color: var(--muted);
}

/* Privacy: lock heading, three short ticks. */
.apply-privacy {
  margin-top: var(--gap-5);
  padding: var(--gap-4);
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 14px;
}
.candidate .apply-privacy__title {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  margin: 0 0 var(--gap-3);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #93c5fd;
}
.apply-privacy__list {
  display: grid;
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.candidate .apply-privacy__list li {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 14.5px;
  line-height: 1.45;
  color: #cfd5df;
}
.apply-privacy__list svg {
  flex: 0 0 auto;
  margin-top: 2px;
  padding: 2px;
  width: 18px;
  height: 18px;
  color: var(--blue-soft);
  background: rgba(59, 130, 246, 0.14);
  border-radius: 50%;
}

/* Returning applicants: present, quiet. */
.candidate .apply-return {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: var(--gap-4) 0 0;
  padding-top: var(--gap-4);
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  font-size: 13px;
  line-height: 1.5;
  color: #8b93a3;
}
.apply-return svg { flex: 0 0 auto; margin-top: 1px; color: #6b7384; }

/* ---- how it works ----------------------------------------------------------------------- */

.apply-steps { min-width: 0; margin-top: var(--gap-3); }
.candidate .apply-steps__title {
  margin: 0 0 var(--gap-4);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.apply-steps__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gap-3);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: none;
}
.apply-step {
  position: relative;
  display: flex;
  gap: var(--gap-3);
  min-width: 0;
  padding: var(--gap-4);
  background: rgba(255, 255, 255, 0.025);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 16px;
}
.apply-step__num {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  font-size: 14px;
  font-weight: 750;
  font-variant-numeric: tabular-nums;
  color: #fff;
  background: linear-gradient(135deg, var(--blue-soft), var(--blue));
  border-radius: 10px;
  box-shadow: 0 6px 16px -6px rgba(59, 130, 246, 0.7);
}
.apply-step__body { min-width: 0; }
.candidate .apply-step__name {
  margin: 4px 0 4px;
  font-size: 16px;
  font-weight: 650;
  line-height: 1.3;
  color: var(--text);
}
.candidate .apply-step__text {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
}
.candidate .apply-step__aside {
  margin: var(--gap-2) 0 0;
  font-size: 13px;
  line-height: 1.45;
  color: #93c5fd;
}

/* ---- wider screens ---------------------------------------------------------------------- */

@media (min-width: 40em) { /* 640px; em so the smoke suite's fixed-width scan reads it as a breakpoint, not a width */
  .apply { padding-top: 56px; }
  .apply-card { padding: 28px; }
  .apply-steps__list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 60em) { /* 960px */
  .apply {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
    column-gap: 56px;
    row-gap: 36px;
    align-items: center;
    padding-top: 44px;
  }
  .apply__hero { padding-bottom: var(--gap-4); }
  .apply-steps { grid-column: 1 / -1; margin-top: 0; }
  .apply-steps__list { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--gap-4); }
  .apply-step { flex-direction: column; gap: var(--gap-2); padding: 18px 20px 20px; }
  .apply-step__name { margin-top: var(--gap-1); }
}


/* ---- apply page, simple version (Oct 6, 2026): one centred column, no steps ---- */
body.candidate.apply-page .apply--simple {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: calc(100vh - 140px);
  padding: 2rem 1.25rem;
}
body.candidate.apply-page .apply-simple {
  width: 100%;
  max-width: 26rem;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.1rem;
}
body.candidate.apply-page .apply-simple .apply__title { margin: 0; }
body.candidate.apply-page .apply-simple .apply__lede { margin: 0 0 0.6rem; }
body.candidate.apply-page .apply-simple .btn--discord { width: 100%; }
body.candidate.apply-page .apply-simple__note {
  margin: 0.2rem 0 0;
  font-size: 0.9rem;
  color: #9ca3af;
}
body.candidate.apply-page .apply-simple__fine {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.5;
  color: #6b7280;
}

/* ============================================================================== mobile pass
 *
 * Oct 6, 2026 — phone audit of every candidate, trainee and admin screen at 390x844 and
 * 360x740 (headless Chrome, touch + mobile UA). LAYOUT ONLY: no rule below changes what a
 * screen says, what unlocks, or what a control does.
 *
 * Breakpoints: max-width 899px is the existing sidebar/drawer switch, matched exactly so the
 * drawer styling and the drawer itself can never disagree. New breakpoints are written in em.
 * ========================================================================================== */

/* Native controls (radios, the price <select> popup, scrollbars) follow the forced-dark
   classroom instead of drawing white light-theme widgets on #0b0c0e. */
body.candidate.classroom { color-scheme: dark; }

/* The Price Ladder's Timing group is a <fieldset class="quiz-opts">: without this it carried
   the browser's default 2px grey groove border and inset padding on the dark card. */
fieldset.quiz-opts {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
fieldset.quiz-opts > legend {
  padding: 0;
  margin: 0 0 var(--gap-2);
}

/* A sticky Back/Next bar INSIDE a card (the games, the lobby screens) painted the page colour
   across the card — a dark band in the middle of the surface. Inside a card it takes the card's. */
.card .quiz-nav { background: var(--surface); }

/* The course-wide curriculum's chapter rows hide the disclosure marker and had nothing in its
   place, so nothing said they open. Same chevron as the index accordion. */
.curric__ch-sum::after {
  content: "";
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  margin: 0 4px 3px 2px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}
.curric__ch[open] > .curric__ch-sum::after { transform: rotate(-135deg); margin-bottom: -3px; }

/* Written answers (cold open, practice): a roomy box, and on browsers that support it the box
   grows with the answer instead of scrolling inside a 6-row window on a phone. */
body.candidate.classroom textarea.textarea {
  min-height: 9.5em;
  padding: 12px 14px;
  font-size: 16px;
  line-height: 1.5;
}
@supports (field-sizing: content) {
  body.candidate.classroom textarea.textarea {
    field-sizing: content;
    max-height: 60vh;
  }
}

/* The lesson player letterboxes on black rather than on the card colour. */
.classroom .video-preview { background: #000; }

@media (max-width: 899px) {
  /* THE BUG: the two-column grid sets align-items:start, and the phone override switched the
     grid to a flex column without resetting it — so every child shrank to its content. The
     closed "Curriculum" toggle was a 135px box, not a bar. */
  .classroom--index,
  .classroom--lesson { align-items: stretch; }

  /* THE CURRICULUM AS A PHONE TOP BAR. Sticky under the scrolled-away header, so the menu is one
     tap away anywhere in a long lesson; opening it drops the list OVER the page (absolute, with
     its own scroll) instead of pushing the step 600px down. Still the same <details>, no JS. */
  body.candidate.classroom .curric-drawer {
    position: sticky;
    top: 0;
    z-index: 5;
    box-shadow: 0 8px 18px -10px rgba(0, 0, 0, 0.85);
  }
  body.candidate.classroom .curric-drawer[open] {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }
  .curric-drawer__toggle {
    gap: var(--gap-3);
    min-height: 48px;
    font-size: 16px;
    -webkit-tap-highlight-color: transparent;
  }
  /* Three-line menu icon, drawn: the CSP allows no icon font and no image request. */
  .curric-drawer__toggle::before {
    content: "";
    flex: 0 0 auto;
    width: 18px;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
    box-shadow: 0 -6px 0 currentColor, 0 6px 0 currentColor;
  }
  .curric-drawer__toggle::after {
    content: "";
    flex: 0 0 auto;
    margin-left: auto;
    width: 8px;
    height: 8px;
    border-right: 2px solid var(--muted);
    border-bottom: 2px solid var(--muted);
    transform: translateY(-2px) rotate(45deg);
    transition: transform 0.15s ease;
  }
  .curric-drawer[open] > .curric-drawer__toggle::after { transform: translateY(2px) rotate(-135deg); }
  .curric-drawer[open] > .curric-drawer__body {
    position: absolute;
    top: 100%;
    left: -1px;
    right: -1px;
    max-height: calc(100vh - 190px);
    max-height: calc(100dvh - 190px);
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding-top: var(--gap-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0 0 var(--radius) var(--radius);
    box-shadow: 0 24px 48px rgba(0, 0, 0, 0.7);
  }

  /* Thumb-sized rows and readable type inside the drawer (12-13px / 36-40px on desktop). */
  .curric-drawer .curric__item { min-height: 48px; }
  .curric-drawer .curric__link { align-self: stretch; min-height: 44px; }
  .curric-drawer .curric__title { font-size: 15px; line-height: 1.35; }
  .curric-drawer .curric__dur,
  .curric-drawer .curric__ch-stat,
  .curric-drawer .curric__meta { font-size: 13px; }
  .curric-drawer .curric__check,
  .curric-drawer .curric__dot { width: 20px; height: 20px; font-size: 12px; }
  .curric-drawer .curric__lock { width: 20px; font-size: 14px; }
  .curric-drawer .curric__ch-sum { min-height: 48px; font-size: 15px; }
  .curric-drawer .curric__test-link,
  .curric-drawer .curric__ch .curric__test-link {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    font-size: 14px;
    line-height: 1.3;
  }
  .curric-drawer .curric__back { min-height: 44px; font-size: 15px; }
  .curric-drawer .curric__preview { min-height: 44px; font-size: 14px; }

  /* TYPING ON A PHONE. With the keyboard up (Android resizes the viewport to what is left), the
     sticky Back/Next bar sat on top of the answer box and its own "Lock in my answer" button.
     While a text field has focus the bar goes back into the flow; it returns on blur. */
  body.candidate:has(textarea:focus) .quiz-nav { position: static; }
}

@media (max-width: 560px) {
  /* Edge-to-edge lesson video inside its card on a phone: the 12px card padding on each side was
     24px of a 360px-wide picture. */
  .classroom .card__body > .video-preview {
    width: calc(100% + 2 * var(--gap-3));
    max-width: none;
    margin-left: calc(-1 * var(--gap-3));
    margin-right: calc(-1 * var(--gap-3));
    border-radius: 0;
  }
  .classroom .card__body > .video-preview + .quiz-progress { margin-top: var(--gap-3); }

  /* Index meta lines: 12px was under the readable floor on a phone. */
  .chapter__stat { font-size: 13px; }

  /* The lesson head took ~300px before the step began (title, duration line, chapter line, bar,
     dots), which put a video step's player below the fold on a 740px phone. Same content, tighter. */
  .classroom--lesson { gap: var(--gap-3); }
  .classroom--lesson .classroom__main { gap: var(--gap-3); }
  .classroom--lesson .classroom__lesson-title { font-size: 20px; }
  .classroom--lesson .classroom__lesson-dur { font-size: 13px; }
  .classroom--lesson .classroom__lesson-head .portal__instruction { font-size: 14px; margin-top: 2px; }
  .classroom--lesson .classroom__main > .quiz-progress { margin-bottom: 0; }
}

/* ---- admin at phone widths (secondary: the operator reads his board on a phone too) ---------
   The admin header wrapped into three rows — brand, the five links over two lines, then Log out
   alone — 240px of a 844px screen before the board started, with 31px-tall link targets. Below
   40em it is brand + Log out on one row and the links in one swipeable 44px row under them. */
@media (max-width: 40em) {
  .topnav:not(.topnav--portal) {
    row-gap: 0;
    padding-bottom: 0;
  }
  .topnav:not(.topnav--portal) .topnav__brand {
    order: 0;
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .topnav:not(.topnav--portal) .topnav__right { order: 1; margin-left: auto; }
  .topnav:not(.topnav--portal) .topnav__links {
    order: 2;
    flex: 1 0 100%;
    flex-wrap: nowrap;
    gap: var(--gap-1);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    margin: 0 calc(-1 * var(--gap-4));
    padding: 0 var(--gap-4) var(--gap-1);
  }
  .topnav:not(.topnav--portal) .topnav__links::-webkit-scrollbar { display: none; }
  /* The board's search box dropped to a full-width row with its Search button alone under it. */
  form.row:has(> input[type="search"]) { flex: 1 1 100%; flex-wrap: nowrap; }
  form.row > input[type="search"] { flex: 1 1 auto; min-width: 0; }
  .topnav:not(.topnav--portal) .topnav__link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 var(--gap-3);
  }
}

/* TRASH / SEND in a lesson drill. The view's own note says "side by side above 360px, stacked
   below it", but .bigchoice is auto-fit minmax(180px) — written for the full-width portal Yes/No
   — and inside a lesson card on a 390px phone two 180px columns never fit, so they always
   stacked: 196px of buttons with SEND under the sticky Back/Next bar. Two columns in the
   classroom; one only on a screen narrower than ~336px. */
.classroom .bigchoice { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 21em) {
  .classroom .bigchoice { grid-template-columns: minmax(0, 1fr); }
}

/* Admin preview only: the step dots are links there, and a 10px dot is not something a thumb can
   hit. An invisible hit area around each one; the dots themselves do not move. */
.quiz-dots__link { position: relative; }
.quiz-dots__link::after {
  content: "";
  position: absolute;
  inset: -12px -3px;
}
/* ...and when a long lesson's dots wrap, the rows sit far enough apart that those hit areas never
   overlap the row above. */
.quiz-dots:has(.quiz-dots__link) { row-gap: 24px; }

/* ---- mobile pass, part 2 (Oct 6, 2026) — layout only ------------------------------------- */

@media (max-width: 40em) {
  /* Course index chapter rows: the "11/11 done · test passed" line sat beside the title and
     squeezed it into three lines. On a phone the title takes the row and the stat goes under it. */
  .chapter__summary { flex-wrap: wrap; row-gap: 2px; }
  .chapter__title { flex: 1 1 calc(100% - 8px - var(--gap-3)); }
  .chapter__stat {
    flex: 1 1 100%;
    padding-left: calc(8px + var(--gap-3));
    white-space: normal;
  }

  /* On-camera answers: a phone's front camera is portrait, so at 60vh the preview pushed
     "Send this now" below the fold of a small phone mid-recording. */
  #rec-video {
    max-height: 45vh;
    max-height: 45dvh;
  }

  /* Admin at phone widths: thumb-sized targets for the small text links and disclosures. */
  body:not(.candidate) .cardlet__name {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  body:not(.candidate) a.small.muted { display: inline-flex; align-items: center; min-height: 44px; }
  body:not(.candidate) details > summary { padding-block: 12px; }
  body:not(.candidate) label.small:has(> input[type="radio"], > input[type="checkbox"]) {
    display: flex;
    align-items: center;
    gap: var(--gap-2);
    min-height: 44px;
  }
  body:not(.candidate) label.small > input[type="radio"],
  body:not(.candidate) label.small > input[type="checkbox"] {
    width: 20px;
    height: 20px;
    margin: 0;
    flex: 0 0 auto;
  }
}
