/* ============================================================
   THE ANNOTATED NOTEBOOK — Topic Questions / AI grader study
   surface (Phase 3 Wave 1, Task 4 — the most complex surface).

   Page-scoped: it legitimately styles the FROZEN grader.js /
   question-pager.js / tab-indicator.js DOM classes (.qg, .tq-*,
   .qg-*, .questions-mode-tab*, .qmt-indicator, .esa-dots,
   .note-pagination …) — restyle, never rename. nb pages load
   ONLY the nb layers (no grader.css / tab-indicator.css /
   noteviewer.css), so every rule those legacy sheets owned is
   reproduced here in ink: the FOUC pre-paginate guard, the
   segmented-control base skin + indicator, the pager, the
   loading dots.

   Discipline (test-enforced by pages/tests_notebook.py):
   · zero raw colors — every color enters as var(--nb-*)/color-mix.
   (NOT on the nb-prefix ratchet: a page layer may style frozen
    foreign classes.)

   Design voice (DESIGN.md three-ink rule):
   · Student BLUE = all interaction (answer field, Mark button,
     pager, links, focus).
   · Examiner RED, serif italic = a LOST mark and nothing else ON
     THE ANSWER CARD (2026-08-28): the unmatched keypoint's X and its
     "Not mentioned" verdict. Allocations, the score, headings, and the
     feedback band are printed matter — ink. Earned marks are green.
     §10's pager grade-echo chips predate this pass and still wear red
     for earned outcomes — flagged to the owner, pager frozen.
   · Functional errors (.qg-hint) use --nb-error + sans, never the
     examiner treatment.

   Contents
     1 · FOUC guard + column
     2 · Content-type switcher (mode tabs + indicator)
     3 · Question paper card (.qg / .tq-paper)
     4 · Question head + mark allocation (examiner voice)
     5 · Answer form (label / counter / textarea / hint / foot)
     6 · Mark button + saved badge
     7 · Grade card (header / skeleton / keypoints)
     8 · Score + examiner feedback band
     9 · Model-answer reveal
    10 · Pager (question-pager.js)
    10b · The Grid sheet under the thumb bar (Thumb Bar v2)
    11 · Loading dots (.esa-dots)
    12 · Empty state + prev/next
    13 · Mobile
    14 · Reduced motion
   ============================================================ */

/* ---------- 1 · FOUC guard + column --------------------------- */
/* Load-bearing anti-FOUC rule, reproduced from grader.css. html.tq-js is set
   synchronously in <head>; before question-pager.js adds .is-paged this hides
   every question but the first, so they never flash stacked on load. */
html.tq-js .question-list:not(.is-paged) > .qg:not(:first-child) {
  display: none;
}

/* The grader column keeps the exam sheet centered in a comfortable measure.

   width:100% is LOAD-BEARING, not decoration. The column inherits
   `margin-inline:auto` from .nb-notes-column (notebook-page.css:263), and a
   grid item with an auto margin in an axis does NOT get the default
   justify-self:stretch — it is sized to fit-content instead. Every other study
   surface hides that: notes/MCQ/flashcards all carry content wider than their
   cap, so fit-content lands on the cap anyway. The grader shows ONE question at
   a time, so the sheet was sized by that question's text: measured at 1440x900
   on a 12-question topic the same card was 647.2px wide on a 27-character
   question and 760px on a 188-character one, and the whole sheet resized under
   the reader on every Prev/Next (site-owner report). With width:100% the column
   takes the track, max-width caps it, and the auto margins centre what is left
   — one width for every question. */
.nb-qg-column {
  width: 100%;
  max-width: 760px;
}
/* Wide desktops: the notes column's left bias (right for prose) strands the
   exam sheet beside a huge right void at 1920+ — center the stage in the
   track beside the docked rail instead (item-5 sizing pass, 2026-07-03). */
@media (min-width: 1440px) {
  .nb-qg-column {
    margin-inline: auto;
  }
}

/* THE FLOATING THEME TOGGLE IS IN THE QUESTION BAR (owner 2026-10-01: "it
   changes place all the time?"). It used to be lifted to bottom:152px here, the
   one page where it left the corner, and the sheet gave up a lane for it at
   1024-1279px. It now keeps the corner (right 24px, centred on the bar's
   height, interaction-feedback.css), so the sheet needs no lane: the bar's
   paper covers that corner, and Next ends the toggle's lane
   (--ft-theme-lane, 24 + 46 + 10px) from the window edge, at every width the
   toggle shows (1024px up). */
@media (min-width: 1024px) {
  html .qg-pager {
    padding-right: var(--ft-theme-lane, 80px);
  }
}

/* THE FOOTER STARTS BELOW THE FOLD (desktop sweep 2026-09-24, d04 refix).
   The anchored pager (section 10) tucks away once the footer reaches it, and
   question-pager.js keeps it out on a page whose footer is ALREADY in the
   bar's band at the top, since no scroll could ever bring a tucked bar back.
   That kept the navigator, but on a tall window with a short topic the bar
   then sat across the footer from the first paint: at 768x1024 and at
   1024x1366 the footer tagline was sliced through its letters at the bar's
   top edge, and no scroll could clear it. So the question page's main area is
   never shorter than the window under the topbar: the footer's top lands at
   the window's bottom edge (100vh, the large viewport on a phone, so a
   collapsing URL bar cannot lift it into the band), the reader scrolls to it,
   and the bar tucks and returns as it does everywhere else. A taller page is
   untouched. Not a reservation: nothing is added below the footer, which still
   reaches the window bottom at the end of the page. */
.nb-page-main:has(.nb-qg-body) {
  min-height: calc(100vh - var(--nb-chrome-h));
}

/* ROOM UNDER THE SHEET FOR THE BAR (item 9 refix). A move never scrolls far
   enough to tuck the bar, so the deepest a question can go is where the
   footer meets the bar's top edge. With only the main area's 48px under the
   sheet, that line came before the header had scrolled away: at 360x740,
   hiding the mode tabs under the topbar on Next needed y=421 against a limit
   of 411, showing them in full pushed the sheet 14px into the bar, and the
   page stopped between the two with "Questions" cut through. So the column
   keeps the bar's own height free under the sheet (question-pager.js writes
   --qg-pager-h from the bar it measures; 0 where no bar is built). Above the
   footer, not below it: the footer still reaches the window bottom at the
   end of the page, where the bar has tucked away. */
.nb-qg-column:has(> .qg-pager) {
  padding-bottom: var(--qg-pager-h, 0px);
}

/* THE RAIL ENDS WHERE THE BAR BEGINS (TQ follow-up 2026-09-24). The chapter
   rail is a sticky scroll box as tall as the window under the topbar
   (notebook-page.css section 8), and the anchored bar covers the bottom of
   the window, so the rail's last links sat under the bar with the rail
   scrolled to its end: at 1440x900 "Summaries" was cut through by the bar's
   top edge and "Flashcards" hidden (1280x720, 1024x768 and 900x700 alike,
   "Notes" too at 900). The rail's box now stops at the bar's top edge, so
   its own end, and its 24px of end padding, sit above the bar. The same
   measured height as the room under the sheet; 0 where no bar is built, so
   a one-question topic keeps the full rail. */
.nb-page-layout:has(.nb-qg-body) .nb-rail {
  max-height: calc(100vh - var(--nb-chrome-h) - var(--nb-s7) - var(--qg-pager-h, 0px));
}

/* THE RAIL'S STICKY RANGE REACHES THE FOOTER (mobile audit 2026-09-25,
   S-018). The rail is sticky inside .nb-page-layout, whose bottom is the
   bottom of this short column, but the main area's 64px of bottom padding
   (notebook-page.css section 3) sat OUTSIDE the layout. The opening pan can
   go as deep as the line where the footer meets the bar, so on a short
   window the layout ran out before the rail did and the rail slid up under
   the topbar: measured at 1024x768 (IGCSE CIE Physics 1.2) the "IGCSE . CIE"
   eyebrow at 52px under a 72px topbar, and flush at 72 on 1366x768 and
   900x800 (24px designed). The same 64px now sits inside the column, so the
   layout ends where the footer starts. Page height, footer position and
   every pan target are unchanged; only the rail's range grows.
   ONLY WHERE THE BAR IS BUILT (2+ questions, the only pages that pan). There
   the rail can never be what sets the page's height: its box is the window
   under the topbar less 32px and the bar, so with the main area's padding it
   stays under the min-height above. On a page with no bar (a sealed chapter,
   one question) a tall rail can set the height, and moving the padding
   into the column there shortened the page by 64px and set the footer
   against the rail. From 861px, where the rail is sticky; below it the rail
   is a drawer. */
@media (min-width: 861px) {
  .nb-page-main:has(.nb-qg-column > .qg-pager) {
    padding-bottom: 0;
  }
  .nb-qg-column.nb-qg-practice:has(> .qg-pager) {
    padding-bottom: calc(var(--qg-pager-h, 0px) + var(--nb-s10));
  }
}

.nb-qg-agg {
  display: inline-block;
  margin-left: var(--nb-s2);
  padding: var(--nb-s1) var(--nb-s3);
  vertical-align: middle;
  background: var(--nb-paper-sunken);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-pill);
  font: var(--nb-type-label);
  letter-spacing: 0.02em;
  color: var(--nb-ink-2);
}

.nb-notes-header .nb-qg-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--nb-s3);
  margin-top: var(--nb-s5);
}
.nb-qg-back {
  padding-left: var(--nb-s2);
  padding-right: var(--nb-s2);
}
.nb-qg-whole {
  margin-left: 0; /* centred masthead: the pair sits together */
}

.nb-qg-body {
  margin-top: var(--nb-s6);
}

/* ---------- 2 · Content-type switcher ------------------------- */
/* nb pages don't load noteviewer.css (base skin) or tab-indicator.css
   (indicator), so BOTH are reproduced here. A segmented control seated in a
   sunken paper well; the active tab reads through the sliding pen wash the
   indicator paints (tab-indicator.js injects .qmt-indicator as first child). */
.questions-mode-tabs {
  position: relative;
  /* ⚑ CENTRED, matching the legacy sheet (noteviewer.css:5688), which has
     always used display:flex + width:fit-content + auto inline margins.
     The notebook twins had drifted to inline-flex with a zero left margin,
     so the control sat hard against the column left edge on both study
     pages while the legacy fallback centred it. fit-content keeps the strip
     exactly as wide as its two tabs, so it is still ONE flex row and
     tab-indicator.js's left/width writes still describe it. */
  display: flex;
  width: fit-content;
  gap: var(--nb-s1);
  margin: 0 auto var(--nb-s6);
  padding: var(--nb-s1);
  background: var(--nb-paper-sunken);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-md);
}
.questions-mode-tab {
  position: relative;
  z-index: 1;
  padding: var(--nb-s2) var(--nb-s4);
  border-radius: var(--nb-r-sm);
  font: var(--nb-type-control);
  text-decoration: none;
  white-space: nowrap;
  color: var(--nb-ink-2);
  transition:
    color var(--nb-dur-1) var(--nb-ease),
    background-color var(--nb-dur-1) var(--nb-ease);
}
/* Hover is interaction blue: the resting tab is already the full ink (one
   ink, 2026-09-26), so an ink hover would show nothing. */
.questions-mode-tab:hover {
  color: var(--nb-blue);
}
.questions-mode-tab.is-active {
  background: var(--nb-selection-wash);
  box-shadow: inset 0 -2px 0 var(--nb-selection-soft-border);
  color: var(--nb-selection-soft-ink);
}
.questions-mode-tab:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
}
/* The sliding indicator (tab-indicator.js writes left/width) carries the
   persistent selection; hover and focus remain interaction blue. */
.qmt-indicator {
  position: absolute;
  top: var(--nb-s1);
  bottom: var(--nb-s1);
  left: 0;
  width: 0;
  border-radius: var(--nb-r-sm);
  background: var(--nb-selection-wash);
  border: 1px solid var(--nb-selection-soft-border);
  box-shadow: inset 0 -3px 0 var(--nb-selection-marker);
  pointer-events: none;
  z-index: 0;
  will-change: left, width; /* hint for the one-shot entrance glide */
}
/* When the indicator is wired, the active tab shows through it (no double fill). */
.questions-mode-tabs.has-indicator .questions-mode-tab.is-active {
  background: transparent;
  box-shadow: none;
}

/* Tactile press — CSS-only, matching the legacy behaviour (transform-only). */
@media (prefers-reduced-motion: no-preference) {
  .questions-mode-tab {
    transition:
      color var(--nb-dur-1) var(--nb-ease),
      background-color var(--nb-dur-1) var(--nb-ease),
      transform var(--nb-dur-1) var(--nb-ease);
  }
  .questions-mode-tab:active {
    transform: scale(0.97);
  }
}

/* ---------- 3 · Question paper card --------------------------- */
/* The list stacks its questions; the pager (is-paged) shows one at a time. */
.question-list {
  display: flex;
  flex-direction: column;
  gap: var(--nb-s7);
}
/* One-question-per-page — load-bearing (reproduced from grader.css). pager.js
   adds .is-paged, then toggles .is-current on the active card; the pager itself
   never sets display, so CSS hides every non-current .qg once paged. Without
   these two rules all questions stack visible under the pager. No-JS shows all
   (progressive enhancement — .is-paged is never added). */
.question-list.is-paged {
  gap: 0;
}
.question-list.is-paged > .qg {
  display: none;
}
.question-list.is-paged > .qg.is-current {
  display: block;
}
.question-list.is-paged .qg:focus {
  outline: none; /* the card takes programmatic focus on nav; ring is noise */
}
.qg[hidden] {
  display: none;
}

/* The exam sheet: a raised page. .tq-paper is the article; the .qg wrapper is
   transparent so the paper reads as the object. */
.qg {
  min-width: 0;
  /* Any scrollIntoView that lands on a card clears the shared .topbar. On
     this page question-pager.js hands every move to window.ESATqReveal (the
     nonce'd script in nb_question_index.html), which measures the topbar
     itself; this stays as the floor for anything that scrolls a card by
     other means. */
  scroll-margin-top: calc(var(--nb-chrome-h) + var(--nb-s5));
}
/* --tq-pad mirrors the sheet's inline padding so the mode boundary and the
   model-answer panel can bleed to the sheet edge with a negative inline
   margin. A LAYOUT variable whose value is always an --nb-s* token; it carries
   no colour. overflow clips those full-bleed bands to the sheet's radius. */
.tq-paper {
  --tq-pad: var(--nb-s7);
  position: relative;
  overflow: hidden;
  padding: var(--nb-s7) var(--tq-pad) var(--nb-s6);
  background: var(--nb-paper-raised);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-lg);
  box-shadow: var(--nb-shadow-1);
  font-family: var(--nb-sans);
  color: var(--nb-ink);
}
/* Stable measure so the pager (Prev/Next) doesn't jump vertically as you step
   between questions of different content heights (site-owner fix). Scoped to
   the GRADABLE card only — its answer form + Mark button already fill this
   height, so the floor just evens out the 1-line vs 2-line question wobble
   (idle ~386-411px) with no empty space. The ungraded card (question + a short
   note) keeps its own, much shorter floor so it never shows a big empty gap.
   Longer questions still grow past the floor (min-height, not height). Mobile
   floor is a shade taller in the 720px block below.

   Stage 2 TRANSPORTS these floors rather than re-deriving them. The ladder
   adds a FIXED amount of chrome to an idle card, so the floor and the cards
   move together and the same short cards stay caught.

   The delta is +4.0px, and it is +4.0 at every column width measured (760,
   680, 440, 350) because it is pure chrome: the rule above the answer
   tightens by 8 (20+1+20 -> 16+1+16) and the model-answer reveal gains 12
   (--nb-s5 -> --nb-s7). Nothing content-dependent moves.
     desktop  413.6 -> 417.6   delta +4.0   420 + 4 = 424
     <=720px  439.1 -> 443.1   delta +4.0   470 + 4 = 474
   Both preserve the shipped headroom exactly: 420 sat 6.4px above the idle
   card and 424 sits 6.4px above it, so the floor still evens out the wobble
   without opening empty space.

   Derived from IDLE, never from a graded card: the graded delta is +23.0 and
   would set a floor no idle card could reach. And NOT from the Phase 3 lab
   figures (+16.0 / +10.0, giving 436 / 480) -- those were recorded against a
   pre-Stage-1 sheet and include the Stage 3 markup moves, neither of which is
   in this pass. Re-measured against the sheet as it actually shipped.

   2026-09-12 RE-DERIVED. The three-line question band in section 4 now takes
   the wobble out at its source rather than absorbing it here, so above 720px
   there is nothing left for the floor to even out: with the floor lifted, all
   12 cards of a real topic measure the SAME natural height at 721, 900 and
   1440px -- 443.1 gradable, 258 ungraded. The floors are re-derived to that
   levelled height (rounded up to the whole pixel) instead of to the old
   386-411 idle SPREAD, so the two sides of the comparison no longer come from
   the same place: the band sets the height, the floor states it. The old 6.4px
   of headroom is not carried over -- it existed to clear a spread that no
   longer exists, and it would now read as empty space under every question.

   NOT re-derived for <=720px: there the marks chip takes its own grid row
   (section 13), so the band only catches the one-line case and the 474 floor
   is still the leveller. Re-measured against it -- with the band in, the
   tallest <=3-line card is 469.1px at 390-719, so 474 still clears every one
   of them by 4.9px. 320px viewports fall outside it (a 280px sheet wraps the
   same questions to 4+ lines); that was true before this pass too. */
.qg--gradable .tq-paper {
  min-height: 444px;
}

/* ---------- 4 · Question head + mark allocation --------------- */
/* Number in the margin, question centre, marks in the right margin — the
   exam-paper reading order. */
/* THE QUESTION BAND IS A FIXED SIZE, not a fit to this question's text.
   Reserving three lines of .tq-question here is what makes every sheet in a
   topic the same object: the question row centres itself inside the band
   (align-content), so a one-line question sits in the middle of the same
   space a three-line question fills, and nothing below it moves. The band is
   the FLOOR, so the 9% of questions that run longer still push it open and
   stack onto a fourth line and beyond — they are never clipped or squeezed.

   WHY THREE. Measured over the 10,547 authored questions in content_stage_all
   at the shipped ~66-character line: 21.9% wrap to one line, 49.5% to two,
   19.8% to three and 8.8% to four or more. Three lines catches 91.2% of the
   corpus — the point where one more reserved line would buy 6.1% at the cost
   of 25.5px of empty band on every short question.

   Expressed in the question's own type (var(--nb-text-md) x its 1.5 line-height
   x 3) rather than a pixel constant, so it tracks the token and the mobile
   type scale instead of drifting from them.

   align-items:baseline still aligns Q-number, question and marks to the
   question's first baseline WITHIN the row; align-content centres that row in
   the band. At <=720px the marks chip takes its own row (see section 13) and
   the two rows centre together — once they outgrow the band it is simply
   inert, which is the correct degradation. */
.tq-q-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  align-content: center;
  min-height: calc(var(--nb-text-md) * 1.5 * 3);
  gap: var(--nb-s4);
}
/* Q-number: quiet index in the margin. */
.tq-q-no {
  font: var(--nb-type-label);
  letter-spacing: 0.04em;
  color: var(--nb-ink-2);
  font-variant-numeric: tabular-nums;
}
.tq-q-main {
  min-width: 0;
}
/* The question text (div[role=heading], not <h2>, to escape base h2 styles). */
.tq-question {
  margin: 0;
  font-family: var(--nb-sans);
  font-size: var(--nb-text-md);
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: -0.005em;
  color: var(--nb-ink);
  overflow-wrap: break-word;
}
.tq-question p {
  margin: 0 0 var(--nb-s3);
}
.tq-question p:last-child {
  margin-bottom: 0;
}
.tq-question .katex {
  color: var(--nb-ink);
}

/* Mark allocation — the examiner's voice: serif italic, examiner red, like a
   "[4]" pencilled into the exam-paper margin. Both the idle "— / 3" and the
   read-only "N marks" (ungraded) variant. */
.tq-q-marks {
  font-family: var(--nb-serif);
  font-style: italic;
  font-weight: 600;
  font-size: var(--nb-text-sm);
  /* The board PRINTS the allocation; printing is not judging. Red now
     means a lost mark and nothing else (2026-08-28). */
  color: var(--nb-ink);
  white-space: nowrap;
}
.tq-q-marks b {
  font-weight: 700;
}
/* The premium score division "— / 3" → "2 / 3" on marking. */
.tq-q-marks--score {
  /* Exam-paper presence for the score division; 1.3 pins the line box to
     the head's shipped height (idle card must not grow — 2026-08-28). */
  font-size: var(--nb-text-lg);
  line-height: 1.3;
}
.tq-q-marks--score .qg-score {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* Was `color: var(--nb-red); opacity: 0.7` — the one place on this card where
   opacity weakened a TEXT colour, which also fades anything nested inside it.
   THAT VALUE FAILED WCAG AA. The slash renders at 17px, so it is normal text
   and owes 4.5:1; 0.7 composites exactly like a 70% mix and measures 3.36:1
   light / 3.30:1 dark. A pre-existing failure, not one introduced here.

   The replacement was full-strength red (now --nb-ink since the
   2026-08-28 red-means-lost-marks pass), and the "quieter slash" idea was
   measured out rather than tuned. Swept 60-100% over the sheet: the lowest mix
   clearing 4.5:1 in BOTH themes is 90% (5.03 light / 4.64 dark), which is
   visually indistinguishable from 100% (6.10 / 5.45). A tint whose only
   justification is "the smallest number that still passes" is a magic value
   tied to two tokens and a font size, so it is not worth carrying. If the score
   division ever moves somewhere it can be set at 24px or larger, the 3.0
   large-text floor applies and a real tint becomes affordable again. */
.tq-q-slash {
  color: var(--nb-ink);
}

/* The margin rule under the question — the notebook's hairline.
   LADDER 1 of 4: zone 1 -> zone 2 = 16 + 1 + 16 = 33px. Reading a question and
   starting to write is one act, so this gap says "carry on", not "new
   section", and it is the smallest of the four. INSET, so the full-bleed rule
   that opens the marking is geometrically unlike it. Was 20 + 1 + 20 = 41. */
.tq-rule {
  height: 1px;
  margin: var(--nb-s3) 0;
  background: var(--nb-border);
}

/* ---------- 5 · Answer form ----------------------------------- */
.qg-form {
  margin: 0;
}
.tq-answer-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--nb-s3);
  margin-bottom: var(--nb-s2);
}
.tq-answer-label {
  font: var(--nb-type-label);
  color: var(--nb-ink-2);
}
.tq-char-count {
  font: var(--nb-type-label);
  /* Resting counter recedes to quiet meaningful text (400 / ink-3) so the
     two warning steps below read as real jumps, not shade shifts. */
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--nb-ink-3);
  transition: color var(--nb-dur-1) var(--nb-ease);
}
/* Dark ink-3 lands at 4.7334:1 here. This is 13px at weight 400, so it is
   normal-size text owing 4.5:1 — passing, but inside the margin band this
   project has been bitten in before. ink-2 in dark only.
   :not(.is-near):not(.is-max) IS LOAD-BEARING: without it this rule
   computes (0,2,1) and swallows both warning states below, which ship at
   (0,2,0) — the 450-character warning and the 500-character cap would
   both go inert in dark. */
html.theme-dark .tq-char-count:not(.is-near):not(.is-max),
html:not(.theme-light) .tq-char-count:not(.is-near):not(.is-max) {
  color: var(--nb-ink-2);
}
/* Approaching the 500-character cap. Both states step on weight and ink,
   never on a new hue -- the counter is a meter, not an alarm. Shipped,
   neither state changed anything: --nb-type-label already carries 600, so
   .is-max's 600 was the weight it already had, and .is-near repeated the
   base colour, leaving grader.js's 450-character warning invisible on this
   skin. Near now darkens to full ink at 700; max keeps the error red and
   takes the same 700, matching grader.css:243 on the legacy skin. */
.tq-char-count.is-near {
  color: var(--nb-ink);
  font-weight: 700;
}
.tq-char-count.is-max {
  color: var(--nb-error);
  font-weight: 700;
}

/* The writing area — a raised paper field in the student's own voice. */
.tq-input-wrap {
  position: relative;
}
.qg-input,
.tq-signin-hint,
.nb-qg-empty .nb-empty-note {
  font: var(--nb-type-ui);
}
.qg-input {
  display: block;
  width: 100%;
  min-height: 108px;
  padding: var(--nb-s3) var(--nb-s4);
  /* The field reads as paper, not a well: a hairline bound with a ruled
     base line (--nb-border-strong: 3.44:1 light; dark keeps its quiet
     boundary doctrine and still beats the old control-border) carrying
     the boundary cue. No float shadow — a page doesn't float on itself.
     Focus (2px blue outline below) does the state work. */
  border: 1px solid var(--nb-border);
  border-bottom-color: var(--nb-border-strong);
  border-radius: var(--nb-r-md);
  color: var(--nb-ink);
  resize: vertical;
  transition:
    border-color var(--nb-dur-1) var(--nb-ease),
    background-color var(--nb-dur-1) var(--nb-ease);
}
/* :not(.is-locked) is LOAD-BEARING even though source order alone would
   let .qg-input.is-locked win here: it guards the live marking-lock state
   (sunken field) against any later sheet that restates the ground at equal
   specificity — the lab's cascade caught exactly that bug on 2026-08-28. */
.qg-input:not(.is-locked) {
  background: var(--nb-paper-raised);
}
html.theme-dark .qg-input:not(.is-locked),
html:not(.theme-light) .qg-input:not(.is-locked) {
  /* Dark's lightest writing surface, mirroring light's relationship:
     5% ink over the raised sheet — the same 1.14:1 step the model-answer
     recess uses. Dark is designed, not derived. */
  background: color-mix(in srgb, var(--nb-ink) 5%, var(--nb-paper-raised));
}
.qg-input::placeholder {
  /* ONE INK (owner, 2026-09-26): a placeholder is full ink, set in italic so an empty field never reads as filled */
  color: var(--nb-ink);
  font-style: italic;
}
html.theme-dark .qg-input::placeholder,
html:not(.theme-light) .qg-input::placeholder {
  /* one ink (owner, 2026-09-26): full ink, the italic says it is a hint;
     the 400-vs-600 weight keeps it quieter than the label. */
  color: var(--nb-ink);
}
.qg-input:hover:not(:focus):not(.is-locked) {
  border-color: var(--nb-border-strong);
}
.qg-input:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
  border-color: var(--nb-blue);
}
/* Locked while grading — reads inert, not broken. */
.qg-input.is-locked {
  background: var(--nb-paper-sunken);
  color: var(--nb-ink-2);
  cursor: progress;
}

/* Functional hint (validation) — plain UI error voice, never the examiner. */
.qg-hint {
  margin: var(--nb-s2) 0 0;
  font: var(--nb-type-label);
  color: var(--nb-error);
}
.qg-hint[hidden] {
  display: none;
}

.tq-answer-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--nb-s3);
  margin-top: var(--nb-s6);
}
.tq-foot-meta {
  display: inline-flex;
  align-items: center;
  gap: var(--nb-s3);
  font: var(--nb-type-label);
  color: var(--nb-ink);
}
/* "Worth N marks" — the examiner's allocation, so it wears the serif-italic red. */
.tq-worth {
  display: inline-flex;
  align-items: center;
  gap: var(--nb-s2);
  font-family: var(--nb-serif);
  font-style: italic;
  /* The board prints the allocation; printing is not judging (2026-08-28). */
  color: var(--nb-ink);
}
.tq-worth svg {
  color: var(--nb-ink);
}
.tq-foot-sep {
  display: inline-block;
  width: 1px;
  height: 0.9em;
  background: var(--nb-border-strong);
}
/* "Saved just now" — a plain UI confirmation (success voice), not a judgment. */
/* Autosave gives up green (2026-08-29). Green now carries two referents
   already — an earned mark, and the AI speaking — and "your work is
   saved" is a third. It is not a mark, it is not the AI, and it sits in
   the student's zone where the examiner has no hand at all, so it is the
   one green the card can give up. Ink-3 light (5.36:1); ink-2 dark,
   because dark ink-3 is the palette's thin tier at label size. */
.qg-saved {
  display: inline-flex;
  align-items: center;
  gap: var(--nb-s2);
  font: var(--nb-type-label);
  color: var(--nb-ink-3);
}
.qg-saved svg {
  color: var(--nb-ink-3);
}
html.theme-dark .qg-saved,
html.theme-dark .qg-saved svg,
html:not(.theme-light) .qg-saved,
html:not(.theme-light) .qg-saved svg {
  color: var(--nb-ink-2);
}
.qg-saved[hidden] {
  display: none;
}

/* ---------- 6 · Mark button ----------------------------------- */
/* The primary action — the student's blue pen. Inherits nothing from nb-btn
   (the fragment carries no nb-btn class), so it is built fully here. */
/* 44px at EVERY width, not only under pointer:coarse. This computed to ~40px
   (18px content + 20px padding + 2px border) while §14 raised four other
   controls and missed the primary action on the page — and 40px is under the
   floor for a mouse too. 12px padding + 18px content + 2px border lands at
   exactly 44; the min-block-size is belt and braces if the icon ever changes. */
.tq-mark-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--nb-s2);
  min-block-size: 44px;
  /* One step narrower than s6 (2026-08-28): the one filled action stops
     competing with the question by footprint, not by colour. */
  padding: var(--nb-s3) var(--nb-s5);
  background: var(--nb-blue);
  border: 1px solid var(--nb-blue);
  border-radius: var(--nb-r-sm);
  color: var(--nb-on-blue);
  font: var(--nb-type-control);
  cursor: pointer;
  transition:
    background-color var(--nb-dur-1) var(--nb-ease),
    transform var(--nb-dur-1) var(--nb-ease);
}
/* Hover only where there is a hover (mobile audit 2026-09-25, S-028a): on a
   touch screen a tap leaves :hover on the button, and after marking it sat a
   shade darker and 1px lifted until the next tap elsewhere. Kept before the
   :active rule, so the equal-specificity press still wins on a mouse. */
@media (hover: hover) {
  .tq-mark-btn:hover:not(:disabled) {
    background: var(--nb-blue-strong);
    border-color: var(--nb-blue-strong);
    transform: translateY(-1px);
  }
}
.tq-mark-btn:active:not(:disabled) {
  transform: translateY(0);
}
.tq-mark-btn:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
}
.tq-mark-btn:disabled {
  background: var(--nb-paper-sunken);
  border-color: var(--nb-border);
  color: var(--nb-quiet-2);
  cursor: not-allowed;
}
.tq-mark-icon {
  /* Retired (2026-08-28): a tick on the SUBMIT dilutes the examiner's own
     mark vocabulary — ticks appear where marks are earned. The fragment
     still carries the SVG (frozen markup); CSS retires it. */
  display: none;
}
/* Spinner slot (fragment carries .qg-spinner; grader.js swaps the label text). */
.qg-spinner {
  display: none;
}

/* ---------- 7 · Grade card ------------------------------------ */
/* THE MODE BOUNDARY — the marked answer, written on the exam sheet itself and
   not in a nested well. Exactly two signals, and no third:
     WEIGHT    2px solid --nb-quiet-3 (the third ink before one ink, kept
               for this rule; a full-ink 2px rule would shout): still the
               heaviest rule on the card
               (5.33:1 light / 4.44:1 dark on the sheet vs 1px --nb-border
               at 1.27/1.31 everywhere else) and within 8-19% of the red it
               replaced in BOTH themes. Neutral on purpose (2026-08-28):
               red now means a lost mark and nothing else, so the mode
               change is carried by weight and geometry alone.
     GEOMETRY  full bleed. Every other rule on this card is inset; this one
               cuts the sheet edge to edge. The negative inline margin bleeds
               the border while the padding puts the content back on the
               card's own measure, so only the RULE crosses the edge.

   NO GROUND STEP — deliberate, measured, and not an oversight to repair. An
   earlier draft sat the marking on the page ground. Separation against the
   sheet measured 1.0555:1 light and 1.1043:1 dark: a value that differs
   rather than a signal, surviving neither a dimmed screen nor an angled
   phone, and markedly weaker in light than in dark — a boundary present in
   one theme and absent in the other is worse than one absent from both.
   Dropped in BOTH themes, owner call 2026-08-22. Restoring it means
   re-measuring, not re-adding.

   LADDER 2 of 4: zone 2 -> zone 3 = 48 + 2 + 32 = 82px, the largest gap on
   the card. Handing your work to the examiner is the one real break in it.
   Revealed on grade (grader.js un-hides). */
.tq-grade-card {
  margin-top: var(--nb-s9);
  margin-inline: calc(-1 * var(--tq-pad));
  padding: var(--nb-s7) var(--tq-pad) var(--nb-s5);
  background: transparent;
  border: 0;
  border-top: 2px solid var(--nb-quiet-3);
  border-radius: 0;
}
.tq-grade-card[hidden] {
  display: none;
}
.qg-live,
.qg-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.tq-grade-head {
  display: flex;
  align-items: baseline;
  gap: var(--nb-s3);
  margin-bottom: var(--nb-s4);
}
/* The old icon-in-a-circle seal is retired — the examiner's voice is the serif
   title itself. The element stays (fragment markup is shared with the legacy
   page) but paints nothing here. */
.tq-grade-badge {
  display: none;
}
.tq-grade-head-text {
  display: flex;
  align-items: baseline;
  column-gap: var(--nb-s3);
  flex-wrap: wrap;
  min-width: 0;
  margin-right: auto;
}
/* "Marked answer" — the examiner's title, serif italic red. */
.tq-grade-title {
  font-family: var(--nb-serif);
  font-style: italic;
  font-weight: 600;
  font-size: var(--nb-text-md);
  color: var(--nb-ink);
}
/* Provenance, not a peer of the title above it, so it drops a tier.
   LIGHT ONLY: ink-3 measures 5.36:1 on the sheet. Dark stays at ink-2
   because dark ink-3 lands at 4.73:1 at label size — passing, but the
   palette's own thin tier, and this is 13px. */
.tq-grade-sub {
  font: var(--nb-type-label);
  color: var(--nb-ink-3);
}
html.theme-dark .tq-grade-sub,
html:not(.theme-light) .tq-grade-sub {
  color: var(--nb-ink-2);
}
/* Reset — a quiet secondary action (blue interaction). Quiet is a matter of
   ink and weight, not of hit area: this computed to ~28px, the smallest target
   on the card. 44px both axes at every width. */
.tq-grade-reset {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--nb-s2);
  min-block-size: 44px;
  min-inline-size: 44px;
  padding: var(--nb-s2) var(--nb-s4);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--nb-blue) 40%, transparent);
  border-radius: var(--nb-r-sm);
  color: var(--nb-blue);
  font: var(--nb-type-control);
  cursor: pointer;
  transition:
    color var(--nb-dur-1) var(--nb-ease),
    border-color var(--nb-dur-1) var(--nb-ease),
    background-color var(--nb-dur-1) var(--nb-ease);
}
.tq-grade-reset:hover {
  color: var(--nb-blue-strong);
  border-color: var(--nb-blue-strong);
  background: var(--nb-blue-wash);
}
.tq-grade-reset:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
}
/* No Reset while a mark is in flight (mobile audit 2026-09-25, S-012). The
   legacy sheet has always had this guard (grader.css, data-state grading);
   the notebook sheet never ported it, so the skeleton card showed a live
   Reset that could not cancel the request: pressing it cleared the answer,
   then the late grade painted a marked card over the empty field and saved
   it. display:none also takes it out of the tab order. It returns with the
   grade, or with the idle layout if marking fails. */
.nb-qg-practice .qg[data-state="grading"] .tq-grade-reset {
  display: none;
}

/* Skeleton (Motion shimmer placeholder while the grade is in flight). */
.tq-grade-skeleton {
  display: none;
}
.tq-grade-card.is-loading .tq-grade-skeleton {
  display: block;
}
.tq-grade-card.is-loading .tq-grade-body {
  display: none;
}
.tq-sk-list {
  list-style: none;
  margin: 0 0 var(--nb-s4);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--nb-s3);
}
.tq-sk-bar,
.tq-sk-line,
.tq-sk-chip {
  position: relative;
  display: block;
  overflow: hidden;
  /* sunken placeholder on the raised sheet (the card well is gone).
     background-color rather than the shorthand, because the pulse below
     animates exactly this property. */
  --tq-sk-base: var(--nb-paper-sunken);
  --tq-sk-peak: var(--nb-border);
  background-color: var(--tq-sk-base);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-sm);
  animation: nb-sk-pulse calc(var(--nb-dur-3) * 4) var(--nb-ease) infinite;
}
/* The wait is a neutral placeholder shimmer (2026-08-28: the red tint went
   with the red-means-lost-marks rule): the ground breathes instead of a
   highlight sliding over it, so there are no edges to go hard. Dark bars sit
   a step ABOVE their ground (--nb-border over the raised sheet) — they
   shipped as sunken-on-raised, darker than their own ground: voids. Reduced motion is handled by ZEROING, not by a rule of its own --
   notebook-tokens.css:328-330 drops --nb-dur-3 to 0ms under
   prefers-reduced-motion, the duration computes to 0, and the bars simply rest
   at their --tq-sk-base (paper-sunken light / border dark). That is the same static placeholder those users
   already got, since buildShimmer returns early at grader.js:323 and never
   injects a span for them. */
@keyframes nb-sk-pulse {
  0%, 100% { background-color: var(--tq-sk-base); }
  50%      { background-color: var(--tq-sk-peak); }
}
html.theme-dark .tq-sk-bar, html:not(.theme-light) .tq-sk-bar,
html.theme-dark .tq-sk-chip, html:not(.theme-light) .tq-sk-chip,
html.theme-dark .tq-sk-line, html:not(.theme-light) .tq-sk-line {
  --tq-sk-base: var(--nb-border);
  --tq-sk-peak: var(--nb-border-strong);
}
.tq-sk-bar {
  height: var(--nb-s5);
}
.tq-sk-bar--label {
  width: 40%;
}
.tq-sk-bar--title {
  width: 55%;
  margin-bottom: var(--nb-s3);
}
.tq-sk-line {
  height: var(--nb-s4);
  margin-bottom: var(--nb-s2);
}
.tq-sk-line--short {
  width: 60%;
}
.tq-sk-chip {
  height: var(--nb-s5);
  width: var(--nb-s8);
}
.tq-sk-total {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--nb-s3);
  margin-bottom: var(--nb-s4);
}
.tq-sk-band {
  padding-top: var(--nb-s3);
  border-top: 1px solid var(--nb-border);
}
/* The shine grader.js appends inside each skeleton surface. It PAINTS NOTHING.
   buildShimmer still creates the span and Motion still translates it from
   -130% to 330%; we simply stop giving it a surface, so grader.js is untouched
   and no DOM contract moves. The wait is carried by the ground pulse on the
   bars below instead.

   A translated span needs SOFT EDGES to read as a shimmer. The blue gradient
   it used to carry had them; the solid wash that briefly replaced it did not,
   and slid across as a hard-edged block. Both were wrong for the same reason
   in the end -- the wait is the examiner's, and it should not be a moving
   highlight in the student's own pen. */
.tq-sk-shine {
  display: none;
}

/* Keypoints — the mark scheme as a ruled checklist on the paper itself:
   hairline-separated rows (notebook lines), no nested boxes. The examiner's
   pen (tick + verdict) is the only colour. */
.tq-keypoints {
  margin: 0;
}
.tq-kp-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
}
/* A real two-track grid, not `justify-content: space-between`. Space-between
   shares only a RIGHT edge, so "Not mentioned" and "1 mark" started at wildly
   different x — measured 2 distinct left edges across 4 rows, which reads as
   prose rather than as a column. A fixed track gives the verdict both edges.
   6.25rem is sized for "Not mentioned", the longest string grader.js:205/:370
   can write; the shorter values right-align inside it.

   The line-height is a LENGTH, not a ratio, and that is load-bearing. A
   unitless 1.65 inherits as the NUMBER, so a 13px child would compute its own
   21.45px box and sit 8px below the label's first line (measured). Resolving it
   here to calc(17px * 1.65) = 28.05px means both children inherit the same
   computed LENGTH, their first line boxes match exactly, and `align-items:
   start` puts the mark level with line one of a wrapped point. */
.tq-kp {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 6.25rem;
  column-gap: var(--nb-s4);
  align-items: start;
  padding: var(--nb-s3) var(--nb-s1);
  border-bottom: 1px solid var(--nb-border);
  font-size: var(--nb-text-md);
  line-height: calc(var(--nb-text-md) * 1.65);
  transition: background-color var(--nb-dur-1) var(--nb-ease);
}
.tq-kp:last-child {
  border-bottom: 0;
}
/* A lost point recedes. Until now .tq-kp carried no .is-matched rule at all, so
   the row itself said nothing about its state — everything rested on the glyph
   and the verdict string. Ink is a value change, so it survives greyscale. */
/* The role stays var(--nb-type-prose) — pinned by
   pages/tests_typography_stage5_wave3.py:283, and correct anyway. It resolves to
   400 1.0625rem/1.65, i.e. 17px on a 28.05px line box, which is EXACTLY what
   .tq-kp sets above. So the label lands on the shared line box without needing
   to inherit anything, and the typography ratchet does not move at all. */
/* ⚑ THE LABEL BREAKS INSIDE ITS OWN TRACK (TQ follow-up 2026-09-24). The
   track was already minmax(0, 1fr) and the label min-width: 0, but that only
   lets the BOX shrink: a run the line breaker cannot split ("Evaporation/
   crystallisation", "(6400→3200→1600→800→400)", or one word wider than the
   whole column at 320) still painted past it, under the verdict at 390, 360
   and 320 and past the sheet's edge at 320. topic-question-card.js offers a
   break after a slash that joins two words and after an arrow, and gives a
   word wider than the column soft hyphens inside a .tq-kp-long span; only a
   word with no syllable join to use (a formula) is marked --anywhere, the
   last resort. The label itself keeps normal wrapping, so no word that fits
   is ever split. */
.tq-kp-label {
  position: relative;
  min-width: 0;
  padding-left: var(--nb-s5);
  font: var(--nb-type-prose);
  color: var(--nb-ink-2);
}
.tq-kp-label .tq-kp-long--anywhere {
  overflow-wrap: anywhere;
}
.tq-kp.is-matched .tq-kp-label {
  color: var(--nb-ink);
}
/* Examiner marks in the margin (restored from legacy by owner request).
   Owner call 2026-07-11: green ✓ = awarded stays — green-means-correct is too
   over-learned to spend attention on. ✓ keeps the serif tick at the canonical
   −4°, stamped in with a quick ink-press when grader.js flips .is-matched. The
   keyframe runs on class-add; --nb-dur-2 zeroes under reduced motion.

   Owner call 2026-08-27: the LOST mark moves --nb-error → --nb-red. A mark the
   examiner declined to award is a judgment, not a form-validation error, and
   the sheet's own header says --nb-error is the plain-UI voice that must never
   wear the examiner treatment — which is exactly what a serif-italic --nb-error
   verdict was doing. The two were also indistinguishable on screen (#9B3028 vs
   #A33B35 light, #E0655A vs #E86A5A dark), so the distinction the header rests
   on never survived rendering. --nb-error now appears on this card in exactly
   one place: .qg-hint.

   line-height: inherit, not 1.6 — it must pick up the computed length set on
   .tq-kp so the glyph shares the label's first line box. */
.tq-kp-label::before {
  content: "\2715"; /* ✕ */
  position: absolute;
  left: 0;
  top: 0;
  font-size: 0.85em;
  font-weight: 700;
  line-height: inherit;
  color: var(--nb-red);
}
.tq-kp.is-matched .tq-kp-label::before {
  content: "\2713"; /* ✓ */
  font-family: var(--nb-serif);
  font-size: 1em;
  font-weight: 700;
  color: var(--nb-success);
  transform: rotate(-4deg);
  animation: nb-kp-stamp var(--nb-dur-2) var(--nb-ease) both;
}
@keyframes nb-kp-stamp {
  from { opacity: 0; transform: rotate(-4deg) scale(0.4); }
  60%  { opacity: 1; transform: rotate(-4deg) scale(1.15); }
  to   { opacity: 1; transform: rotate(-4deg) scale(1); }
}
/* The verdict per point — serif italic, in the same voice as the margin marks:
   "1 mark" = green (earned), "Not mentioned" = examiner red (lost). Right-
   aligned inside its fixed track, with tabular figures so the numerals line up
   the way every other number on this card already does (.tq-q-no, .qg-score,
   .qg-pager-num, .qg-pager-count b). line-height: inherit for the shared first
   line box — see .tq-kp. */
.tq-kp-status {
  font-family: var(--nb-serif);
  font-style: italic;
  font-size: var(--nb-text-sm);
  line-height: inherit;
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--nb-red);
}
.tq-kp.is-matched .tq-kp-status {
  color: var(--nb-success);
  font-weight: 600;
}

/* ---------- 8 · Score + examiner feedback band ---------------- */
/* The margin score division (.tq-total-value carries is-full/is-partial). It
   is written in the question head (.tq-q-marks--score); the class-toggle just
   re-weights it. */
.tq-total-value.is-full,
.tq-total-value.is-partial {
  font-weight: 700;
}

/* The examiner's comment. Still a SIBLING of .tq-grade-card, as grader.js
   expects, but the two now share one full-bleed region and touch, so they read
   as one hand rather than two announcements. The 3px double red rule is
   DEMOTED to a neutral inset hairline: there is one mode change on this card
   and it has already happened above. Inset rather than full bleed on purpose
   — a second rule crossing the sheet edge would dilute the single signal the
   mode boundary has left.
   LADDER 3 of 4: zone 3 -> the comment = 20 (the grade card's own
   padding-bottom) + 1 + 16 = 37px. A pause inside the examiner's hand. */
.tq-feedback-band {
  margin: 0;
  padding: var(--nb-s4) 0 var(--nb-s5);
  background: transparent;
  border: 0;
  border-top: 1px solid var(--nb-border);
}
.tq-feedback-band[hidden] {
  display: none;
}
.tq-block-title {
  display: inline-flex;
  align-items: center;
  gap: var(--nb-s2);
  margin-bottom: var(--nb-s3);
  font-family: var(--nb-serif);
  font-style: italic;
  font-weight: 600;
  font-size: var(--nb-text-sm);
  letter-spacing: 0.01em;
}
/* The AI marker's label, in green (owner call 2026-08-29). Named
   --nb-success, not --nb-adm-tip: the two are byte-identical in both
   themes, so the pixel is exactly as specified, but --nb-adm-* is the
   reserved NOTES callout palette and DESIGN.md documents exam-tip as "a
   plain green sans note" — which this Vollkorn italic 600 label is not.
   RECORDED: this is byte-identical to .tq-kp.is-matched .tq-kp-status in
   colour AND in all four typographic axes, about 37px away across one
   hairline. Green therefore says both EARNED MARK and THE AI IS SPEAKING
   inside one region. Accepted knowingly; a colour-only pass cannot
   separate them, and .qg-saved gives up its green below to keep the
   count from rising further. */
.tq-block-title--accent {
  color: var(--nb-success);
}
.tq-block-title--accent svg {
  color: var(--nb-success);
}
/* The comment itself reads in the examiner's hand: the notebook serif, ink. */
/* The comment reads at a PURE value — black on paper, white at night
   (owner call 2026-08-29). It is the only pure ink on the site, and the
   consequence is deliberate: at 19.72:1 light / 17.23:1 dark the AI's
   note is now the brightest text in the marking region, above the
   mark-scheme points it explains. --nb-feedback-ink carries it because
   this file must stay at zero hex literals. */
.tq-block-body {
  margin: 0;
  font: var(--nb-type-examiner);
  color: var(--nb-feedback-ink);
  /* ⚑ LINING FIGURES. --nb-type-examiner resolves to Vollkorn italic, whose
     DEFAULT figures are oldstyle: digit 1 sits as a serifed i-form and 0 as
     a small o, so "4π²" and "24 hours" and "86400 s" read wrong in the one
     place on this card where a number is the mark. Verified with fontTools
     against the shipped binaries: every vollkorn-v30-latin-*.woff2 carries
     the `lnum` feature and real one.lf / zero.lf alternates, so this
     substitutes rather than silently doing nothing.
     ⚑ Not counted by the typography ratchet, which collects only
     font / font-family / font-weight (tests_font_foundation.py:628) and raw
     font-size (:631), so this adds no debt and moves no fingerprint. */
  font-variant-numeric: lining-nums;
}
html.theme-dark .tq-block-body,
html:not(.theme-light) .tq-block-body {
  /* DO NOT delete this rule to chase the colour: max-width is the only
     thing capping the dark comment's measure, and dropping it re-wraps
     the paragraph and changes the graded card's height in dark ONLY —
     a theme-asymmetric height break. The colour below is now the same
     token the light rule uses; the rule survives for the measure.
     (Superseded 2026-08-29: this was --nb-ink-2, chosen when the dark
     comment read as a near-white serif wall. The owner has since asked
     for the opposite — maximum legibility, pure white.) */
  color: var(--nb-feedback-ink);
  max-width: 68ch;
}
.qg-feedback-text strong {
  font-weight: 700;
  /* Key terms inherit the body's pure value. At --nb-ink they rendered
     DIMMER than the text they emphasise, which reads as a bug. Weight
     carries the emphasis on its own. */
  color: inherit;
}

/* ---------- 9 · Model-answer reveal --------------------------- */
/* LADDER 4 of 4: zone 3 -> zone 4 = 20 (the comment's padding-bottom) + 4 =
   24px, the closest pair on the card. The model answer is offered in the same
   breath as the comment. With the comment hidden the reveal falls back to a
   full 32px against the marking above it. */
.tq-reveal {
  margin-top: var(--nb-s7);
}
.tq-feedback-band:not([hidden]) + .tq-reveal {
  margin-top: var(--nb-s1);
}
/* The toggle — a DISCLOSURE, not a second action (2026-08-28): the pill
   chrome went; the chevron + blue label carry the affordance and the wash
   answers the pointer. The border box stays 1px (transparent) so layout
   holds. The 44px floor stays here from §14 so it applies at every width,
   not only on a touch-primary device; §14 keeps its own copy. */
.tq-reveal-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--nb-s2);
  min-block-size: 44px;
  padding: var(--nb-s2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--nb-r-sm);
  color: var(--nb-blue);
  font: var(--nb-type-control);
  cursor: pointer;
  transition:
    color var(--nb-dur-1) var(--nb-ease),
    border-color var(--nb-dur-1) var(--nb-ease),
    background-color var(--nb-dur-1) var(--nb-ease);
}
/* Hover only where there is a hover (S-028a): a tapped reveal kept the
   hover ink after it opened. */
@media (hover: hover) {
  .tq-reveal-btn:hover {
    color: var(--nb-blue-strong);
    background: var(--nb-blue-wash);
  }
}
.tq-reveal-btn:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
}
.tq-reveal-chevron {
  transition: transform var(--nb-dur-2) var(--nb-ease);
}
.tq-reveal.is-open .tq-reveal-chevron {
  transform: rotate(180deg);
}
/* The panel — the model answer as reference material the reader just asked
   for, not a callout. It stops being a box: no border, no radius, full bleed,
   recessed onto --nb-paper-sunken, and it is now the card's ONLY ground
   change. It was tinted with --nb-adm-def, the DEFINITION-callout hue that
   notebook-tokens.css reserves for callouts and forbids reusing decoratively.
   Its separation from the sheet measures 1.1410:1 light / 1.1423:1 dark. That
   is under the 1.15 bar the marking seam failed, and it is kept only because
   it carries no mode change: this is a disclosure the reader opened with the
   button above it, its full-bleed geometry is its real edge, and the two
   themes agree to within 0.002. RESOLVED 2026-08-28: the band reads as a
   recessed well OF the sheet — inset, radiused, hairline-bound — and the
   1.14:1 ground step is now defended by shape + border, no token change. */
.tq-reveal-panel {
  margin: var(--nb-s4) 0 0;
  padding: var(--nb-s4) var(--nb-s5);
  background: var(--nb-paper-sunken);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-md);
}
/* DARK ONLY (2026-08-29). --nb-paper-sunken (#0A0B0E) sits BELOW the
   card's raised ground and read as a hole cut in the sheet — the same
   fault the textarea had. The panel lifts by the sheet's own established
   step, 5% ink over raised, the exact expression the field ships above.
   THE LIFT IS PAIRED WITH THE HAIRLINE, and the pairing is not optional:
   measured, the lifted ground separates at 1.1368:1, BELOW the sunken
   1.1423:1 it replaces, and against it the shipped --nb-border collapses
   from 1.4921:1 to 1.1490:1. Lifting the ground without lifting the edge
   would leave the well with no step AND no visible border — worse than
   the hole. --nb-border-strong restores the edge to 1.4581:1.
   Light is untouched: its recess reads correctly as a well in paper. */
html.theme-dark .tq-reveal-panel,
html:not(.theme-light) .tq-reveal-panel {
  background: color-mix(in srgb, var(--nb-ink) 5%, var(--nb-paper-raised));
  border-color: var(--nb-border-strong);
}
.tq-reveal-panel[hidden] {
  display: none;
}
.tq-model-body {
  font: var(--nb-type-prose);
  color: var(--nb-ink);
}
.tq-model-body :first-child {
  margin-top: 0;
}
.tq-model-body :last-child {
  margin-bottom: 0;
}
.tq-model-body .katex {
  color: var(--nb-ink);
}

/* Non-gradable variant (free / no-scheme / signed-out): the exam sheet minus
   the marking form; a quiet upgrade note where the answer space would be. */
.tq-paper--ungraded {
  /* --nb-paper -> --nb-paper-raised. This card carries no examiner content at
     all, so it must not sit on the marking ground. With the zone 3 ground step
     measured away the card carries ONE ground top to bottom, and a gradable
     sheet and an ungraded sheet are visibly the same object — correct, since
     the only difference between them is whether it can be marked. --nb-paper
     is left to the desk the card sits on. */
  background: var(--nb-paper-raised);
  /* Its own, much smaller stable floor (these cards are short: question +
     a one-line note). Evens out the 1- vs 2-line question wobble so the pager
     stays put here too, WITHOUT the big empty box the gradable floor would
     leave. Idle range ~183-234px, so this fits snugly.
     Transported from the idle measurement like the gradable floor above,
     and re-measured against the shipped sheet rather than the lab's Phase 3
     figures: 236.5 -> 240.5, delta +4.0, so 240 + 4 = 244.

     2026-09-12 RE-DERIVED with the gradable floor, same method and the same
     reason: the section 4 question band levels these cards at source, so every
     ungraded card of a real topic measures 258.0px natural at 721, 900 and
     1440px with the floor lifted. 244 would now be inert, which is worse than
     wrong -- it would read as a live constraint. Below 720px the floor stays
     under the natural heights (242-300px across 320-719) exactly as it did
     before; the signed-out card is not levelled there and was not before. */
  min-height: 258px;
}
.tq-signin-hint {
  margin: 0;
  color: var(--nb-ink-2);
}

/* ---------- 10 · Pager (question-pager.js) -------------------- */
/* Injected after .noteviewer-body, then ANCHORED here: the bar is fixed to the
   bottom of the viewport, so where the reader finds Next no longer depends on
   how tall the question above it happens to be. Previous | windowed numbers +
   progress | Next. All interaction, so all blue.

   WHY. In flow, the bar rode on the card. Stepping one 18-question set with
   the model answer open, the Next button's viewport top travelled 497.9px
   across 15 distinct positions, and on the longest question the whole bar sat
   at y=1126 in a 900px window — below the fold. The card floors in §3 only
   ever levelled the IDLE case (spread 134.6px); once a reader opens a model
   answer the spread is 399.3px and once a question is marked it is 841.6px,
   because the same card is legitimately 420px idle and 1536px graded. No
   height can be shared, so the bar had to stop depending on height.

   FIXED, NOT STICKY. position:sticky can only lift the bar off its natural
   place while its containing block's bottom is below the fold, so on a window
   taller than the page it silently degrades to the old behaviour: measured
   302.8px of travel at 1440x1300, and a min-height on .nb-qg-column does NOT
   rescue it (sticky has nothing to stick against when the page cannot scroll).
   Fixed measured 0.0px of travel at 1440x900, 1440x1300 and 390x844 alike.

   FULL-BLEED PAPER, NOT A FLOATING CARD. A card-width bar lets content pass
   BESIDE it as well as behind it, so a display equation is cut into three
   pieces and reads as a rendering fault. An edge-to-edge --nb-paper surface
   under a hairline makes the occlusion legible — content slides under a desk
   edge. This page already lost a fixed pager once (grader.css, 2026-06-24,
   reverted because content scrolling behind it looked like breakage), so the
   second attempt has to be obvious at a glance. Paper, not an app bar.

   The gutter matches .nb-page-main's own (notebook-page.css:252), so the bar
   is the bottom mirror of the full-width .topbar rather than a second, subtly
   different measure. No clearance is reserved anywhere any more — the bar gets
   out of the way instead; see the .is-tucked rule below. */
.qg-pager {
  position: fixed;
  inset: auto 0 0 0;
  /* Above page content; below the floating theme toggle (800), the mobile nav
     drawer (900), the topbar (1000) and the rail drawer (1200/1210). */
  z-index: 700;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--nb-s4);
  padding: var(--nb-s3) clamp(20px, 2.5vw, 48px);
  background: var(--nb-paper);
  border-top: 1px solid var(--nb-border);
  /* The lift of a sticky bar (DESIGN.md section 4: --nb-shadow-2 is the step
     for "sticky bars"). The token casts down, so off the bottom of the window
     only its blur shows, as a soft shade just above the desk edge: enough to
     read as a control strip lifted over the page (item 10 refix, the phone
     bar was still flat paper on paper). Surface and hairline are unchanged. */
  box-shadow: var(--nb-shadow-2);
  /* Transform only — the bar never animates a layout property. visibility is
     transitioned with a 0s duration DELAYED to the end of the slide, so the
     tucked bar leaves the a11y tree and the tab order once it is off-screen,
     and re-enters instantly on the way back. */
  transition:
    transform var(--nb-dur-2) var(--nb-ease),
    visibility 0s linear 0s;
}

/* THE PROGRESS RULE (owner list 2026-09-24, item 10: "make the TQs navbar for
   questions more prominent"). The bar's top hairline doubles as its progress
   meter: a 3px blue ink-fill rides along the desk edge from the left, as far
   as the reader has come through the set. question-pager.js writes the
   fraction as --qg-progress (0 to 1) on the bar at every render, so this
   works at every width, including the phone bar where the chips and the
   inline track stand down. It replaces the 96px inline track, which said the
   same thing in a 3px line nobody could find, and it costs the bar no height.
   Transform only (scaleX from the left edge); --nb-dur-3 zeroes under
   reduced motion, so the fill simply lands. Blue, because progress is one of
   the five things the student's blue is for (DESIGN.md, Three-Ink Rule). The
   bar surface itself stays --nb-paper under a hairline: desk paper, not an
   app bar. */
.qg-pager::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  width: 100%;
  height: 3px;
  background: var(--nb-blue);
  transform: scaleX(var(--qg-progress, 0));
  transform-origin: left center;
  transition: transform var(--nb-dur-3) var(--nb-ease);
  pointer-events: none;
}

/* ⚑ THE BAR TUCKS WHEN THE FOOTER REACHES IT, AND IS STILL FIXED THE REST OF
   THE TIME. Read the paragraph above before touching this: fixed is the THIRD
   position this pager has had, and both earlier ones were measured failures.

     · IN FLOW  — the bar scrolled away from the reader on any topic longer
       than the window. That is the state this design replaced.
     · STICKY   — reverted: position:sticky can only lift the bar while its
       containing block's bottom is below the fold, so on a window taller than
       the page it silently degrades to in-flow. MEASURED 302.8px OF TRAVEL at
       1440x1300; fixed measured 0.0px at 1440x900, 1440x1300 and 390x844.
       A min-height on .nb-qg-column does not rescue it — sticky has nothing to
       stick against when the page cannot scroll.
     · FIXED, FULL-BLEED PAPER — and even this shape is the second attempt. A
       card-width fixed bar was shipped and REVERTED on 2026-06-24 (grader.css)
       because content passing beside as well as behind it read as a rendering
       fault. Edge-to-edge paper under a hairline is what makes the occlusion
       legible.

   So the complaint this rule answers — "the footer never reaches the bottom of
   the page" — is NOT fixed by returning the bar to flow. It is fixed by giving
   the bar an end: question-pager.js watches the footer with an
   IntersectionObserver whose rootMargin is the bar's own height, so .is-tucked
   goes on at exactly the moment the bar would start covering the footer, and
   the bar slides out of the way instead of sitting on top of it.

   THE BODY RESERVATION IS GONE WITH IT, and that is why the footer can now
   reach the window bottom. It used to be `body:has(.qg-pager){padding-bottom:
   132px}`, holding the footer 51px clear of a bar that never left. Its other
   job — keeping the bar off the last card and off the partial-paywall sheet
   (nb_question_index.html renders that AFTER the pager's insertion point, so
   it is the final flow block on those topics) — is now covered by the tuck
   instead: those blocks can pass under the bar mid-scroll, but the footer
   follows them, so by the time the reader reaches the document bottom the bar
   has tucked and nothing is left occluded. Nothing is permanently hidden,
   which is the property that actually mattered. */
.qg-pager.is-tucked {
  transform: translateY(100%);
  visibility: hidden;
  transition:
    transform var(--nb-dur-2) var(--nb-ease),
    visibility 0s linear var(--nb-dur-2);
}
@media (prefers-reduced-motion: reduce) {
  /* No slide, just hide. The duration tokens already collapse to 0ms under
     this query (notebook-tokens.css), but the translate is spelled away too so
     the tucked bar does not depend on that collapse to avoid moving. */
  .qg-pager {
    transition: none;
  }
  .qg-pager.is-tucked {
    transform: none;
    visibility: hidden;
    transition: none;
  }
}
/* THE BAR STEPS ASIDE WHILE A TOUCH KEYBOARD IS UP (mobile audit 2026-09-25,
   S-027). A browser that shrinks the page for the on-screen keyboard scrolls
   the caret into view and lands it under this fixed bar: measured 73px of the
   answer box covered at 390x844 and 95px at 768x1024, the caret line hidden.
   While an answer box has focus on a touch screen the bar tucks the same way
   it tucks for the footer, and it comes back on blur. Deliberately NOT
   .is-tucked: question-pager.js takes that class off as the page scrolls, and
   it would fight the keyboard. (0,3,1), so it outranks .is-tucked either way.
   Default iOS Safari and Chrome on Android keep the bar behind the keyboard,
   so there the tuck is simply unseen. A mouse and a fine pointer never match,
   so desktop is untouched. */
@media (hover: none) and (pointer: coarse) {
  html:has(.qg-input:focus) .qg-pager {
    transform: translateY(100%);
    visibility: hidden;
    transition:
      transform var(--nb-dur-2) var(--nb-ease),
      visibility 0s linear var(--nb-dur-2);
  }
}
@media (hover: none) and (pointer: coarse) and (prefers-reduced-motion: reduce) {
  html:has(.qg-input:focus) .qg-pager {
    transform: none;
    visibility: hidden;
    transition: none;
  }
}
/* Prev / Next: 44px tall at EVERY width (the coarse-pointer block below keeps
   its own copy, as that test requires), centred content so the phone's
   arrow-only squares sit true. The inline padding stays at --nb-s4: one step
   wider (the first cut of item 10) took 32px from the centre, and on a
   portrait iPad (768, 44px touch chips) a 9-question set's nine chips then no
   longer fitted beside the buttons and broke onto a second row, a 143px bar. */
.qg-pager-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--nb-s2);
  min-block-size: 44px;
  padding: var(--nb-s2) var(--nb-s4);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--nb-blue) 40%, transparent);
  border-radius: var(--nb-r-sm);
  color: var(--nb-blue);
  font: var(--nb-type-control);
  cursor: pointer;
  white-space: nowrap;
  transition:
    color var(--nb-dur-1) var(--nb-ease),
    border-color var(--nb-dur-1) var(--nb-ease),
    background-color var(--nb-dur-1) var(--nb-ease);
}
html.theme-light .qg-pager-btn:not(:disabled) {
  border-color: var(--nb-control-border);
}
/* Hover style guarded to :not(:disabled) — preserves the June-24 label-hide
   fix (a disabled button must not adopt the hover treatment). */
.qg-pager-btn:hover:not(:disabled) {
  color: var(--nb-blue-strong);
  border-color: var(--nb-blue-strong);
  background: var(--nb-blue-wash);
}
.qg-pager-btn:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
}
.qg-pager-btn:disabled {
  color: var(--nb-quiet-2);
  border-color: var(--nb-border);
  cursor: not-allowed;
}
/* NEXT IS THE FILLED ONE. Moving on is what the bar is for, so the forward
   control takes the student's solid blue (the same pair as Mark answer and
   the current-question chip) and Previous stays the outlined partner. This
   is what makes the bar findable at a glance on a phone, where it is two
   arrows and a count. (0,4,0) so it beats the light-theme control border
   (0,3,1) above without a !important; a disabled Next (last question) falls
   through to the quiet disabled rule. */
.qg-pager .qg-pager-btn.qg-pager-next:not(:disabled) {
  background: var(--nb-blue);
  border-color: var(--nb-blue);
  color: var(--nb-on-blue);
}
/* Hover only where there is a hover: on a phone a tap leaves :hover stuck on
   the button, and the filled Next then sat a shade darker than itself on the
   next question (seen on the 390px capture). */
@media (hover: hover) {
  .qg-pager .qg-pager-btn.qg-pager-next:hover:not(:disabled) {
    background: var(--nb-blue-strong);
    border-color: var(--nb-blue-strong);
    color: var(--nb-on-blue);
  }
}
.qg-load-error-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--nb-s3);
  max-width: 34rem;
  text-align: center;
}
.qg-load-error-inner p {
  margin: 0;
  color: var(--nb-error);
  font: var(--nb-type-ui);
}
.qg-load-retry {
  justify-content: center;
  min-block-size: 44px;
}
/* ⚑ ONE LAYOUT PER WIDTH, NEVER PER QUESTION. The centre is two rows (chips,
   then the count under them) and becomes ONE row only from 960px, where every
   set this site has fits beside the count. Deciding by content instead (a
   wrapping row, the first cut of item 10) made the bar change height as the
   reader moved: the chip window is 4 chips on Q1 and Q22 but 7 chips and two
   gaps from Q3, so at 768 the bar was 69px, then 91px, then 69px again, with
   the count jumping from beside the chips to under them. Widest centre
   measured: 542.6px (Q150 of the 176-question whole chapter, 3-digit chips
   and count), and a row holds it from 909px; 960 leaves 48px of slack. Below
   960 the two rows are the same at every question, as they were before
   item 10. The phone block (720 and down) hides the chips, so there the
   centre is the count alone either way. */
.qg-pager-center {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--nb-s2);
  min-width: 0;
}
@media (min-width: 960px) {
  .qg-pager-center {
    flex-direction: row;
    flex-wrap: wrap;
    column-gap: var(--nb-s5);
  }
}
/* And the chips themselves only show where their widest window fits on one
   row between the two labelled buttons: 768px and up. Between 721 and 767
   the 7-chip window of a 3-digit set (up to 382px, 380px with the 44px touch
   chips) is wider than the gap Previous and Next leave (360px at 721), so it
   broke onto a second row on Q99 to Q150 of the whole chapter and the bar
   grew from 91px to 135px mid-set. There the bar takes the phone's shape,
   Previous | Question N of M | Next on one row, which is the same at every
   question. (0,2,0) because the base .qg-pager-nums rule comes after this
   one in the sheet.) */
@media (min-width: 721px) and (max-width: 767px) {
  .qg-pager .qg-pager-nums {
    display: none;
  }
}
/* From 768 the chips are back, and the narrowest case is a portrait iPad
   (768, touch chips 44px wide): a 9-question set shows all nine, 428px, and
   with the bar's 16px gaps the centre had 422.9px, so the chips broke onto a
   second row and the bar stood 143px tall (pre-existing, also at HEAD). The
   8px gap gives the centre 438.9px there. */
@media (min-width: 768px) and (max-width: 959px) {
  .qg-pager {
    gap: var(--nb-s2);
  }
}
/* A PHONE ON ITS SIDE GETS ONE ROW (mobile audit 2026-09-25, S-009). The
   two-row bar above is chosen by width alone, so an 844x390 phone in
   landscape got the tablet's 95px bar under a 72px topbar: 167 of 390px,
   with the answer box and Mark answer underneath it. On a short window the
   chips and the count share one row, as they do from 960px, and Previous
   and Next go to their arrows (their aria-labels stay, as on a small phone),
   so there is room for the widest set, Q150 of 156: the bar is 73px at
   844x390, 932x430 and 915x412, one row, nothing past the window edge.
   Portrait tablets keep the two rows. */
@media (min-width: 768px) and (max-width: 959px) and (max-height: 500px) {
  .qg-pager .qg-pager-center {
    flex-direction: row;
    flex-wrap: nowrap;
    column-gap: var(--nb-s4);
  }
  .qg-pager .qg-pager-prev > span,
  .qg-pager .qg-pager-next > span {
    display: none;
  }
  .qg-pager .qg-pager-prev,
  .qg-pager .qg-pager-next {
    min-inline-size: 48px;
    min-block-size: 48px;
    padding-inline: var(--nb-s3);
  }
}
.qg-pager-nums {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--nb-s1);
  margin: 0;
  padding: 0;
  list-style: none;
}
.qg-pager-gap {
  padding: 0 var(--nb-s1);
  color: var(--nb-ink-3);
}
/* 40px chips in full ink (were 32px in ink-2): a number you can hit and read
   at a glance. The current chip keeps the solid selection fill below. */
.qg-pager-num {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--nb-s8);
  height: var(--nb-s8);
  padding: 0 var(--nb-s2);
  overflow: hidden;
  background: transparent;
  border: 1px solid var(--nb-control-border);
  border-radius: var(--nb-r-sm);
  color: var(--nb-ink);
  font: var(--nb-type-control);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition:
    color var(--nb-dur-1) var(--nb-ease),
    border-color var(--nb-dur-1) var(--nb-ease),
    background-color var(--nb-dur-1) var(--nb-ease);
}
.qg-pager-num:hover:not(.is-current) {
  border-color: var(--nb-border-strong);
  color: var(--nb-ink);
}
html.theme-light .qg-pager-num:hover:not(.is-current) {
  border-color: var(--nb-control-border);
}
.qg-pager-num:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
}
.qg-pager-num.is-current {
  background: var(--nb-selection-fill);
  border-color: var(--nb-selection-border);
  color: var(--nb-selection-ink);
  box-shadow: inset 0 -3px 0 var(--nb-selection-marker);
}
.qg-pager-num-n {
  position: relative;
  z-index: 1;
}
/* Grade-state chip colour (server-derived bucket) — cosmetic, an examiner
   mark echoed onto the pager. full/partial wear examiner red at two weights;
   zero stays neutral. The current-question chip keeps the selection fill on top. */
/* THE BUG THIS PASS EXISTS TO FIX (2026-08-29): a FULL score was painted
   in --nb-red, the lost-mark colour, so a student who aced a question saw
   the same red as one who scored nothing. Now the chip speaks the card's
   own verdict vocabulary: full marks IS an earned mark and takes
   --nb-success; anything less IS a lost mark and takes --nb-red.
   Blue was considered and rejected — "has been graded" is not an action,
   link, focus, progress or selection, and a 40%-blue border measures
   1.8185:1 on light paper, under the 3:1 control floor. Green and red
   clear it in both themes with no theme split (5.70/8.63 and 5.33/6.01).
   partial and zero are intentionally identical: the vocabulary says "you
   lost something", not how much, and the shipped chip never said how much
   either. Zero keeps a colour rather than the old neutral, which made the
   worst result the only chip that looked unmarked. */
.qg-pager-num[data-grade-state="full"] {
  border-color: var(--nb-success);
  color: var(--nb-success);
}
.qg-pager-num[data-grade-state="partial"],
.qg-pager-num[data-grade-state="zero"] {
  border-color: var(--nb-red);
  color: var(--nb-red);
}
.qg-pager-num.is-current[data-grade-state] {
  color: var(--nb-selection-ink);
  border-color: var(--nb-selection-border);
}
/* playGrade() transient fill + confetti dots (grader/pager animation). */
/* The burst stays DECORATIVE and is deliberately not hued by bucket. It
   always fires on the chip just graded — the CURRENT chip — whose ground
   is the blue selection fill, and over that plate a green wash separates
   by 1.030:1 light / 1.011:1 dark at 9%, and only 1.048:1 / 1.024:1 at
   18%. A green celebration is invisible at every strength in both themes.
   What IS fixed: in light --nb-red-wash is #F9ECEB, an OPAQUE near-white,
   so for the burst's duration the white chip label measured 1.1522:1
   against it and vanished. Rebuilt as a translucent mix — the same
   construction --nb-red-wash already uses in dark — the label holds
   6.15:1 light / 6.92:1 dark. */
.qg-pf-fill {
  position: absolute;
  inset: 0;
  transform-origin: bottom;
  background: color-mix(in srgb, var(--nb-red) 9%, transparent);
}
.qg-pf-dot {
  position: absolute;
  width: 5px;
  height: 5px;
  border-radius: var(--nb-r-pill);
  background: var(--nb-red);
  pointer-events: none;
}
/* The 5px dots land on that same blue plate, where --nb-red separates at
   1.0552:1 light / 1.2117:1 dark — pre-existing, and invisible. On the
   current chip they take the plate's own foreground (5.68:1 / 7.28:1);
   the base above keeps --nb-red for the rare non-current case. */
.qg-pager-num.is-current[data-pf] .qg-pf-dot {
  background: var(--nb-selection-ink);
}

/* Progress meter. The inline 96px track is RETIRED at every width: the
   bar's own top edge is the progress rule now (.qg-pager::before above), and
   two meters for one fact is one too many. question-pager.js still writes the
   track's markup and its fill width, so the elements stay styled (hidden) and
   no DOM contract moves; the fill's old width transition went with them. The
   phone block below keeps its own display:none for the same element. */
.qg-pager-progress {
  display: flex;
  align-items: center;
  gap: var(--nb-s3);
}
.qg-pager-track {
  position: relative;
  display: none;
  width: 96px;
  height: 3px;
  background: var(--nb-border);
  border-radius: var(--nb-r-pill);
  overflow: hidden;
}
.qg-pager-fill {
  position: absolute;
  inset: 0 auto 0 0;
  height: 100%;
  background: var(--nb-blue);
  border-radius: var(--nb-r-pill);
}
/* "Question 3 of 18" is scaffolding around one fact, so the words recede
   a tier and the bolded position below stays the only full-ink token in
   the bar. Same tier both themes — 9.09:1 light / 8.77:1 dark.
   One size up (13px to 15px, item 10): the count is the one thing on the
   phone bar that says where you are, and it sat at fine-print size. The role
   stays --nb-type-label (weight, family, line); only the size steps, through
   the scale token. */
.qg-pager-count {
  font: var(--nb-type-label);
  font-size: var(--nb-text-base);
  color: var(--nb-ink-2);
  white-space: nowrap;
}
.qg-pager-count b {
  color: var(--nb-ink);
  font-variant-numeric: tabular-nums;
}

/* THE JUMP SHEET (mobile audit 2026-09-25, S-001). Below 768px the bar has
   no room for the number chips, so a phone could only step one question at
   a time and never saw which questions were marked. There the count becomes
   the way in: question-pager.js lays a transparent button over "Question N
   of M" (44px tall, the blue chevron beside it is the cue) that opens a
   sheet above the bar with every question as the bar's own chip, so the
   jump, the lazy load and the grade colours are the desktop chips' own.
   The sheet lists the whole set rather than the desktop's seven-chip
   window: a phone has no keys to reach the rest, and the sheet has the room.
   Paper lifted over the bar (the dropdown step, --nb-shadow-2), scrolling
   inside itself. From 768px nothing here renders and the chips are back. */
.qg-pager-jump,
.qg-pager-cue,
.qg-jump-sheet {
  display: none;
}
@media (max-width: 767px) {
  .qg-pager .qg-pager-progress {
    position: relative;
    gap: var(--nb-s1);
  }
  .qg-pager .qg-pager-jump {
    display: block;
    position: absolute;
    top: 50%;
    inset-inline: calc(-1 * var(--nb-s2));
    block-size: 44px;
    margin: 0;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: var(--nb-r-sm);
    cursor: pointer;
    transform: translateY(-50%);
    -webkit-tap-highlight-color: transparent;
  }
  .qg-pager .qg-pager-jump:focus-visible {
    outline: 2px solid var(--nb-blue);
    outline-offset: 2px;
  }
  .qg-pager .qg-pager-cue {
    display: inline-flex;
    color: var(--nb-blue);
    transition: transform var(--nb-dur-1) var(--nb-ease);
  }
  .qg-pager .qg-pager-jump[data-ui-expanded="true"] + .qg-pager-cue {
    transform: rotate(180deg);
  }
  .qg-pager .qg-jump-sheet:not([hidden]) {
    display: block;
    position: absolute;
    bottom: calc(100% + var(--nb-s2));
    inset-inline: var(--nb-s4);
    max-block-size: min(50svh, 320px);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--nb-s3);
    background: var(--nb-paper-raised);
    border: 1px solid var(--nb-border);
    border-radius: var(--nb-r-md);
    box-shadow: var(--nb-shadow-2);
  }
  .qg-jump-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
    gap: var(--nb-s1);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .qg-jump-grid .qg-pager-num {
    inline-size: 100%;
    block-size: 44px;
  }
}

/* ---------- 10b · The Grid sheet (Thumb Bar v2, Stage 3) -------- */
/* Under 861px on a page with the thumb bar, the pager's bar steps aside
   (notebook-thumbbar.css section 9) and the bar's "n of N" tab opens the
   Grid, #nb-qgrid (pages/includes/thumbbar/questions.html): every question as
   the pager's own chip, the same data-goto and grade colours, drawn by
   question-pager.js into #nb-qgrid-body under "3 of 12 marked" ("12
   questions" for a reader who cannot mark). It is a
   native sheet of nb-sheets.js, which moves it (transform only) and owns its
   history step, its scrim and the page lock; the grabber, the head and Close
   are the sheet grammar's (notebook-thumbbar.css section 4). Here is only its
   box: paper lifted onto the bar, sized to its chips up to the top bar, 640px
   and centred from 768px, a side sheet from the rail in short landscape.
   Every rule is scoped by the sheet's ID, and none reaches 861px. The chips
   are 48px squares, the demo's; every word is the ink. */
@media (max-width: 860px) {
  #nb-qgrid.nb-qgrid {
    position: fixed;
    right: 0;
    bottom: var(--nb-tb-h, 0px);
    left: 0;
    z-index: 1200;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    max-height: calc(100vh - var(--nb-tb-h, 0px) - var(--nb-chrome-h, 64px) - 4px - env(safe-area-inset-top, 0px));
    max-height: calc(100dvh - var(--nb-tb-h, 0px) - var(--nb-chrome-h, 64px) - 4px - env(safe-area-inset-top, 0px));
    margin: 0;
    padding: 0;
    overflow: hidden;
    background: var(--nb-paper-raised);
    border: 0;
    border-top: 1px solid var(--nb-border);
    border-radius: var(--nb-r-lg) var(--nb-r-lg) 0 0;
    box-shadow: var(--nb-shadow-3);
    color: var(--nb-ink);
    text-align: left;
    transform: translate3d(0, 110%, 0);
    overscroll-behavior: contain;
  }
  #nb-qgrid.nb-qgrid[hidden] {
    display: none;
  }
  #nb-qgrid .nb-qgrid-meta {
    margin-left: var(--nb-s2);
    color: var(--nb-ink);
    font: 500 var(--nb-text-sm)/1.2 var(--nb-sans);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }
  #nb-qgrid .nb-qgrid-body {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--nb-s1) var(--nb-s4) var(--nb-s6);
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  #nb-qgrid .qg-jump-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, 48px);
    justify-content: space-between;
    gap: var(--nb-s3) var(--nb-s2);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  #nb-qgrid .qg-jump-grid .qg-pager-num {
    inline-size: 48px;
    block-size: 48px;
    min-width: 0;
    height: 48px;
    padding: 0;
    border-radius: var(--nb-r-md);
    -webkit-tap-highlight-color: transparent;
  }
}
@media (min-width: 768px) and (max-width: 860px) {
  #nb-qgrid.nb-qgrid {
    right: max(0px, calc(50% - 320px));
    left: max(0px, calc(50% - 320px));
  }
}
@media (max-width: 860px) and (orientation: landscape) and (max-height: 500px) {
  #nb-qgrid.nb-qgrid {
    top: 0;
    right: auto;
    bottom: 0;
    left: var(--nb-tb-rail, 0px);
    width: min(360px, calc(100vw - var(--nb-tb-rail, 0px) - 48px));
    max-height: none;
    padding: env(safe-area-inset-top, 0px) 0 0;
    border-top: 0;
    border-right: 1px solid var(--nb-border);
    border-radius: 0 var(--nb-r-lg) var(--nb-r-lg) 0;
    transform: translate3d(-120%, 0, 0);
  }
  /* (1,2,0): the sheet grammar's grabber rule is (1,1,0) in a sheet that
     loads later. */
  #nb-qgrid.nb-qgrid .nb-sheet-grab {
    display: none;
  }
}

/* ---------- 11 · Loading dots (.esa-dots) --------------------- */
/* pager.js injects the 3-dot loader while a not-yet-loaded question is fetched.
   nb doesn't load lazy-loading.css, so the loader is defined here in ink. */
.qg-loading {
  display: none;
}
.qg-loading.is-current {
  display: block;
}
.esa-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 180px;
}
.esa-dots {
  display: inline-flex;
  gap: var(--nb-s1);
  align-items: center;
  line-height: 0;
  color: var(--nb-ink-3);
}
.esa-dots > span {
  width: var(--nb-s2);
  height: var(--nb-s2);
  border-radius: var(--nb-r-pill);
  background: currentColor;
  opacity: 0.35;
  animation: nb-qg-dots 1.1s infinite ease-in-out both;
}
.esa-dots > span:nth-child(2) { animation-delay: 0.16s; }
.esa-dots > span:nth-child(3) { animation-delay: 0.32s; }
@keyframes nb-qg-dots {
  0%, 80%, 100% { opacity: 0.3; transform: translateY(0) scale(0.8); }
  40%           { opacity: 1;   transform: translateY(-4px) scale(1); }
}

/* ---------- 12 · Empty state + prev/next --------------------- */
.nb-qg-empty {
  margin-top: var(--nb-s4);
}

/* Prev/next topic nav (hidden by pager.js when the pager is active). */
.nb-qg-pagination {
  display: flex;
  gap: var(--nb-s4);
  margin-top: var(--nb-s7);
  padding-top: var(--nb-s5);
  border-top: 1px solid var(--nb-border);
}
.nb-pagenav {
  flex: 1;
  /* min-width 0 and wrapping titles: the links carry .nb-btn (nowrap), so a
     long "Next 2.2 Size of specimens" ran 77px off a phone and the whole page
     panned sideways (mobile audit 2026-09-25). Short titles are unchanged. */
  min-width: 0;
  max-width: 50%;
  white-space: normal;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  text-align: left;
}
.nb-pagenav--next {
  align-items: flex-end;
  text-align: right;
  margin-left: auto;
}
.nb-pagenav-dir {
  font: var(--nb-type-label);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--nb-blue);
}
.nb-pagenav-title {
  font: var(--nb-type-label);
  color: var(--nb-ink-2);
  overflow-wrap: anywhere;
  text-wrap: balance;
}

/* Paywall placement */
.nb-paywall-wrap {
  display: flex;
  justify-content: center;
  margin-top: var(--nb-s8);
}

/* ---------- 13 · Mobile --------------------------------------- */
@media (max-width: 720px) {
  .tq-paper {
    --tq-pad: var(--nb-s5);
    padding: var(--nb-s5) var(--tq-pad) var(--nb-s5);
  }
  /* Narrow paper wraps the question to more lines + the answer form is taller,
     so the gradable floor is a shade higher than desktop (gradable cards idle
     ~411-462px here). Keeps the pager put on mobile too.

     This one is NOT re-derived alongside the desktop floors: the section 4
     question band only reaches the one-line case here, because the marks chip
     takes its own grid row below and the two rows together already outgrow the
     band. So the floor is still the leveller at this width, and it still is
     one -- re-measured with the band in, the tallest <=3-line card is 469.1px
     across 390-719, which 474 clears by 4.9px. */
  .qg--gradable .tq-paper {
    min-height: 474px;
  }
  .tq-q-head {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--nb-s3);
  }
  /* Marks drop under the number+question on narrow paper. */
  .tq-q-marks {
    grid-column: 1 / -1;
    justify-self: start;
  }
  /* The stack keeps each button at its own width, centred: what always
     rendered here, because the old (0,1,0) `align-items: stretch` lost to the
     (0,2,0) base rule's `center`. Written as it renders, and at (0,2,0), so
     the declared intent and the render cannot drift apart again (S-032). */
  .nb-notes-header .nb-qg-actions {
    flex-direction: column;
    align-items: center;
  }
  /* The back link wraps instead of pushing the page sideways (item 9 refix
     sweep). .nb-btn is nowrap, and "← Physics C: Electricity and Magnetism
     resources" is 346px of text: at 360 the page scrolled 6px sideways and at
     320 47px, with the whole layout viewport widened to match. A long subject
     name now takes a second, balanced line inside the same button. */
  .nb-qg-back {
    white-space: normal;
    text-wrap: balance;
    text-align: center;
  }
  /* Whole chapter wraps the same way (mobile audit round 2, S-R1-2 and
     S-G5). It kept .nb-btn's nowrap, so at 160 CSS px (a 320px phone at 200%
     zoom) "Whole chapter" was 145px in a 128px column: the page grew to
     177px, Chromium widened its layout viewport to match, and the fixed
     question bar drew Next off the screen. max-inline-size keeps the button
     inside the column; at default text it is one line, as before. */
  .nb-qg-whole {
    margin-left: 0;
    white-space: normal;
    text-wrap: balance;
    text-align: center;
    max-inline-size: 100%;
  }
  /* Anchored bar on a phone: ONE row, not three. Left to wrap, the bar stood
     133.6px tall — 15.8% of an 844px viewport, permanently — and sliced the
     question text. Vertical space is scarcest exactly here, so the number
     chips and the progress track stand down and the bar keeps the two controls
     that matter plus the count: Previous | Question N of M | Next. Measured
     53.0px. Jumping to an arbitrary question is the count's job here: it
     opens the jump sheet (section 10, S-001), whose chips carry the grade
     colours; the coarse-pointer 44px minimums in §14 are untouched. */
  .qg-pager {
    flex-wrap: nowrap;
    justify-content: space-between;
    gap: var(--nb-s3);
    padding-inline: var(--nb-s4);
  }
  .qg-pager-center {
    order: 0;
    width: auto;
  }
  .qg-pager-nums,
  .qg-pager-track {
    display: none;
  }
  .tq-answer-foot {
    align-items: flex-start;
  }
  /* ⚑ THE BAR STAYS ONE ROW UNTIL THE ROW CANNOT HOLD IT (mobile audit
     2026-09-25, S-G5). With large text or page zoom the count outgrew the
     room between the buttons: at 390px and 200% zoom it lay over Previous,
     so a tap on Previous opened the jump sheet, and Chromium drew Next off
     the screen. The bar is a size container, so the rules below read its own
     width; the parts that scale with the text are in rem and the fixed ones
     in px, so the limits follow the reader's text size and zoom. At default
     text no phone reaches them and the bar is the one row described above.
     Past them, first the labels stand down (481 to 720px; below that they
     already have), then the count takes a row of its own above Previous |
     Next. The rule above keeps nowrap as the resting intent; the wrap here
     only lets that full-width count row break the line. question-pager.js
     measures the bar into --qg-pager-h, so the room kept under the sheet
     follows the taller bar. */
  .qg-pager {
    container-type: inline-size;
    flex-wrap: wrap;
    row-gap: var(--nb-s1);
  }
  /* Two 48px arrows, two 12px gaps and the 14px cue with its 4px gap make
     138px. The widest count is 7.66rem up to 99 questions ("Question 10 of
     10": every figure is the same width) and 8.78rem for a three-digit set,
     which question-pager.js marks with .qg-pager--long-count. */
  @container (max-width: calc(138px + 7.7rem)) {
    .qg-pager-center {
      order: -1;
      flex: 1 0 100%;
    }
  }
  @container (max-width: calc(138px + 8.8rem)) {
    .qg-pager--long-count .qg-pager-center {
      order: -1;
      flex: 1 0 100%;
    }
  }
  /* ⚑ THE SWITCHER STACKS WHEN ITS WORDS CANNOT SHARE A ROW (mobile audit
     2026-09-25, S-G7). A tab cannot shrink below its longest word, and the
     strip was one nowrap row, so with large text or page zoom "Multiple
     Choice Questions" ran past the screen edge (at 320px and 2x text the tab
     ended at 372px; at 560px the strip was 658px wide). The strip now keeps
     to the column and the tabs wrap: side by side while both fit, one
     full-width row each when they do not. At default text nothing moves (the
     fit-content strip leaves no free space above 480px, and below it the
     two-up split still fits). tab-indicator.js (v3) follows the active tab
     onto its own row. notebook-mcq.css carries the same rule, because the two
     pages share this control. */
  .questions-mode-tabs {
    max-width: 100%;
    flex-wrap: wrap;
  }
  /* A tab is a flex box here so its label sits in the middle of the tab
     (S-R1-4). The row stretches both tabs to the height of the two-line
     "Multiple Choice Questions", and as a plain block the one-line "Topic
     Questions" kept to its top padding: 7px above it and 26px below. The
     tab's own box does not move, so tab-indicator.js reads the same rect. */
  .questions-mode-tab {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1 1 auto;
    white-space: normal;
    text-align: center;
  }
}

/* The labelled bar between 481 and 720px (S-G5, above): once Previous (58px +
   3.77rem), Next (58px + 6.05rem), two 12px gaps and the count (18px + 7.66rem,
   or 8.78rem for a three-digit set) no longer fit, the labels stand down
   exactly as they do below 480px and the bar stays one row of arrows and the
   count. */
@media (min-width: 481px) and (max-width: 720px) {
  @container (max-width: calc(158px + 17.55rem)) {
    .qg-pager-prev > span,
    .qg-pager-next > span {
      display: none;
    }
    .qg-pager .qg-pager-prev,
    .qg-pager .qg-pager-next {
      min-inline-size: 48px;
      min-block-size: 48px;
      padding-inline: var(--nb-s3);
    }
  }
  @container (max-width: calc(158px + 18.65rem)) {
    .qg-pager--long-count .qg-pager-prev > span,
    .qg-pager--long-count .qg-pager-next > span {
      display: none;
    }
    .qg-pager--long-count .qg-pager-prev,
    .qg-pager--long-count .qg-pager-next {
      min-inline-size: 48px;
      min-block-size: 48px;
      padding-inline: var(--nb-s3);
    }
  }
}

/* Small phones: the labels stand down, the arrows carry the buttons. With one
   row and three nowrap children, "Question N of M" ran INTO both buttons below
   ~400px — measured overlaps of 6.8px at 390, 21.8px at 360 and 37.2px at 320,
   with no page overflow to make it visible as breakage. The arrows are
   unambiguous and question-pager.js already gives each button its own
   aria-label ("Previous question" / "Next question"), so the accessible name
   survives the labels going; the count is the one thing a reader cannot infer,
   so that is what stays. .qg-load-retry writes its label as a bare text node,
   not a span, so the retry button keeps its word. */
@media (max-width: 480px) {
  /* The switcher stops being an inline pill and becomes a two-up control that
     shares the column. "Multiple Choice Questions" is a nowrap 369px strip —
     wider than the study column below 420px, so on a phone it poked out past
     the sheet it labels and pushed the document to 389px inside a 320px and a
     360px viewport (both real handsets). Letting the two tabs split the row and
     the longer label wrap inside its own tab keeps the control inside the
     measure at every width. The strip wraps (the 720px block above, S-G7):
     where the two longest words cannot share the row the tabs stack, and
     tab-indicator.js moves the indicator onto the active one. */
  .questions-mode-tabs {
    display: flex;
    /* full width again: below 480 the two tabs split the row, so the
       strip IS the measure rather than a pill sitting inside it. */
    width: auto;
  }
  .questions-mode-tab {
    flex: 1 1 0;
    white-space: normal;
    text-align: center;
  }
  /* The title is left-aligned at this width (notebook-page.css section 10),
     so the stacked header buttons line up under it (S-031b). */
  .nb-notes-header .nb-qg-actions {
    align-items: flex-start;
  }
  .qg-pager-prev > span,
  .qg-pager-next > span {
    display: none;
  }
  /* Arrow-only on a small phone, so the two controls become square 48px
     targets, one step over the 44px floor: they are the only way to move
     between questions here (item 10). (0,2,0) so the coarse-pointer 44px
     floor at the end of the sheet, which is (0,1,0) and later, cannot
     undercut it. */
  .qg-pager .qg-pager-prev,
  .qg-pager .qg-pager-next {
    min-inline-size: 48px;
    min-block-size: 48px;
    padding-inline: var(--nb-s3);
  }
  /* The verdict track is fixed, so on a small phone it takes its 100px out of
     the mark-scheme text rather than out of slack. Measured at a 320px viewport:
     the label track fell to 114px, about 18px narrower than the old flex layout
     gave it, because "Not mentioned" only needs ~82px at --nb-text-sm. Stepping
     the verdict to --nb-text-xs brings the string to ~76px, so 5.5rem still
     clears it and the label gets the difference back. Both edges of the column
     stay straight, which is the whole point of the track. */
  .tq-kp {
    grid-template-columns: minmax(0, 1fr) 5.5rem;
  }
  .tq-kp-status {
    font-size: var(--nb-text-xs);
  }
}

/* Below 360px (a 320px phone) each tab has 133px, and at the base 16px of
   inline padding "Multiple Choice Questions" broke onto three lines and the
   tabs stood 70px tall (mobile audit 2026-09-25, S-035). 8px keeps the long
   label on the two lines it has from 360px up and the tabs at 52px; the
   sliding indicator still reads the tab's own box. notebook-mcq.css carries
   the same rule, because the two pages share this control. */
@media (max-width: 359px) {
  .questions-mode-tab {
    padding-inline: var(--nb-s2);
  }
}

/* ---------- 14 · Reduced motion — duration-only guard --------- */
@media (prefers-reduced-motion: reduce) {
  .esa-dots > span { animation: none; opacity: 0.55; }
}

/* Confirmed high-frequency study controls keep their compact desktop rhythm,
   but gain a reliable finger-sized hit area on touch-primary devices.

   ⚑ DO NOT GROUP THESE SELECTORS. They repeat `min-block-size: 44px` four —
   now six — times, and collapsing them into one comma-separated rule is the
   obvious tidy-up. It breaks the build. pages/tests_notebook.py:3230
   (test_question_and_mcq_controls_gain_coarse_pointer_height) asks its helper at
   :3217 for each control's rule body, and that helper searches this block with

       (?:^|\})\s*<selector>\s*\{(?P<body>[^}]*)\}

   which only matches a selector standing ALONE after `}` or a line start. A
   grouped `.tq-reveal-btn, .qg-pager-btn { … }` returns no match and fails on
   assertIsNotNone. One rule per selector, always.

   .tq-reveal-btn, .tq-mark-btn and .tq-grade-reset now also carry the 44px
   floor in their base rules above, so on a coarse pointer these three are
   redundant — they stay because the test above requires .tq-reveal-btn to be
   owned by a rule in THIS block, and because keeping the set complete is
   cheaper than explaining which three are load-bearing. */
@media (pointer: coarse) {
  /* The 44px floor makes the tab taller than its one line, so the label is
     centred in it (S-R1-4, the touch half): on a landscape phone and a tablet
     it sat 7px from the top and 18px from the bottom. The tab's box is the
     same, so tab-indicator.js reads the same rect. */
  .questions-mode-tab {
    min-block-size: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .tq-reveal-btn {
    min-block-size: 44px;
  }
  .tq-mark-btn {
    min-block-size: 44px;
  }
  .tq-grade-reset {
    min-inline-size: 44px;
    min-block-size: 44px;
  }
  .qg-pager-btn {
    min-block-size: 44px;
  }
  .qg-pager-num {
    min-inline-size: 44px;
    min-block-size: 44px;
  }
}
