/* ⛔ EVERY :hover HERE SITS INSIDE @media (hover: hover) (owner, 2026-10-06). A touch screen keeps
   :hover on whatever is under the finger after a tap — tapping "Show the options" left the first
   option, which slid into its place, glowing as if chosen (iPhone PWA). Mouse devices are unchanged.
   A NEW hover rule goes inside such a block too; test_quiz_engine.js fails on a bare one. */
/* quiz.css — the four quizzes. QUIZ_PROJECT.md §9.
   ⚠ THE COMPONENT RULES BELOW ARE COPIED VERBATIM FROM live/quiz-mockup.html, which the owner
   approved on 2026-09-19 ("this looks fucking excellent … lock this in"). Do not restyle them
   without a new approval — the mockup is the reference, not a sketch.
   ⛔ The card-strip rules (.topic-quiz) are NOT here: they belong to topics-page.css and ship in
   Phase 8. This file is only what the quiz overlay itself needs.
   ⚠ The site has no dark mode (0 prefers-color-scheme in topics-page.css), so none here either. */

/* The one new token family this feature introduces. Declared here as a fallback so the overlay
   renders correctly on a page that has not loaded topics-page.css. */
:root {
  --quiz-ink: #1F5D3A;
  --quiz-bg: #F1F7F3;
  --quiz-line: rgba(31, 93, 58, 0.18);
}

/* ── page shell ──────────────────────────────────────────────────────────────────────────
   ⭐⭐ THE QUIZ IS A VIEW ON THE TOPIC PAGE, NOT AN OVERLAY (owner, 2026-09-21).
   It was `position: fixed; inset: 0` with a scrim and its own scroll container. Everything that
   made it read as a page on desktop was a disguise fighting that shape, and sizing a
   self-scrolling box against the viewport is what put "My results" below the fold on iOS Safari.
   ⛔⛔ DO NOT REINTRODUCE `position: fixed`, A SCRIM, OR AN INNER SCROLL CONTAINER. The document
   is the scroller now. The old comment here said a quiz is "a MODE, not a section of the topic
   page" and that is still true — but the mode is achieved by HIDING the topic content
   (`body.tq-mode`, below), not by floating a box over it. Hiding is what makes the nav usable,
   the a11y claim honest, and the viewport arithmetic unnecessary. */
.tq-page {
  /* normal flow, directly under the sticky site nav */
  width: 100%;
  padding: 14px 12px;
  box-sizing: border-box;
}
.tq-page[hidden] { display: none; }

/* ⭐⭐ QUIZ MODE — HIDE THE TOPIC PAGE, BY RULE, NEVER BY AN ELEMENT LIST.
   ⚠⚠ A LIST WOULD ROT AND THE ROT WOULD BE INVISIBLE. In the SHIPPED HTML a topic page has no
   <main> wrapper — body's children are the nav placeholder, the eyebrow, the h1, #player-root,
   #sentence-list, #tq-entry and nav.topic-nav — and player.js appends TEN more body-level
   elements at runtime (the floating mini transport, the dyn select bar, toasts, download
   overlays), while footer.js injects a footer that is in no page's source at all.
   ⚠⚠⚠ AND THE STATIC HTML IS NOT THE RUNTIME DOM — measured in a browser, 2026-09-21, and it is
   the reason to distrust reading the source here. By the time a quiz can be opened, player.js has
   WRAPPED the topic content in `div.page-wrap`, so #sentence-list and #player-root are no longer
   body children at all. A hand-written list taken from the page source would have named exactly
   those, matched NOTHING, and hidden nothing — with the quiz rendering on top of a fully visible
   topic page and no error anywhere. The blanket rule is immune to that by construction: it hides
   whatever the direct children turn out to be.
   🎯 THE MINI TRANSPORT IS THE ONE THAT WOULD HAVE BITTEN, and it is the likely case rather than
   an exotic one: it is `position: fixed; top: 54px; z-index: 60`, it latches on once the learner
   plays the topic audio, and it shows whenever #player-root is out of view. display:none gives a
   zero rect, so the IntersectionObserver reads "not in view" and the bar slides in OVER the quiz.
   The z-index:900 overlay used to cover it; nothing covers it now. This rule is what stops it.
   ⚠ .te-consent is exempt: it is a UK GDPR obligation and must stay reachable. It is
   position:fixed at z-index 9999, so it already sat above the old overlay — no change either way.
   ⚠ Scripts are children too; display:none on a <script> is inert. */
/* ⛔⛔ AND IT HIDES ANYTHING ELSE A SCRIPT APPENDS TO <body> WHILE A QUIZ IS OPEN (W73, measured
   2026-10-03). It hid the Builder's dragged tile for twelve days (fixed: the flyer now lives in the
   sheet), and it hides two messages the audio gate can open IN quiz mode — the gate fires from
   openMenu(), before the settings screen: player.js premiumInfoSheet() (the app, a non-subscriber
   on a premium unit) and showLicenceOverlay() (a subscriber offline for more than 50 days). Both
   append to body, so a tap on the quiz did nothing visible. They are exempt by id below.
   ⚠ A new body-level message that can open during a quiz must be added here, or rendered inside
   #tq-page. test_quiz_engine.js pins both exemptions. */
body.tq-mode > *:not(nav.site-nav):not(#tq-page):not(.te-consent):not(#te-premium-sheet):not(#te-licence-overlay):not(#te-mini) { display: none !important; }
/* The topic's mini player stays reachable in a quiz (owner, 2026-10-06): the quiz shares the topic's
   URL, so audio started there keeps playing and this is the only way to pause it. It is fixed under
   the nav, so the quiz is pushed below it (player.js updateMiniVisibility sets --te-mini-h). */
html.te-mini-on body.tq-mode #tq-page { padding-top: var(--te-mini-h, 0px); }

/* ⛔⛔ PULL-TO-REFRESH MUST NOT BE ABLE TO DESTROY A RUN (owner, 2026-09-21, agreed explicitly).
   The overlay was its own scroll container with `overscroll-behavior: contain` and the document
   was locked at `overflow: hidden`. The quiz now scrolls the document, and the site sets
   overscroll-behavior NOWHERE else — so at the top of a question a slightly over-enthusiastic
   downward swipe on Android Chrome would reload the page and take the run with it, silently,
   with no warning possible: a reload is the one exit class a click handler cannot see.
   ✅ This kills the gesture for the duration of the quiz and restores it on exit. It was chosen
   over `beforeunload`, which cannot show our honest wording (browsers have forced a generic
   "Changes you made may not be saved" since 2016) and which a deliberate reload deserves anyway.
   ⚠ Needed on BOTH: the propagating element differs between engines. */
html.tq-mode, body.tq-mode { overscroll-behavior-y: contain; }
/* ⛔ SCROLL ANCHORING OFF INSIDE THE QUIZ. The browser re-pins the viewport to whatever element
   was in view when content is inserted above it - which is exactly what the quiz does on every
   render, and what put the learner part-way down the menu after entering from the foot of a long
   topic page. There is nothing here worth anchoring: every screen is meant to start at the top.
   ⚠⚠ THIS IS THE BLINK HALF OF THAT FIX AND IT DOES NOTHING ON iOS. Safari has never implemented
   scroll anchoring, so on the iPhone PWA - which is where the owner actually saw the fault, and
   not on the Android app - the cause is WebKit clamping the scroll offset when the document
   collapses and restoring toward it as the sheet paints. quiz.js's scrollToTop() carries that half
   with two deferred passes. ⛔ Neither half covers both browsers; removing either reopens the bug
   on one of them. */
html.tq-mode, body.tq-mode { overflow-anchor: none; }

/* ⛔⛔ THE SURFACE COLOUR MUST COVER THE WHOLE VIEWPORT, NOT JUST THE SHEET — CAUGHT IN A BROWSER,
   NOT IN THE DIFF (2026-09-21).
   The body is #FAFAF8, a warm off-white; the sheet is `--surface`, pure #FFFFFF. The overlay used
   to be `position: fixed; inset: 0` WITH that surface colour, so the entire screen was white and
   the sheet had no visible edge. Drop the overlay and the sheet becomes an 820px pure-white panel
   sitting on an off-white page — two faint vertical seams down the screen, exactly the "card on a
   backdrop" read the desktop pass spent two rounds removing.
   ⚠ ON THE BODY, NOT ON .tq-page: .tq-page is only as tall as its content, so on a tall window
   with a short screen (the picker) the off-white would come back below it. A body background
   propagates to the canvas and covers the viewport however short the content is.
   ⚠ This is a COSMETIC PARITY rule — its whole job is that the migration changed nothing visible.
   Removing it does not break a layout, it just quietly reintroduces the seams. */
body.tq-mode { background: var(--surface, #fff); }
.tq-sheet {
  background: var(--surface, #fff);
  border: 0.5px solid var(--border, rgba(0,0,0,.09));
  border-radius: var(--radius-lg, 14px);
  padding: 16px;
  width: 100%; max-width: 480px;
  /* ⚠ `margin: auto` centred the sheet inside a flex overlay that no longer exists. In normal
     flow the vertical auto resolves to 0, so this is `0 auto` written explicitly. */
  margin: 0 auto;
  font-family: var(--font-ui, 'Inter', system-ui, sans-serif);
  color: var(--text-primary, #1A1A1A);
  /* ⛔⛔ OS TEXT SCALING — see the block at the foot of this file. A long English word
     ("comprehension") is wider than its box once the system font size passes ~150%, and with
     the default `overflow-wrap: normal` it does not break: it simply runs out of the sheet and
     takes the whole document sideways with it. `break-word` inherits to every descendant, so
     one declaration covers the headers, the options, the exclusion rows and the results table.
     ⚠ NOT `anywhere`: that also shrinks min-content, which would let a flex/grid track collapse
     under text it should be sizing to. The min-content cases are fixed with `min-width: 0` at
     the item that needs it, which is the §9 lesson in TEXT_SCALING.md. */
  overflow-wrap: break-word;
}
.tq-sheet h3 { margin: 0 0 3px; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.tq-sheet .sub { margin: 0 0 14px; font-size: 12.5px; color: var(--text-tertiary, #9A9A9A); }

/* ⭐⭐ A TITLE AND A SUBTITLE, TOLD APART (owner, 2026-09-20: "all text is the exact same
   color, and same background too, could distinguish between the title and subtitle ... right now
   it looks like a 4 year old did it with a crayon").
   The first attempt put both lines in the same gold on the same tint, which is two labels
   stacked rather than a heading. Now they differ on THREE axes at once — colour, weight and
   ground — so the hierarchy survives whichever one a given screen or eye picks up:
     · "Test yourself" is the small tinted eyebrow, INSIDE the coloured band
     · the unit name is larger, darker, and sits on the sheet's own white below it
   ⚠ IT ALSO HAD TO GET SMALLER. Both lines were wrapping to two — "Test yourself" at 17px bold
   in a panel with 15px of padding could not fit a phone, and the unit names are long now that
   they carry a colon ("Food culture: Regional food & etiquette"). The eyebrow is one short
   phrase so it can be small; the unit name gets the room that frees up. */
.tq-head.hero {
  --tqh-ink: var(--accent, #261B65);
  --tqh-bg: var(--accent-light, #ECEBFA);
  margin: 0 0 10px;
}
/* ⛔⛔ THE QUIZ ARM HAS NO PREMIUM SKIN (A6/A9-A13, owner 2026-09-22: "quiz should also
   adopt navy theme", and premium topics join the one accent). Every `.tq-sheet.premium` /
   `.tq-entry.premium` rule below now restates the BASE pair rather than a gold one. They are
   kept, not deleted, because the class is still applied by quiz.js and a future tier skin
   would be written here - and because a deleted rule is invisible, whereas one that says
   "same as base, deliberately" is not. Retired pair: text gold #B29234 on #FBF5DC. */
.tq-sheet.premium .tq-head.hero { --tqh-ink: var(--accent, #261B65); --tqh-bg: var(--accent-light, #ECEBFA); }
/* ⚠ The gap under the eyebrow is 12px, not 7 (owner, 2026-09-20: "on quiz menu the topic
   title is too close to the 'test yourself' it is cramped"). The pill and the unit name are
   different enough in size and weight that a tight gap reads as one crowded block rather than
   two lines of a hierarchy. */
.tq-head.hero h3 {
  display: inline-block; margin: 0 0 12px;
  background: var(--tqh-bg); color: var(--tqh-ink);
  border-radius: 999px; padding: 4px 11px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  line-height: 1.2; white-space: nowrap;
}
.tq-head.hero .sub {
  margin: 0; font-size: 19px; font-weight: 600; line-height: 1.2;
  letter-spacing: -0.015em; color: var(--text-primary, #1A1A1A);
}

/* ⭐ THE QUIZ'S OWN GLYPH BESIDE ITS NAME on its menu and its results (owner, 2026-09-20).
   Deliberately the SAME chip as the picker row — 34px, 10px radius, the accent tint, gold on a
   premium unit — so the menu reads as the row the learner just tapped having opened, rather than
   as an unrelated screen that happens to share a word. */
/* ⚠ THE NON-HERO HEADER HAD NO BOTTOM MARGIN AT ALL (owner, 2026-09-20: "on all four quizzes
   the 'How many questions' line is too close to the title ... its not actually the words in the
   title its too close to - its the ICON - it is practically touching it").
   His diagnosis is the whole fix. Spacing under this header came from the h3's own 3px margin,
   which was survivable while the header WAS the h3 — and the 34px icon chip is taller than the
   text, so it overhangs the h3's box and closed the gap to 4px. Measured, then set here rather
   than on the h3, because the header is what the next block must clear.
   ⚠ It applies to the in-quiz Options menu too, which shares this header and had the same 4px. */
.tq-head:not(.hero) { margin: 0 0 18px; }
.tq-head.has-ic { display: flex; align-items: center; gap: 11px; }
.tq-head.has-ic .tq-head-ic {
  width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent-light, #ECEBFA); color: var(--accent, #261B65);
}
/* ⛔⛔ SIZE THE SVG. Without this the chip rendered as an EMPTY TINTED BOX on iPhone (owner,
   2026-09-20: "the icons dont load on iphone within each individual quiz - they are empty
   blueish boxes ... works on quiz menu and on view from topic pages").
   ⚠⚠ IT WAS NEVER A LOADING PROBLEM — the markup is an inline <svg>, so there is nothing to
   load. An inline SVG with a viewBox and no CSS width/height has no intrinsic size, and the two
   engines disagree about what to do with that: Blink resolves it against the flex container and
   draws something, WebKit gives it zero and draws nothing. The two chips that WORK are the ones
   with explicit rules — `.qpick .ic svg` and `.tqe-ic svg` — which is exactly why the picker and
   the topic block were fine and only the new header was broken.
   ⚠ 17px against a 34px chip, matching .qpick .ic svg, because this chip is deliberately the
   same object as a picker row's. */
.tq-head.has-ic .tq-head-ic svg { width: 17px; height: 17px; display: block; }
.tq-sheet.premium .tq-head.has-ic .tq-head-ic { background: var(--accent-light, #ECEBFA); color: var(--accent, #261B65); }
.tq-head.has-ic .tq-head-tx { min-width: 0; }

/* ── quiz-type picker ────────────────────────────────────────────────────────────────── */
.qpick { display: flex; flex-direction: column; gap: 8px; }
.qpick button {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  background: var(--surface, #fff); border: 0.5px solid var(--border-strong, rgba(0,0,0,.16));
  border-radius: var(--radius-md, 10px); padding: 11px 13px; cursor: pointer;
  font-family: inherit; transition: border-color .15s, background .15s;
}
@media (hover: hover) {
  .qpick button:hover { border-color: var(--accent, #261B65); background: var(--accent-light, #ECEBFA); }
}
.qpick .ic {
  width: 30px; height: 30px; border-radius: 9px;
  background: var(--accent-light, #ECEBFA); color: var(--accent, #261B65);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 15px;
}
/* ⚠ The tile used to hold a single letter, so it needed no rule for its contents. The svg is
   sized here rather than on the element, because the four are one set and should not be able to
   drift apart. 17px in a 30px tile leaves the same optical padding the letters had. */
.qpick .ic svg { width: 17px; height: 17px; display: block; }
/* ⛔ THE TEXT COLUMN MUST BE ABLE TO SHRINK. It is the middle flex item between a fixed icon
   and the score, and a flex item is floored at its MIN-CONTENT width — so at large system text
   "comprehension" set the row's width and the row burst the sheet (measured +41px at 320@2.0×).
   `min-width: 0` is what lets it shrink; the `overflow-wrap` inherited from .tq-sheet is what
   then breaks the word instead of clipping it. Same pair as .tq-head-tx above, and the same
   fix TEXT_SCALING.md §9 records for .pl-box-meta. */
.qpick .tx { min-width: 0; }
.tq-sheet.premium .qpick .ic { background: var(--accent-light, #ECEBFA); color: var(--accent, #261B65); }
.qpick .nm { font-size: 14px; font-weight: 500; color: var(--text-primary, #1A1A1A); display: block; line-height: 1.25; }
.qpick .ds { font-size: 11.5px; color: var(--text-tertiary, #9A9A9A); display: block; line-height: 1.35; }
.qpick .sc { margin-left: auto; font-size: 12px; font-weight: 600; color: #1F5D3A; font-variant-numeric: tabular-nums; }
.qpick .sc.none { color: var(--text-tertiary, #9A9A9A); font-weight: 500; }

/* ── pre-quiz menu ───────────────────────────────────────────────────────────────────── */
.mgroup { margin: 0 0 17px; }
.mlab {
  font-size: 11px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-tertiary, #9A9A9A); margin: 0 0 7px;
}
.segs { display: flex; gap: 6px; flex-wrap: wrap; }
.seg {
  flex: 1; min-width: 58px; text-align: center; font-size: 13px; font-weight: 500;
  padding: 7px 4px; border-radius: var(--radius-sm, 6px);
  border: 0.5px solid var(--border-strong, rgba(0,0,0,.16)); background: var(--surface, #fff);
  cursor: pointer; font-family: inherit; color: var(--text-secondary, #5A5A5A);
}
.seg.on { background: var(--accent, #261B65); border-color: var(--accent, #261B65); color: #fff; }
.seg[disabled] { opacity: .34; cursor: default; }
.radios { display: flex; flex-direction: column; gap: 5px; }
.radio {
  display: flex; align-items: flex-start; gap: 9px; font-size: 13.5px;
  padding: 6px 8px; border-radius: var(--radius-sm, 6px); cursor: pointer;
}
@media (hover: hover) {
  .radio:hover { background: var(--accent-light, #ECEBFA); }
}
.radio input { margin: 4px 0 0; accent-color: var(--accent, #261B65); flex-shrink: 0; }
.radio .rt { line-height: 1.35; }
.radio .rd { color: var(--text-tertiary, #9A9A9A); font-size: 12px; }
.hint {
  font-size: 12px; color: var(--text-secondary, #5A5A5A);
  background: var(--accent-light, #ECEBFA); border-radius: var(--radius-sm, 6px);
  padding: 8px 10px; margin: 8px 0 0; line-height: 1.45;
}
.checkrow {
  display: flex; align-items: flex-start; gap: 9px; font-size: 13.5px;
  padding: 6px 8px; border-radius: var(--radius-sm, 6px); cursor: pointer;
}
@media (hover: hover) {
  .checkrow:hover { background: var(--accent-light, #ECEBFA); }
}
.checkrow input { margin: 4px 0 0; accent-color: var(--accent, #261B65); flex-shrink: 0; }
.exline {
  display: flex; align-items: center; justify-content: space-between; font-size: 13px;
  border: 0.5px solid var(--border-strong, rgba(0,0,0,.16)); border-radius: var(--radius-sm, 6px);
  padding: 9px 11px; color: var(--text-secondary, #5A5A5A); cursor: pointer;
  background: var(--surface, #fff); width: 100%; font-family: inherit; text-align: left;
}
@media (hover: hover) {
  .exline:hover { border-color: var(--accent, #261B65); }
}
.exline b { color: var(--text-primary, #1A1A1A); font-weight: 500; }
.startbtn {
  width: 100%; background: var(--accent, #261B65); color: #fff; border: 0;
  border-radius: var(--radius-md, 10px); padding: 12px;
  font-size: 14.5px; font-weight: 600; font-family: inherit; cursor: pointer; margin-top: 4px;
}
@media (hover: hover) {
  .startbtn:hover { background: var(--accent-mid, #342682); }
}

/* Secondary actions under the start button — Return, and "use these settings everywhere". */
.tq-minor {
  display: flex; flex-direction: column; gap: 7px; margin-top: 9px;
  /* ⚠ 6px, not 9px - one of the three stacked spacings under the tiger. See the gap-s note. */
  padding-top: 6px; border-top: 0.5px solid var(--border, rgba(0,0,0,.09));
}
.tq-minor button {
  background: none; border: 0; padding: 4px; cursor: pointer; font-family: inherit;
  font-size: 12.5px; color: var(--text-secondary, #5A5A5A); text-align: left;
}
@media (hover: hover) {
  .tq-minor button:hover { color: var(--accent, #261B65); text-decoration: underline; }
}

/* ⭐ THE WAY OUT IS A CONTROL, NOT A FOOTNOTE (owner, 2026-09-20: "slightly larger/more
   prominent (not aggressively so)"). It sat in .tq-minor at 12.5px with no affordance, which is
   right for "← All four quizzes" — a sideways step inside the quiz — and wrong for the one
   button that LEAVES.
   ⚠ BOTH WAYS OUT GET THE SAME PILL (owner, 2026-09-20: "the '<- other quizzes' can we have
   that identical in style to the back to the topic message that appears just below it"). An
   earlier version argued they should DIFFER — ← for a step back inside the quiz, → for the one
   that leaves — and so gave "Other quizzes" no affordance at all. Two adjacent controls doing
   the same KIND of thing, one drawn as a button and one as a footnote, reads as an oversight;
   and the owner reverted the forward arrow too, so both are ←, leading.
   ⛔ Restrained on purpose: a bordered pill at 13.5px, not a filled accent button. It is the
   way out of the quiz, not the thing the screen is for. */
.tq-minor .tq-return,
.tq-minor .tq-back {
  align-self: flex-start; font-size: 13.5px; font-weight: 500;
  color: var(--text-primary, #1A1A1A);
  border: 0.5px solid var(--border-strong, rgba(0,0,0,.16));
  border-radius: 999px; padding: 7px 14px;
}
@media (hover: hover) {
  .tq-minor .tq-return:hover,
  .tq-minor .tq-back:hover {
    text-decoration: none; border-color: var(--accent, #261B65);
    background: var(--accent-light, #ECEBFA); color: var(--accent, #261B65);
  }
}
@media (hover: hover) {
  .tq-sheet.premium .tq-minor .tq-return:hover,
  .tq-sheet.premium .tq-minor .tq-back:hover {
    border-color: var(--accent, #261B65); background: var(--accent-light, #ECEBFA); color: var(--accent, #261B65);
  }
}
.tq-minor .done { color: #1F5D3A; text-decoration: none; cursor: default; }

/* ── the exclusions list ─────────────────────────────────────────────────────────────── */
.tq-exlist { display: flex; flex-direction: column; gap: 2px; max-height: 46vh; overflow-y: auto; margin-bottom: 12px; }
.tq-exitem {
  display: flex; align-items: flex-start; gap: 9px; font-size: 13px; line-height: 1.4;
  padding: 7px 8px; border-radius: var(--radius-sm, 6px); cursor: pointer;
}
@media (hover: hover) {
  .tq-exitem:hover { background: var(--accent-light, #ECEBFA); }
}
.tq-exitem input { margin: 3px 0 0; accent-color: var(--accent, #261B65); flex-shrink: 0; }
.tq-exitem .x-th { font-family: var(--font-thai, 'Sarabun', sans-serif); }
.tq-exitem .x-en { color: var(--text-tertiary, #9A9A9A); font-size: 12px; display: block; }

/* ── question screens ────────────────────────────────────────────────────────────────── */
.qbar {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 12px; color: var(--text-tertiary, #9A9A9A); margin-bottom: 10px;
}
.qprog { flex: 1; height: 3px; background: var(--border, rgba(0,0,0,.09)); border-radius: 2px; margin: 0 11px; overflow: hidden; }
.qprog i { display: block; height: 100%; background: var(--accent, #261B65); border-radius: 2px; transition: width .2s; }
.qx {
  background: none; border: 0; padding: 0 6px 0 0; cursor: pointer; line-height: 1;
  font-size: 18px; color: var(--text-tertiary, #9A9A9A); font-family: inherit;
}
@media (hover: hover) {
  .qx:hover { color: #B4342E; }
}
.qopts {
  /* ⭐ A GEAR, NOT AN ELLIPSIS, AND BIGGER (owner, 2026-09-20). The ⋯ read as decoration and was
     missed; settings live behind it, so it has to look like settings. 34px is also a real tap
     target — the old one was ~19px, under the 44px guidance and fiddly on a phone. */
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex-shrink: 0;
  color: var(--text-secondary, #5A5A5A);
  cursor: pointer; background: none; border: 0; padding: 0; line-height: 0;
  border-radius: 50%; font-family: inherit;
  transition: background .15s, color .15s;
}
.qopts svg { width: 21px; height: 21px; fill: currentColor; }
@media (hover: hover) {
  .qopts:hover { color: var(--accent, #261B65); background: var(--accent-light, #ECEBFA); }
}
.playbtn {
  display: flex; align-items: center; justify-content: center; gap: 9px; width: 100%;
  background: var(--chip-accent-light, #F6F6FD); color: var(--chip-accent, #261B65);
  border: 0.5px solid rgba(38, 27, 101, .2); border-radius: var(--radius-md, 10px); padding: 15px;
  font-size: 14px; font-weight: 500; font-family: inherit; cursor: pointer; margin-bottom: 14px;
}
.playbtn .tri {
  width: 0; height: 0; border-left: 13px solid currentColor;
  border-top: 8px solid transparent; border-bottom: 8px solid transparent;
}
/* ⚠ PAUSE, NOT STOP. Two bars, because a filled square says "stop" — and the control does not
   stop anything, it pauses and resumes. Same 13px optical height as the triangle so the button
   does not resize between states. */
.playbtn.playing .tri {
  border: 0; width: 13px; height: 13px; background: transparent; box-sizing: border-box;
  border-left: 4px solid currentColor; border-right: 4px solid currentColor;
}
.opt {
  display: block; width: 100%; text-align: left; background: var(--surface, #fff);
  border: 0.5px solid var(--border-strong, rgba(0,0,0,.16)); border-radius: var(--radius-md, 10px);
  padding: 11px 13px; font-size: 13.5px; font-family: inherit; cursor: pointer;
  margin-bottom: 7px; color: var(--text-primary, #1A1A1A); line-height: 1.4;
}
@media (hover: hover) {
  .opt:hover { border-color: var(--accent, #261B65); background: var(--accent-light, #ECEBFA); }
}
.opt.right { border-color: #1F5D3A; background: #F1F7F3; color: #1F5D3A; font-weight: 500; }
.opt.wrong { border-color: #B4342E; background: #FCF2F1; color: #B4342E; }

/* ⚠ The Check button after it has been pressed. Same box, same width, same padding as
   .nextbtn — only the colours and the word change, so nothing below it can move. The border
   replaces the fill so the height is identical to the unpressed state to the pixel. */
.nextbtn.t-verdict { cursor: default; }
.nextbtn.t-verdict.right { background: #F1F7F3; color: #1F5D3A; box-shadow: inset 0 0 0 1px #1F5D3A; }
.nextbtn.t-verdict.wrong { background: #FCF2F1; color: #B4342E; box-shadow: inset 0 0 0 1px #B4342E; }
.nextbtn.t-verdict[disabled] { opacity: 1; }
.opt.picked { border-color: var(--accent, #261B65); background: var(--accent-light, #ECEBFA); font-weight: 500; }
.opt .mk { float: right; font-weight: 600; }
.opt[disabled] { cursor: default; }
.opt .o-th { font-family: var(--font-thai, 'Sarabun', sans-serif); font-size: 17px; }

.thaibig { font-family: var(--font-thai, 'Sarabun', sans-serif); font-size: 21px; line-height: 1.55; color: var(--text-primary, #1A1A1A); margin: 0 0 2px; }
.tl { font-size: 12.5px; color: var(--text-tertiary, #9A9A9A); line-height: 1.5; margin: 0 0 10px; }
/* ⛔⛔ THE TWO SURFACES THE OWNER ASKED TO LEAVE PURPLE (2026-09-22): "i think the gloss
   chips as the light blue purple is fine as is, same for play the sentence." Everything else in
   this arm went navy in the same breath, so these two need to be IMMUNE rather than merely
   remembered - hence their own variables, which no palette change touches.
   ⚠ NOT A CONTRADICTION with A4's "the ind sentence play buttons" going navy: that is
   .sent-play-btn on a TOPIC page. This is .playbtn, the "Play the sentence" button inside a quiz
   reveal - a different control on a different screen, and he named this one by its label.
   ⚠ The topic page's own .gloss-chip needs nothing: it is built from --surface / --border /
   --text-*, so it never carried the accent and the recolour could not reach it. These are the
   quiz's reveal chips, which did. */
:root {
  /* ⚠ THE GROUND FOLLOWS THE SITE TINT NOW; only the INK stays the owner's purple. 2026-09-22:
     "incl the gloss chips in the quiz page answers, they now clash with the overriding color
     theme so please apply the tint to those as well". The pinned lavender ground existed only
     because the tint was grey, and it no longer is. */
  /* ⛔ WAS #4B41AD, the retired purple, held by owner exemption until 2026-09-22 — when he saw
     it live against the navy and reversed it: "i think quiz chips should also look like this
     new #f6f6fd and #261b65 ... also the play the thai boxes too".
     ⚠ THIS REACHES EXACTLY THREE SURFACES, and that is the point: .chip .th, .chip.is-target
     and .playbtn. The THAI BUILDER is NOT one of them — its tiles are .tile/.drop/.tray on
     --accent, and the owner asked for those to stay as they are ("the chips in the thai
     builder in the actual question that you click and drag are fine as is"). Do not merge
     the two families on the grounds that they now hold the same value. */
  --chip-accent: #261B65;
  --chip-accent-light: var(--card-tint, #F6F6FD);
}
.chips { display: flex; flex-wrap: wrap; gap: 5px; }
.chip { background: var(--chip-accent-light, #F6F6FD); border-radius: var(--radius-sm, 6px); padding: 4px 8px; font-size: 12px; line-height: 1.35; }
.chip .th { font-family: var(--font-thai, 'Sarabun', sans-serif); color: var(--chip-accent, #261B65); font-weight: 500; }
.chip .gl {
  color: var(--text-secondary, #5A5A5A); margin-left: 5px;
  /* ⚠ PER-CHARACTER FALLBACK, NOT A THAI FONT. A gloss can legitimately CONTAIN THAI SCRIPT: the
     grammar units name a discontinuous frame in both halves — ยัง = "still (ยัง…อยู่)" — 49 such
     chips as of 2026-09-20, deliberately in Thai rather than transliteration because the
     Transliteration pill hides .g-tl but never the gloss. Inter carries no Thai glyphs, so
     without this the pair name resolves in whatever face the OS supplies (Leelawadee on Windows,
     Thonburi on iOS) beside the Sarabun next to it.
     ⛔ It must NOT simply use --font-thai: Noto Sans Thai HAS Latin glyphs and would restyle the
     English gloss too. Latin stays in Inter and only Thai falls through. Same fix as player.js
     r224 / player-dyn.css; this is the quiz's copy of it. */
  font-family: var(--font-ui, 'Inter', system-ui, sans-serif), var(--font-thai, 'Sarabun', sans-serif);
}
.chip .tlx { display: block; color: var(--text-tertiary, #9A9A9A); font-size: 10.5px; }
/* ⚠ The chip the question was about. An outline rather than a different fill: the chips sit
   on the accent-light background already, so a second fill would fight it, and an outline
   survives both themes without a second colour token. */
.chip.is-target { box-shadow: inset 0 0 0 1.5px var(--chip-accent, #261B65); }

/* ⚠ The published translation, under a prompt that was reworded to line up with the chips.
   Secondary by design: it says what the sentence MEANS, while the line above says how it is
   BUILT. At the same weight the learner would work from whichever they happened to read first,
   which is the ambiguity this exists to remove. */
/* ⚠ 13.5px and the SECONDARY ink, not the tertiary (owner, 2026-09-20: "should be a bit
   bigger/more noticeable"). At 12.5px in --text-tertiary it read as a caption — the grey the
   rest of the site uses for things you are not meant to read — and the learner skipped the one
   line that says what the sentence MEANS.
   ⛔ STILL CLEARLY SECONDARY, and the gap to the prompt is what carries that: 15.5px medium
   against 13.5px italic. It must not close further. If the two lines read as equal the learner
   works from whichever they happen to see first, which is the ambiguity §5.4 exists to remove —
   one line says how the sentence is BUILT, the other what it MEANS. */
.enq-nat {
  margin: -6px 0 13px; font-size: 13.5px; line-height: 1.45;
  color: var(--text-secondary, #5A5A5A); font-style: italic;
}

/* ══ dragging in the Thai Builder ══════════════════════════════════════════════
   ⛔ touch-action on the MOVABLE tiles only. Without it the browser claims the gesture for
   scrolling and the drag never starts on a phone; with it on everything, the sheet could not be
   scrolled at all. The locked head-start tiles are deliberately left scrollable — they cannot be
   dragged, so a press on one should still scroll the page. */
.tile[data-t], .tile[data-p] { touch-action: none; }
.tq-lifted { opacity: .35; }

/* ══ My results ══════════════════════════════════════════════════════════════════
   ⚠ COLLAPSED BY DEFAULT. The picker's job is to start a quiz; a table of every unit's history
   above the fold would bury the four buttons the screen exists for — and on a long history it
   would be the majority of the page. One tap, and it remembers nothing, because reopening the
   picker means someone is choosing a quiz again. */
.tq-results { border-top: 0.5px solid var(--border, rgba(0,0,0,.09)); margin-top: 8px; padding-top: 2px; }
/* ⚠ THE CARET SITS NEXT TO THE LABEL, not at the far edge (owner, 2026-09-20: "the little
   triangle that flips when you click 'my results' can we have it directly next to my results,
   instead of on the other side of the page"). It was `justify-content: space-between`, which
   flung it to the right margin — fine on a table header where the right edge means something,
   and wrong here, where it is the disclosure marker FOR those two words and reads as unrelated
   at arm's length from them.
   ⚠ The button stays full width so the whole row is still the tap target; only the contents
   pack left. */
.tq-res-h {
  width: 100%; display: flex; align-items: center; justify-content: flex-start; gap: 6px;
  background: none; border: 0; padding: 11px 2px; cursor: pointer; font-family: inherit;
  font-size: 13px; font-weight: 600; color: var(--text-secondary, #5A5A5A);
}
.tq-res-cv { transition: transform .15s ease; }
.tq-res-h.open .tq-res-cv { transform: rotate(180deg); }
.tq-res-u { margin-bottom: 14px; }
.tq-res-sep { margin-top: 16px; }
.tq-res-t { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.tq-res-t thead th {
  font-weight: 500; color: var(--text-tertiary, #9A9A9A); text-align: right;
  padding: 0 0 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
}
/* ⛔⛔ THE UNIT NAME WRAPS — IT USED TO BE `white-space: nowrap` AND THAT IS WHAT MADE THIS
   TABLE THE WORST SURFACE IN THE ARM UNDER OS TEXT SCALING (+302px at 320@2.0×, measured
   2026-09-22). Six columns is already the most this sheet can hold; a name column that refuses
   to wrap takes whatever it needs and the five numeric columns are pushed straight off the
   screen — and a <table> has no min-width:0 escape, so nothing downstream can recover it.
   ⚠ IT IS A NO-OP AT DEFAULT SIZE, which is why it is safe: the names are "Vocab Trainer",
   "Listening", "Thai Builder", "Speak Thai" and none of them wraps in the width it is given.
   Verified byte-identical at 390@1× before shipping. */
.tq-res-t tbody th {
  font-weight: 500; text-align: left; color: var(--text-primary, #1A1A1A);
  padding: 5px calc(8px * var(--te-ui, 1)) 5px 0;
  /* ⛔⛔ `anywhere`, so the TABLE'S WIDTH STOPS DEPENDING ON THE DATA. A <table> has no
     `min-width: 0` escape: auto layout floors every column at its min-content, and for this one
     that is a whole quiz NAME. "Listening" alone was 63px of the 273px a 320px phone has at
     2.0×, and the five numeric columns are sized by their header words and cannot give it back.
     ⚠ THE POINT IS THE FUTURE, not today's four names. 113 units are being wired into this arm
     and a fifth quiz would bring its own name; with `anywhere` the table's min-content is one
     character and no name that arrives later can reopen this. In practice it never breaks — auto
     layout still hands the column ~67px, which fits "Listening" on one line. */
  overflow-wrap: anywhere;
}
.tq-res-t td { text-align: right; padding: 5px 0 5px 10px; color: var(--text-secondary, #5A5A5A); }
/* ⚠ Tone, not colour alone — these read as counts either way; the colour is a hint. */
.tq-res-t .ok   { color: #2F7D62; }
.tq-res-t .no   { color: #B23A3A; }
/* ⚠ AVG sits between `wrong` and `best` and must not compete with BEST for emphasis. BEST is the
   headline (600 weight, primary ink); AVG is secondary ink at normal weight — present, readable,
   and clearly the quieter of the two percentages. Giving both the bold treatment made the row
   read as two headlines and it was no longer obvious which number the learner was meant to take
   away. ⚠ Deliberately NOT tinted green/red like .ok/.no: those are COUNTS whose colour is a
   hint about right vs wrong, and a percentage that is sometimes green and sometimes red would
   imply a pass mark this site does not set. */
.tq-res-t .avg  { color: var(--text-secondary, #5A5A5A); font-variant-numeric: tabular-nums; }
/* ⚠ tabular-nums on both percentage columns so 9% / 61% / 100% stay right-aligned to the same
   column edge down a table of several units. */
.tq-res-t .best { font-weight: 600; color: var(--text-primary, #1A1A1A); font-variant-numeric: tabular-nums; }
/* ⚠ Six columns now, so the numeric cells lose some of their breathing room.
   ⛔⛔ AND THE GUTTER SHRINKS AS THE TYPE IS CAPPED — `* var(--te-ui, 1)`, which is exactly 1 at
   default size and so changes nothing until the system text is raised. This is the LAST 7px and
   it was measured, not guessed: at 320@2.0× the six columns come to 280px inside 273px, and each
   numeric column is sized by its HEADER WORD ("wrong" is the widest at 52px), so there is no
   content to trim. Five gutters at 8px is 40px; at the capped 4.6px it is 23px, which clears it
   with room.
   ⚠ This is the one place in the codebase where a px gutter is deliberately made SMALLER under
   inflation rather than merely held constant (TEXT_SCALING.md §1 covers the holding case). It is
   justified because this table is the arm's single pinch point and the alternative is dropping a
   column the learner asked to see. Do not generalise it. */
.tq-res-t td { padding-left: calc(8px * var(--te-ui, 1)); }
.tq-res-t tbody tr + tr th,
.tq-res-t tbody tr + tr td { border-top: 0.5px solid var(--border, rgba(0,0,0,.07)); }
/* ⚠ The empty slot a taken chip leaves behind. visibility, NOT display or opacity: display
   would collapse the box and bring the sliding back, and opacity would leave it clickable and
   readable as a faded chip the learner could try to take twice. */
.tile.slot { visibility: hidden; }
/* ⚠ `.tile.tq-flyer`, not `.tq-flyer` (W73, measured 2026-10-03): the plain `.tile` rule further
   down has the same specificity and comes later, so its faint 1px shadow won and the "lifted"
   shadow below never showed, even when the flyer was visible (v568). */
.tile.tq-flyer {
  opacity: .95; box-shadow: 0 6px 18px rgba(0,0,0,.22);
  transform-origin: top left; will-change: transform;
}
/* the gap that opens where the tile would land */
.tq-ghost {
  background: transparent; border: 1.5px dashed var(--accent, #261B65);
  border-radius: var(--radius-sm, 6px); box-sizing: border-box;
}
.chip.is-target .gl { color: var(--text-primary, #1A1A1A); }
/* the breakdown sits between the English and the play button, so it needs its own air */
/* ⚠ The play button needs air ABOVE it, not only below (owner, 2026-09-20: "very very close to
   the above gloss chips, but a healthy gap below it until the next button"). The chips are a
   dense block of small type and the button read as belonging to them. */
.vchips { margin: 10px 0 15px; }

/* ⛔⛔ NOTHING HIDES A TRANSLITERATION HERE ANY MORE. .tlx, .o-tl and .tl are EMITTED, or
   not, by curScript() in quiz.js — the three-way script setting, which every one of the four
   quizzes now carries. There used to be .tq-tl-off rules driven by the site pill; they were
   deleted because the pill and the three-way were two controls over one thing, and whichever
   one the learner had not just touched won. If you are tempted to add a display:none here for
   a transliteration, the bug is in what the engine emitted. */

.reveal { border-top: 0.5px solid var(--border, rgba(0,0,0,.09)); margin-top: 14px; padding-top: 13px; }
.nextbtn {
  width: 100%; background: var(--accent, #261B65); color: #fff; border: 0;
  border-radius: var(--radius-md, 10px); padding: 11px;
  font-size: 14px; font-weight: 600; font-family: inherit; cursor: pointer; margin-top: 13px;
}
@media (hover: hover) {
  .nextbtn:hover { background: var(--accent-mid, #342682); }
}

/* ── Thai Builder: tray + answer box ─────────────────────────────────────────────────── */
.enq { font-size: 15.5px; font-weight: 500; line-height: 1.4; margin: 0 0 13px; }
.drop {
  min-height: 66px; border: 1.5px dashed var(--border-strong, rgba(0,0,0,.16));
  border-radius: var(--radius-md, 10px); padding: 9px;
  display: flex; flex-wrap: wrap; gap: 5px; align-content: flex-start;
  margin-bottom: 13px; background: #FCFCFB;
}
.drop.filled { border-style: solid; border-color: var(--accent, #261B65); background: var(--accent-light, #ECEBFA); }
.tile {
  background: var(--surface, #fff); border: 0.5px solid var(--border-strong, rgba(0,0,0,.16));
  border-radius: var(--radius-sm, 6px); padding: 6px 10px;
  font-family: var(--font-thai, 'Sarabun', sans-serif); font-size: 16px;
  cursor: pointer; line-height: 1.4; box-shadow: 0 1px 2px rgba(0,0,0,.04);
  font-weight: 400;
}
.tile.in { background: var(--accent, #261B65); border-color: var(--accent, #261B65); color: #fff; }
.tile .tt { display: block; font-family: var(--font-ui, 'Inter', sans-serif); font-size: 10.5px; color: var(--text-tertiary, #9A9A9A); line-height: 1.3; }
.tile.in .tt { color: rgba(255,255,255,.72); }
/* ⛔ A LOCKED head-start tile is not removable and must LOOK unremovable (§3A.6). */
.tile.lock { background: #EFEFF4; border-color: var(--border, rgba(0,0,0,.09)); color: var(--text-secondary, #5A5A5A); cursor: default; box-shadow: none; }
.traylab { font-size: 11px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--text-tertiary, #9A9A9A); margin: 0 0 7px; }
.tray { display: flex; flex-wrap: wrap; gap: 5px; }

/* ── Speak Thai ──────────────────────────────────────────────────────────────────────── */
/* ⚠ THE "Literally:" LINE KEEPS THE LIGHT PURPLE (owner, 2026-09-22): "the 'literally:'
   messages in the vocab quiz can stay the same light purple as the answer gloss chips and the play
   the sentence box." So it joins --chip-accent-light rather than following --accent. Those three
   together are the quiz's deliberate purple family; everything else in the arm is navy. */
.tq-say {
  font-size: 12.5px; color: var(--text-secondary, #5A5A5A);
  background: var(--chip-accent-light, #F6F6FD); border-radius: var(--radius-sm, 6px);
  padding: 9px 11px; margin: 0 0 14px; line-height: 1.45;
}
.tq-selfmark { display: flex; gap: 8px; margin-top: 13px; }
.tq-selfmark button {
  flex: 1; border-radius: var(--radius-md, 10px); padding: 12px 8px;
  font-size: 14px; font-weight: 600; font-family: inherit; cursor: pointer; border: 0.5px solid;
}
.tq-got { background: #F1F7F3; border-color: #1F5D3A; color: #1F5D3A; }
@media (hover: hover) {
  .tq-got:hover { background: #E4F0E8; }
}
.tq-not { background: #FCF2F1; border-color: #B4342E; color: #B4342E; }
@media (hover: hover) {
  .tq-not:hover { background: #F8E6E4; }
}

/* ── results ─────────────────────────────────────────────────────────────────────────── */
.tq-res { text-align: center; padding: 6px 0 2px; }
.tq-pct { font-size: 40px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.1; color: var(--quiz-ink); font-variant-numeric: tabular-nums; }
.tq-raw { font-size: 14px; color: var(--text-secondary, #5A5A5A); margin-top: 2px; font-variant-numeric: tabular-nums; }
.tq-best { font-size: 12.5px; color: var(--text-tertiary, #9A9A9A); margin-top: 9px; }
.tq-newbest { color: var(--quiz-ink); font-weight: 600; }
.tq-progptr { font-size: 12px; color: var(--text-secondary, #5A5A5A); background: var(--quiz-bg); border-radius: var(--radius-sm, 6px); padding: 9px 11px; margin: 14px 0 0; line-height: 1.45; }

/* ── phone ───────────────────────────────────────────────────────────────────────────── */
@media (max-width: 480px) {
  /* ✅ RESOLVED BY THE PAGE MIGRATION (2026-09-21), and the history is worth keeping because the
     symptom was subtle. There used to be a dark scrim behind the sheet, and on a phone the owner
     saw it only on overscroll: "it only shows when you try to scroll beyond the edge of the page
     i.e. scroll up at the top or down at the bottom. in its resting state its fine." That was the
     correct diagnosis and it ruled out the height-falls-short explanation I first reached for —
     the sheet covered every pixel at rest, and iOS rubber-banding revealed the one thing under
     it. The fix then was to paint the overlay in the sheet's own colour.
     ⛔ THERE IS NO SCRIM AND NO OVERLAY NOW, so there is nothing to paint and nothing to reveal.
     Do not re-add a background here "to be safe" — the page's own surface is already behind it. */
  .tq-page { padding: 0; }
  .tq-sheet { border-radius: 0; max-width: none; border: 0; }
  .tq-pct { font-size: 34px; }
  .seg { min-width: 48px; font-size: 12.5px; }
}

/* ── the site's Thai Font pill reaches the quiz too (§9.3) ───────────────────────────────
   ⚠ player-dyn.css toggles this class on <html> and lists the surfaces it applies to by
   SELECTOR — .sent-preview, .row-thai, .g-thai, .dyn-th. The quiz's Thai lives in different
   classes, so without its own rule the overlay would keep Sarabun while the page behind it
   switched, which is the "silent half-fix" shape player.js r223/r224 both warned about.
   ⛔ The quiz's classes must NOT be added to player-dyn.css's list instead: that file is
   shared and actively edited, and the rule belongs with the component it styles. */
html.te-thai-modern .thaibig,
html.te-thai-modern .tile,
html.te-thai-modern .chip .th,
html.te-thai-modern .opt .o-th,
html.te-thai-modern .tq-exitem .x-th { font-family: 'Noto Sans Thai', var(--font-thai, 'Sarabun', sans-serif); }

/* ── leaving mid-quiz (owner, 2026-09-20) ────────────────────────────────────────────────
   ⛔ The site's own modal, never window.confirm — the rule progress.html's reset already
   follows. ⚠ It sits INSIDE .tq-page and above it, so the quiz stays visible behind the
   warning: the learner can see what they are about to abandon.
   ⚠⚠ IT STAYS position:fixed EVEN THOUGH THE QUIZ NO LONGER IS (2026-09-21). This one really
   IS a dialog — it interrupts, it has two buttons and nothing else may be reached until one of
   them is pressed — so it keeps its scrim and its viewport anchoring. The migration removed the
   FALSE modal (the quiz itself), not this true one. */
.tq-confirm {
  position: fixed; inset: 0; z-index: 950;
  background: rgba(20, 16, 40, 0.55);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  /* ⛔ THE ONE SCROLLER THIS FILE ALLOWS, and it is allowed because this really is a fixed
     dialog (see the note above) — the ban on an inner scroll container is about the QUIZ, which
     is a view in normal flow. At large system text the card grows past a short viewport, and a
     centred flex child taller than its container overflows BOTH edges: the top of the card, and
     the "Keep going" button, become unreachable with no way to scroll to them. `auto` costs
     nothing while the card fits.
     ⚠ `align-items: center` must stay paired with `margin: auto` on the card, or an overflowing
     card is clipped at the START edge — the same trap .tq-fill's auto margins document. */
  overflow-y: auto; overscroll-behavior: contain;
}
.tq-confirm > .tq-confirm-card { margin: auto; }
.tq-confirm-card {
  background: var(--surface, #fff); border-radius: var(--radius-lg, 14px);
  max-width: 360px; width: 100%; padding: 20px;
  font-family: var(--font-ui, 'Inter', system-ui, sans-serif);
  box-shadow: 0 12px 40px rgba(0,0,0,.22);
}
.tq-confirm-card p { font-size: 14px; line-height: 1.6; margin: 0 0 1.2rem; color: var(--text-primary, #1A1A1A); }
.tq-confirm-card strong { font-weight: 600; color: #B4342E; }
.tq-confirm-actions { display: flex; gap: 10px; }
.tq-confirm-actions button {
  flex: 1; border-radius: var(--radius-md, 10px); padding: 10px;
  font-size: 13.5px; font-weight: 600; font-family: inherit; cursor: pointer;
}
.tq-stay { background: var(--accent, #261B65); color: #fff; border: 0; }
@media (hover: hover) {
  .tq-stay:hover { background: var(--accent-mid, #342682); }
}
.tq-leave { background: var(--surface, #fff); color: var(--text-secondary, #5A5A5A); border: 0.5px solid var(--border-strong, rgba(0,0,0,.16)); }
@media (hover: hover) {
  .tq-leave:hover { border-color: #B4342E; color: #B4342E; }
}

/* ── owner feedback round 1 (2026-09-20) ─────────────────────────────────────────────────── */

/* JOB 2 — transliteration on a Vocab option and in the exclusions list */
.opt .o-th { font-family: var(--font-thai, 'Sarabun', sans-serif); font-size: 17px; }
.opt .o-tl { display: block; font-size: 11.5px; color: var(--text-tertiary, #9A9A9A); line-height: 1.3; margin-top: 1px; }
/* JOB 7 — after answering, every option shows what it meant */
.opt .o-en { display: block; font-size: 12.5px; color: var(--text-secondary, #5A5A5A); line-height: 1.35; margin-top: 3px; }
.opt.right .o-en { color: #1F5D3A; }
.opt.wrong .o-en { color: #B4342E; }
.tq-exitem .x-tl { font-size: 11.5px; color: var(--text-tertiary, #9A9A9A); }
.cen { font-size: 13px; color: var(--text-secondary, #5A5A5A); margin: 0 0 4px; }

/* JOB 5 — a two-answer question must announce itself, and say so again mid-answer */
.tq-need {
  font-size: 13.5px; font-weight: 600; color: var(--accent, #261B65);
  background: var(--accent-light, #ECEBFA); border-radius: var(--radius-sm, 6px);
  padding: 7px 11px; margin: 0 0 12px; display: inline-block;
}
.tq-need b { font-size: 15px; }
/* ⚠ Sits where the reveal will go, so it occupies the space the answer is about to use rather
   than pushing the options around. Red because it is the one message the learner is STUCK
   without — the screen is correctly inert after one pick (§6.3 forbids marking it) and nothing
   else on screen can explain that. */
.tq-more {
  font-size: 14px; font-weight: 600; color: #B4342E;
  background: #FCF2F1; border: 0.5px solid rgba(180,52,46,.25);
  border-radius: var(--radius-md, 10px); padding: 11px 13px; margin: 13px 0 0; text-align: center;
}

/* JOB 4 — the persistent exclude control, last thing in the sheet on every question screen.
   ⚠ Quiet by design: it must be FINDABLE without competing with the answer. It is the only
   control on the question screen that changes future runs rather than this one. */
.tq-foot { margin-top: 20px; padding-top: 14px; border-top: 0.5px solid var(--border, rgba(0,0,0,.09)); }
.tq-skip {
  width: 100%; text-align: left; background: none; cursor: pointer; font-family: inherit;
  border: 0.5px solid var(--border-strong, rgba(0,0,0,.16)); border-radius: var(--radius-md, 10px);
  padding: 10px 13px; font-size: 13px; color: var(--text-secondary, #5A5A5A);
  transition: border-color .15s, color .15s, background .15s;
}
@media (hover: hover) {
  .tq-skip:hover { border-color: #B4342E; color: #B4342E; }
}
.tq-skip-sub { display: block; font-size: 11.5px; color: var(--text-tertiary, #9A9A9A); margin-top: 2px; }
.tq-skip.done { border-color: #1F5D3A; background: #F1F7F3; color: #1F5D3A; font-weight: 500; }
@media (hover: hover) {
  .tq-skip.done:hover { border-color: #1F5D3A; color: #1F5D3A; }
}
.tq-skip.done .tq-skip-sub { color: #3E7A57; }

/* ══════════════════════════════════════════════════════════════════════════════════════════
   JOB 6 — QUIZ TEXT SIZE (owner, 2026-09-20)
   ══════════════════════════════════════════════════════════════════════════════════════════
   ⭐ WHY THIS IS AN OVERRIDE LAYER AND NOT AN EDIT TO THE RULES ABOVE. Everything above this
   line is copied verbatim from the mockup the owner approved. Rewriting those values in em or
   calc() would make the approved design a derived thing that nobody could check against the
   mockup again. Instead the baseline stays exactly as approved and this layer scales FROM it:
   at --tq-scale: 1 every declaration below computes to the same number it overrides, so the
   default rendering is identical by construction, not by care.

   ⚠ It scales the BOXES AND THE GAPS, not just the glyphs — the owner asked for the heights and
   spacing to grow "as appropriate". Padding, min-heights and gaps are all in here for that
   reason; text-only scaling makes a large size look cramped rather than large.

   ⛔ NO TRANSFORM. `transform: scale()` was the obvious one-liner and is wrong here: it scales
   the layout box too, so the sheet would overflow its own width at the larger stops and the
   text would resample rather than re-render — blurry Thai at exactly the size someone chose
   because they were struggling to read it.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
.tq-sheet { --tq-scale: 1; }

/* question text */
.enq       { font-size: calc(15.5px * var(--tq-scale)); margin-bottom: calc(13px * var(--tq-scale)); }
.enq-nat   { font-size: calc(13.5px * var(--tq-scale)); margin: calc(-6px * var(--tq-scale)) 0 calc(13px * var(--tq-scale)); }
.thaibig   { font-size: calc(21px   * var(--tq-scale)); }
.tl        { font-size: calc(12.5px * var(--tq-scale)); }
.cen       { font-size: calc(13px   * var(--tq-scale)); }
.traylab,
.mlab      { font-size: calc(11px   * var(--tq-scale)); }

/* options */
.opt       { font-size: calc(13.5px * var(--tq-scale)); padding: calc(11px * var(--tq-scale)) calc(13px * var(--tq-scale)); margin-bottom: calc(7px * var(--tq-scale)); }
.opt .o-th { font-size: calc(17px   * var(--tq-scale)); }
.opt .o-tl { font-size: calc(11.5px * var(--tq-scale)); }
.opt .o-en { font-size: calc(12.5px * var(--tq-scale)); }

/* the Builder tray and answer box */
.tile      { font-size: calc(16px   * var(--tq-scale)); padding: calc(6px * var(--tq-scale)) calc(10px * var(--tq-scale)); }
.tile .tt  { font-size: calc(10.5px * var(--tq-scale)); }
/* ⚠ min-height here is only the FLOOR for an empty box; quiz.js measures the real one after the
   first layout and sets it inline on both containers so nothing reflows as tiles move. */
.drop      { min-height: calc(66px  * var(--tq-scale)); padding: calc(9px * var(--tq-scale)); gap: calc(5px * var(--tq-scale)); align-content: flex-start; }
.tray      { align-content: flex-start; }
.tray      { gap: calc(5px * var(--tq-scale)); }

/* chips */
.chip      { font-size: calc(12px   * var(--tq-scale)); padding: calc(4px * var(--tq-scale)) calc(8px * var(--tq-scale)); }
.chip .tlx { font-size: calc(10.5px * var(--tq-scale)); }
.chips     { gap: calc(5px * var(--tq-scale)); }
.vchips    { margin: calc(10px * var(--tq-scale)) 0 calc(15px * var(--tq-scale)); }

/* buttons and prompts */
.playbtn       { font-size: calc(14px   * var(--tq-scale)); padding: calc(15px * var(--tq-scale)); }
.nextbtn       { font-size: calc(14px   * var(--tq-scale)); padding: calc(11px * var(--tq-scale)); }
.startbtn      { font-size: calc(14.5px * var(--tq-scale)); padding: calc(12px * var(--tq-scale)); }
.tq-say        { font-size: calc(12.5px * var(--tq-scale)); padding: calc(9px * var(--tq-scale)) calc(11px * var(--tq-scale)); }
.tq-need       { font-size: calc(13.5px * var(--tq-scale)); }
.tq-need b     { font-size: calc(15px   * var(--tq-scale)); }
.tq-more       { font-size: calc(14px   * var(--tq-scale)); padding: calc(11px * var(--tq-scale)) calc(13px * var(--tq-scale)); }
.tq-selfmark button { font-size: calc(14px * var(--tq-scale)); padding: calc(12px * var(--tq-scale)) calc(8px * var(--tq-scale)); }
.tq-skip       { font-size: calc(13px   * var(--tq-scale)); padding: calc(10px * var(--tq-scale)) calc(13px * var(--tq-scale)); }
.tq-skip-sub   { font-size: calc(11.5px * var(--tq-scale)); }

/* menu controls */
.tq-res-h  { font-size: calc(13px   * var(--tq-scale)); }
/* ⛔⛔ THE ONE SURFACE IN THIS ARM THAT IS CAPPED AGAINST OS TEXT SCALING, and it is capped for
   the reason TEXT_SCALING.md §2 gives for the nav: it is a DENSE SIX-COLUMN GRID whose width is
   set by its content, so full inflation does not make it more readable, it makes it unreachable.
   `--te-ui` caps growth at 1.15 and is exactly 1 at default size, so this is a no-op until the
   learner has actually raised their system text.
   ⚠ IT CAPS, IT DOES NOT FREEZE — WCAG 1.4.4 wants text resizable, and the wrap fix above is
   what carries the rest of the load. Everything ELSE in the quiz scales in full: this table is
   the exception because it is a table, not because small type is acceptable here.
   ⛔ Do not extend `--te-ui` to .tq-res-h, the picker or any question screen — those are
   content, they reflow, and capping them would be the freeze this document forbids. */
.tq-res-t  { font-size: calc(12.5px * var(--tq-scale) * var(--te-ui, 1)); }
.tq-res-t thead th { font-size: calc(11px * var(--tq-scale) * var(--te-ui, 1)); }
.tq-mascot-t { font-size: calc(14px * var(--tq-scale)); }
.seg       { font-size: calc(13px   * var(--tq-scale)); padding: calc(7px * var(--tq-scale)) calc(4px * var(--tq-scale));
             min-width: calc(58px * var(--tq-scale)); }
/* ⚠ The row wraps rather than scrolls, so the gap has to scale too or the wrapped rows
   sit on top of each other at the larger stops. */
.segs      { gap: calc(6px * var(--tq-scale)); }
.radio,
.checkrow  { font-size: calc(13.5px * var(--tq-scale)); }
.radio .rd { font-size: calc(12px   * var(--tq-scale)); }
.hint      { font-size: calc(12px   * var(--tq-scale)); }
.exline    { font-size: calc(13px   * var(--tq-scale)); }
.tq-exitem { font-size: calc(13px   * var(--tq-scale)); }

/* the picker */
.qpick .nm { font-size: calc(14px   * var(--tq-scale)); }
.qpick .ds { font-size: calc(11.5px * var(--tq-scale)); }

/* ⭐ THE WHOLE MENU HAS TO FIT ONE SCREEN, with "Back to the topic" the last thing on it
   (owner, 2026-09-20). Four rows, a mascot and a way out is not much, but the rows were sized
   for a screen with nothing else on it. Everything here is a trim rather than a redesign: the
   rows lose vertical padding, the gaps close, and the mascot takes what that frees.
   ⚠ The trims are on .qpick-big only — the compact .qpick is not used anywhere yet, and
   changing a variant nobody renders is how a rule gets forgotten. */
/* ⚠ The last 16px, taken from the rows rather than the mascot. Four rows at 2px less
   padding each is invisible; making the tiger smaller is the thing the owner asked me to
   undo. An icon, a name and a one-line description do not need 83px of row. */
.tq-fill { padding-bottom: 0; }
.tq-fill .tq-minor { margin-top: 5px; }
.qpick .sc { font-size: calc(12px   * var(--tq-scale)); }
.tq-sheet h3 { font-size: calc(16px * var(--tq-scale)); }
/* ⛔ The hero header is the PICKER's, and the picker is not text-scaled (see applyFontScale).
   Leaving it on the variable would be harmless today because the picker pins --tq-scale to 1,
   but it would silently start scaling again the first time that changes. */
.tq-sheet .sub { font-size: calc(12.5px * var(--tq-scale)); }

/* the size picker itself — five A's, ascending, the standard shape for this control */
.tq-fsize { display: flex; align-items: baseline; gap: 6px; }
.tq-fs {
  flex: 1; background: var(--surface, #fff); cursor: pointer; font-family: inherit;
  border: 0.5px solid var(--border-strong, rgba(0,0,0,.16)); border-radius: var(--radius-sm, 6px);
  padding: 8px 4px; color: var(--text-secondary, #5A5A5A); line-height: 1; font-weight: 600;
}
.tq-fs.on { background: var(--accent, #261B65); border-color: var(--accent, #261B65); color: #fff; }

/* JOB 12 — "use these settings for all X quizzes"
   ⚠ It needs to LOOK clickable: as plain text in a list of settings it read as a caption
   (owner: "so its not just text - otherwise you dont really know its clickable"). The sync
   glyph is the affordance; the border makes it a control rather than a label. */
.tq-all {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  background: var(--surface, #fff); border: 0.5px solid var(--border-strong, rgba(0,0,0,.16));
  border-radius: var(--radius-md, 10px); padding: 10px 13px; cursor: pointer;
  font-family: inherit; font-size: calc(13px * var(--tq-scale, 1));
  color: var(--text-secondary, #5A5A5A);
  transition: border-color .15s, color .15s, background .15s;
}
@media (hover: hover) {
  .tq-all:hover { border-color: var(--accent, #261B65); color: var(--accent, #261B65); background: var(--accent-light, #ECEBFA); }
}
/* ⚠ STROKED, not filled — it is the dyn player's sync glyph, which is a stroke icon. A
   leftover `fill: currentColor` here would paint both arcs solid and make it a blob. */
.tq-sync { width: 17px; height: 17px; fill: none; flex-shrink: 0; }
.tq-all.done {
  border-color: #1F5D3A; background: #F1F7F3; color: #1F5D3A; font-weight: 500; cursor: default;
}
.tq-all.done .tq-sync { display: none; }

/* ══════════════════════════════════════════════════════════════════════════════════════════
   JOBS 8 + 13 — USING THE SCREEN (owner, 2026-09-20)
   "at the end of the quiz on android app i feel that everything is very top heavy, bottom 2/3
   of screen nearly is white" — and the same of the quiz menu.
   ⚠ The cause is structural, not cosmetic: on a phone .tq-sheet is min-height:100% (so the
   overlay is a full screen) while the CONTENT of the picker and the results is short. Making
   the type bigger alone would not fix it; the content has to be told to occupy the height.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
/* ⛔⛔ .tq-fill NO LONGER STRETCHES, and that was the real cause of the overflow rather than
   any single measurement. `flex: 1` on the rows plus `margin-top: auto` on the footer were there
   to push a SHORT picker down the screen — written when the picker was four rows and nothing
   else. With a mascot and a results row added it is no longer short, and those two rules were
   spending height the content now needs, on a phone where there is none to spend.
   ⚠ The owner's words were "there's room for all elements to shift up", which is exactly what
   removing the stretch does: everything sits at its natural height from the top. */
.tq-fill { display: flex; flex-direction: column; }

/* ⛔⛔ THE PICKER'S SLACK IS DISTRIBUTED, NOT DUMPED (owner, 2026-09-20, with a screenshot of
   both phones: "way too much dead space at bottom of the menu for the quizzes page - about twice
   the height of one of the four quiz boxes in deadspace ... you can shift the four quizzes down
   a bit so they arent so close to the title, make the tiger not so close to the four boxes. i
   guess just decompress stuff a bit - everything is a good size").
   ⚠⚠ THE DEAD SPACE WAS CREATED BY THE FIX FOR THE OPPOSITE FAULT. .tq-sheet is min-height:100%
   on a phone, so the sheet is always full height; the old rules (`flex:1` on .qpick and
   `margin-top:auto` on .tq-minor) absorbed the slack, and removing them to stop a SHORT screen
   overflowing left a tall one with ~340px in one lump under the last element. Both readings were
   right about their own phone and wrong as a rule.
   ✅ So: flexible GAPS that grow into the slack up to a cap, then auto margins on the first and
   last children centre whatever is still over. A short screen collapses every gap to its min and
   scrolls exactly as before — flex-grow resolves BEFORE auto margins, so the caps are honoured
   first and the centring only ever sees the remainder.
   ⚠ Auto margins rather than `justify-content: center`, deliberately: centring can push the
   START of an overflowing flex container out of reach, and the first thing off the top would be
   the unit name. Auto margins never overflow the start edge. */
.tq-fill:not(.tq-fill-res) > .tq-gap { flex: 1 1 0; min-height: 6px; max-height: 56px; }
.tq-fill:not(.tq-fill-res) > .tq-gap-s { min-height: 2px; max-height: 34px; }
.tq-fill:not(.tq-fill-res) > :first-child { margin-top: auto; }
.tq-fill:not(.tq-fill-res) > :last-child { margin-bottom: auto; }

/* ⛔⛔ THE ONLY .qpick-big BLOCK. There were TWO, ~60 lines apart, and the later one silently
   won every trim the earlier one made — identical specificity, so source order decided, and two
   passes of "make the rows tighter" changed nothing at all while reporting success. The row
   stayed at 15px padding and a 40px icon throughout. That is why the menu would not come down to
   size: the measurements were honest and the rule being edited was dead.
   ⚠ If a third is ever added, this one wins or loses purely on where it sits in the file. One
   block.
   ⚠ The --tq-scale here is vestigial: applyFontScale() pins the picker to 1 (the text-size
   control deliberately does not reach this screen), so these resolve to the literal px. Left in
   place rather than stripped, because the compact .qpick variant may yet want scaling. */
/* ⚠ THE ROW SEPARATION IS A `gap`, NOT A MARGIN (owner, 2026-09-20: "the four quiz pills
   could be further apart from each other ... there's plenty of whitespace around the 4 quiz
   pills"). It was `gap: 8px` on .qpick PLUS `margin-bottom: 6px` on each button — 14px between
   rows and a stray 6px hanging off the last one. One property now, and the trailing space goes.
   ✅ Nothing else had to move: the flexible .tq-gap elements give up exactly this much and the
   tiger stays where it is, which is what the owner asked for ("no need to move the tiger down
   or to push the elements above up"). That is the whole point of making the slack flexible. */
.qpick-big { gap: 20px; }
.qpick-big button { padding: 9px 13px; gap: 12px; }
.qpick-big .ic { width: 34px; height: 34px; border-radius: 10px; }
.qpick-big .nm { font-size: 14.5px; }
.qpick-big .ds { font-size: 11.5px; margin-top: 1px; }
.qpick-big .sc { font-size: 13px; }

/* ⭐ THREE ROWS, NOT FOUR (§9.2a). Grammar units and playlists have no Vocab Trainer, and a
   four-row panel with three rows in it reads as empty rather than as a shorter menu. Owner:
   "enlarge the bars a little and space them out more". ⛔ A VARIANT, never a second .qpick —
   this block must stay immediately after .qpick-big and must be the ONLY .qpick-three, or it
   drifts exactly as the two duplicated .qpick-big blocks did (see the warning above them). */
.qpick-three { gap: 26px; }
.qpick-three button { padding: 13px 15px; }
.qpick-three .ic { width: 38px; height: 38px; }
.qpick-three .nm { font-size: 15.5px; }
.qpick-three .ds { font-size: 12px; }

/* the results screen: the score is the point of the screen, so it takes the space */
.tq-fill-res .tq-res { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 8px 0 18px; }
/* ⛔⛔ THE SCORE IS CAPPED AGAINST THE VIEWPORT, NOT ONLY AGAINST --tq-scale. "100%" at this
   size is ~4.3 characters wide, so the number alone decides whether the screen side-scrolls:
   measured 619px inside a 343px sheet at 390@2.0×, i.e. 260px of page overflow, and it already
   overflowed by ~21px at 320px with NO scaling at all (§7a's lesson — check 1× first).
   ⚠ `min()`, so the vw arm only ever bites when it is the smaller of the two: at 390px and
   above the default rendering is the unchanged 64/72px. It is a ceiling, not a new size.
   ⛔ Not `transform: scale()` and not a smaller constant — the number IS the screen, and the
   surrounding rules (.tq-raw, .tq-best) are deliberately left to scale in full. */
.tq-fill-res .tq-pct { font-size: min(calc(64px * var(--tq-scale, 1)), 19vw); line-height: 1; }
.tq-fill-res .tq-raw { font-size: calc(16.5px * var(--tq-scale, 1)); margin-top: 8px; }
.tq-fill-res .tq-best { font-size: calc(14px * var(--tq-scale, 1)); margin-top: 14px; }
.tq-selfnote { margin: 20px auto 0; max-width: 300px; text-align: left; }
.tq-acts { display: flex; flex-direction: column; gap: 9px; margin-top: auto; }
.tq-acts .tq-exit, .tq-acts .tq-return {
  background: none; border: 0.5px solid var(--border-strong, rgba(0,0,0,.16));
  border-radius: var(--radius-md, 10px); padding: calc(11px * var(--tq-scale, 1));
  font-family: inherit; font-size: calc(13.5px * var(--tq-scale, 1));
  color: var(--text-secondary, #5A5A5A); cursor: pointer; text-align: center;
}
@media (hover: hover) {
  .tq-acts .tq-exit:hover, .tq-acts .tq-return:hover { border-color: var(--accent, #261B65); color: var(--accent, #261B65); }
}

/* ⚠ Only worth filling when the sheet actually IS the screen. On desktop the sheet is a 480px
   card in the middle of a backdrop, and stretching a card to the viewport looks broken. */
@media (max-width: 480px) {
  /* ⭐⭐ THIS MIN-HEIGHT IS WHAT KEEPS THE PHONE SPACING THE OWNER APPROVED — IT IS NOT LEFTOVER
     VIEWPORT GYMNASTICS. KEEP IT (owner, 2026-09-21: "preserve spacing - how it looks now is
     good").
     The picker and results distribute their slack through FLEXIBLE GAPS (`.tq-gap { flex: 1 1 0;
     max-height: 56px }`) plus auto margins on the first and last child. Those gaps can only grow
     into slack that EXISTS, and the slack came from the sheet being told to fill the screen.
     Delete this and every gap collapses to its 6px minimum: the picker and the results screen
     become tight and top-aligned, undoing the 2026-09-20 pass ("you can shift the four quizzes
     down a bit ... i guess just decompress stuff a bit - everything is a good size").
     ⚠⚠ AND IT NO LONGER CARRIES THE BUG THAT MADE "MY RESULTS" INVISIBLE, which is the whole
     reason it is safe to keep. That fault (owner, 2026-09-20: "on mobile browser on iphone (not
     PWA) i cant see the 'my results' dropdown ... why?") needed TWO things: iOS Safari's `100vh`
     being the LARGE viewport — taller than what you can see — AND the sheet being a FIXED,
     self-scrolling box, so anything past the visible height was simply unreachable. The box is
     gone. This is normal flow now: if the min-height overestimates, the PAGE scrolls and the last
     child is one swipe away. A min-height can no longer hide anything.
     ⚠ THE PWA WAS UNAFFECTED AND THAT WAS THE TELL: standalone mode has no URL bar, so there
     vh == what you see. A fault that appears only in the browser and not in the installed app is
     this, almost every time. Worth remembering even though the class is now closed.
     ⚠ dvh tracks the chrome as it collapses; the vh line stays as the fallback for anything that
     does not know dvh. Both subtract the 54px sticky nav, which is IN FLOW above the quiz — omit
     that and the quiz is one nav taller than the space it has, which is a scrollbar on a screen
     that should have none. */
  .tq-fill { min-height: calc(100vh - 54px); }
  .tq-fill { min-height: calc(100dvh - 54px); }
  /* ⚠ Same viewport ceiling as the base rule above — a phone is exactly where it bites. */
  .tq-fill-res .tq-pct { font-size: min(calc(72px * var(--tq-scale, 1)), 19vw); }

  /* ⭐ A FINE TRIM SO "MY RESULTS" IS NOT HALF CUT OFF (owner, 2026-09-22, with a screenshot):
     "we can get some space by making the space around the tiger slightly smaller (both above and
     below) and reducing the space between the title and the vocab trainer (the top quiz choice) —
     but you really dont have to take off much."
     ⚠⚠ AND THE REASON IT HAD TO BE WRITTEN HERE: the caps he was actually looking at were the
     TOP-LEVEL ones (56px / 34px, ~line 860). The 30px/10px pair that reads like the phone answer
     — including the 2026-09-22 "space under the tiger, HALVED" fix — sits inside
     `@media (min-width: 480.02px)`, which is the DESKTOP block. A phone at 390px has never seen
     it. Measured in a 390px iframe against the shipped file: 66px title→picker, 58px above the
     tiger, 39px below, with the computed max-height reading 56px, not 30px.
     ⛔ So do NOT "consolidate" these with the 480.02px block's pair. They look like duplicates
     and they serve opposite sides of the breakpoint.
     ⚠ These are CAPS on flexible gaps, not sizes: on a tall screen the slack is still shared out,
     and on a short one the gaps were already shrinking below the cap and this changes nothing.
     That is the intended asymmetry — it only bites where there is slack to give back. */
  /* ⭐ …AND THE BLOCK STARTS AT THE TOP ON A PHONE (owner, 2026-09-22, second pass): "i think if
     we trim a bit of space from above 'test yourself' that would be good, then everything else
     shifts up and spacing preserved."
     The top-level rule gives the first child `margin-top: auto`, which pairs with the last
     child's to CENTRE the block — so on a phone the slack was being split above and below, and
     the share above the eyebrow was the space he could scroll up into. Dropping it here sends the
     whole share to the bottom instead: every gap keeps the size it had and the block simply sits
     higher, which is exactly what he asked for.
     ⚠ The auto margin's stated purpose — "never overflow the start edge" — is not lost. That
     protects content TALLER than the box, and a zero top margin cannot overflow the start edge at
     all; it starts there. ⛔ Phone only: the desktop sheet is a card on a backdrop and still
     centres. */
  .tq-fill:not(.tq-fill-res) > :first-child { margin-top: 0; }
  .tq-fill:not(.tq-fill-res) > .tq-gap { max-height: 34px; }
  .tq-fill:not(.tq-fill-res) > .tq-gap-s { max-height: 20px; }
  /* the last few px come off the tiger's own box, so the picture keeps its breathing room */
  .tq-mascot { margin-top: 0; }
  .tq-mascot img { margin-bottom: 3px; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════
   JOB 10 — THE ENTRY BLOCK, TIER-COLOURED (owner, 2026-09-20)
   "the Test yourself button should be the color of the topic … please make it more prominent …
   its a great feature. and shouldnt be an afterthought."
   ⛔ THERE IS NO PREMIUM PAIR ANY MORE (A6, 2026-09-22) — one accent, both tiers. Retired: the
   TEXT gold #B29234 on #FBF5DC (never the graphic gold #F0CC5C).
   The standing palette rule is that the bright gold is for graphics and the darker one for
   anything carrying text, and the topic card's own premium pill already uses exactly this pair.
   Matching it is what makes the block read as part of the page rather than bolted on.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
/* ⛔ NO TAP FLASH (owner, 2026-09-20: "on android when i hit the 'choose a quiz' button ...
   there's a blue highlight thing, which very very briefly then overlays the test menu as it pops
   up, like a little blue shadow").
   Chromium paints its tap highlight on the element that was touched and holds it for ~300ms
   AFTER the touch ends — so on a control whose job is to open an overlay, the flash outlives
   the screen it belonged to and lands on top of the new one. It cannot be fixed on the overlay;
   it has to be suppressed on the control. Every quiz control gets it, not just this one: the
   same overlap happens on any tap that swaps the screen.
   ⚠ Only the HIGHLIGHT goes. :active states and focus rings stay — they are the feedback that
   the tap registered, and removing those would trade a cosmetic flash for an unresponsive feel. */
.tq-sheet button, .tq-page button,
.tqe-go, .tqe-tile, .tq-entry button { -webkit-tap-highlight-color: transparent; }

/* ══ the mascot at the foot of the quiz menu ══════════════════════════════════════
   ⚠⚠ TWO CAPS, NOT ONE, AND THAT IS THE BIT THAT HAS GONE WRONG BEFORE. A percentage alone
   lets the image balloon on a wide sheet; a fixed px alone overflows a narrow phone. `width` is
   the intent, `max-width: 46%` is the guard, and the aspect-ratio holds the box so nothing jumps
   when the bytes land.
   ⚠ The sheet is already narrower than the page, so this is deliberately smaller than the
   158px the /topics mascot uses — the same figure at the same size would dominate a panel a
   third of the width. */
/* ⚠ Bigger than the first pass (owner: "the tiger is too small currently"), and it can afford
   to be now that the rows above it are tighter. The two caps stay: a fixed width for intent, a
   percentage for the guard, so it cannot balloon on a wide sheet or overflow a narrow one. */
/* ⚠ SIZED AGAINST THE SITE'S OTHER MASCOTS, not guessed. guide.html renders khwai.png at
   200×177 — all but identical proportions to the tiger's 440×389 — and about.html's is 195px.
   172px looked small precisely because it was smaller than every other animal on the site.
   ⚠ It grows UPWARD: the top margin shrinks rather than the block pushing the exit down, which
   is what keeps the whole menu on one screen. The two caps stay — a fixed width for intent, a
   percentage for the guard. */
.tq-mascot { text-align: center; margin: 2px 0 0; }
.tq-mascot img {
  width: 200px; max-width: 58%; height: auto; aspect-ratio: 440 / 389;
  display: block; margin: 0 auto 5px;
}
.tq-mascot-t {
  margin: 0; font-size: 14px; font-weight: 500; letter-spacing: .02em;
  color: var(--accent, #261B65);
}
/* A13: "all pictures on website have text under them e.g. stay sharp - these all need to update" */
.tq-sheet.premium .tq-mascot-t { color: var(--accent, #261B65); }
/* ⛔ The ellipsis animates WIDTH, never `content`: the Android app's webview cannot animate
   `content`, and this is lifted from topics-page.css where that is already documented.
   steps(3, start) is what makes it 1 -> 2 -> 3 dots rather than a smooth slide. */
.tq-mascot-t .dots { display: inline-block; width: 1.1em; text-align: left; }
/* ⛔⛔ `white-space: nowrap` IS LOAD-BEARING AND IT IS NOT ABOUT THIS ELEMENT'S OWN COPY.
   This is a THREE-CHARACTER string in a box declared `width: 0` — the animation reveals it by
   growing the box, and `overflow: hidden` clips it meanwhile. Add `overflow-wrap: break-word`
   anywhere above it (the .tq-sheet rule does, for the OS-text-scaling pass) and the browser
   obediently breaks "..." at zero width into THREE LINES, one dot each. The inline-block becomes
   three lines tall, the caption's line box follows, and "Stay sharp" gains 45px of height —
   measured at 390@1×, i.e. AT DEFAULT SIZE, on a change that was supposed to be a no-op.
   ⚠⚠ THE GENERAL TRAP, worth more than the fix: an inherited wrapping rule reaches every
   ZERO-WIDTH OR CLIPPED box under it, and those are exactly the boxes whose height nobody is
   watching. If you add a wrap rule to a container, look for `width: 0` beneath it.
   ✅ Caught only by the §6 no-op check (TEXT_SCALING.md — measure twice, with the new rules
   overridden away, and confirm the numbers match). Nothing else would have reported it. */
.tq-mascot-t .dots::after {
  content: '...'; display: inline-block; width: 0; overflow: hidden;
  white-space: nowrap;
  vertical-align: bottom; animation: tq-dots 1.5s steps(3, start) infinite;
}
@keyframes tq-dots { from { width: 0; } to { width: 1.05em; } }
/* ⚠ Someone who has asked for less motion should not get a blinking caption. */
@media (prefers-reduced-motion: reduce) {
  .tq-mascot-t .dots::after { animation: none; width: 1.05em; }
}

.tq-entry {
  --tqe-ink: var(--accent, #261B65);
  --tqe-bg: var(--accent-light, #ECEBFA);
  --tqe-line: rgba(28, 18, 78, .22);   /* the accent at 22% - keep in step with --accent */
  /* ⚠ 80%, CENTRED (owner, 2026-09-20: "reduce the width of the overall box they are
     contained in a bit as well to remove excessive dead space? maybe by like 20%"). It is a
     percentage rather than a fixed width so it stays 20% narrower at every size, and the 760px
     cap still holds on a wide desktop. */
  width: 80%; max-width: 760px; margin: 32px auto 44px; padding: 20px 18px 18px;
  /* ⚠ NEUTRAL GROUND, tier colour on the edge. See the grid rules below: a tinted ground made
     one of the four coloured tiles invisible on a premium unit. */
  background: var(--surface-2, #F7F7F6);
  border: 0.5px solid var(--tqe-line);
  border-radius: var(--radius-lg, 14px);
  font-family: var(--font-ui, 'Inter', system-ui, sans-serif);
  /* ⚠ The same inherited guard .tq-sheet carries — the heading, the subtitle and the "Choose a
     quiz" button all hold a word long enough to burst this block at large system text. */
  overflow-wrap: break-word;
}
.tq-entry.premium { --tqe-ink: var(--accent, #261B65); --tqe-bg: var(--accent-light, #ECEBFA); --tqe-line: rgba(28, 18, 78, .22); }

.tqe-head { margin-bottom: 14px; }
.tqe-t { margin: 0 0 3px; font-size: 18px; font-weight: 600; letter-spacing: -0.01em; color: var(--tqe-ink); }
.tqe-s { margin: 0; font-size: 13px; color: var(--text-secondary, #5A5A5A); }

/* the 2x2 the owner suggested — each tile a shortcut, and each carrying its own best score */
/* ⭐⭐ A SQUARE, COLOURED 2×2 (owner, 2026-09-20: "more geometric and colorful and visually
   nice"). Icon centred, name beneath, and the block's own ground showing between the tiles as
   the "little moat" he described.
   ⚠ THE BLOCK GOES NEUTRAL SO THE TILES CAN BE COLOURED. It used to be tinted with the tier
   colour, which on a premium unit is the same gold as one of the tiles would have been — the
   tile would have disappeared into its own background. The tier colour now lives on the heading,
   the border and the button, which is where it carries meaning; the ground is neutral so all
   four tints read equally on any tier.
   ⚠ aspect-ratio 1, not a fixed height: the tiles stay square as the column width changes, and
   a long quiz name wraps inside the square rather than stretching it. */
.tqe-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 13px; }
/* ⛔⛔ SQUARE TILES, EVENLY SPACED, IN A NARROWER BOX (owner, 2026-09-20: "can you make the
   four quiz boxes inside actually square and evenly spaced from one another? and then maybe
   reduce the width of the overall box").
   ⚠⚠ SQUARE AND SMALL ARE IN TENSION, WHICH IS WHY THE BOX NARROWS TOO. A square tile's height
   IS half the box's inner width, so the only way to keep the square and lose the bulk is to take
   width off the container — which is exactly what the owner proposed. Modelled before choosing:
   at a 390px viewport a full-width box gives 160px squares and a 479px block; at 80% it gives
   124px squares and a 407px block, i.e. 48% of an 844px phone against the 60% it started at.
   ⚠ An intermediate 2:1 tile got it to 353px and was rejected — the shape mattered more than the
   last 54px, and it is his call.
   ⚠ ONE `gap` governs both axes, so the four are evenly spaced by construction; do not reach for
   row-gap/column-gap here, because unequal spacing is what makes a 2×2 read as two rows of two
   rather than a block of four. */
.tqe-tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  aspect-ratio: 1 / 1; border: 0; border-radius: var(--radius-md, 10px); padding: 8px 6px;
  /* ⛔⛔ A GRID ITEM IS FLOORED AT ITS MIN-CONTENT WIDTH UNLESS YOU SAY OTHERWISE, and that
     floor is a whole WORD. At large system text "comprehension" was wider than half the block,
     so the `1fr` track grew to fit it, the 2×2 grew with it and the topic page side-scrolled by
     78px (measured 390@2.0×, 2026-09-22). The tile's own overflow was never the problem — the
     TRACK SIZING was, and no amount of wrapping inside the tile can undo it.
     ⚠ Pairs with `overflow-wrap: anywhere` on .tqe-nm below: this lets the track shrink, that
     lets the word break once it has. Either alone leaves half the fault in place. */
  min-width: 0;
  font-family: inherit; cursor: pointer; text-align: center;
  background: var(--t-bg); color: var(--t-ink);
  transition: transform .12s ease, box-shadow .15s ease;
}
@media (hover: hover) {
  .tqe-tile:hover { transform: translateY(-1px); box-shadow: 0 3px 10px rgba(0,0,0,.09); }
}
.tqe-tile:active { transform: none; }

/* the four, from tints already in this stylesheet */
.tqe-tile.t-v { --t-bg: var(--accent-light); --t-ink: var(--accent); }   /* Vocab — the accent, themed */
.tqe-tile.t-l { --t-bg: #F1F7F3; --t-ink: #1F5D3A; }   /* Listen  — the "right" green */
.tqe-tile.t-b { --t-bg: #FCF2F1; --t-ink: #B4342E; }   /* Builder — the "wrong" clay */
.tqe-tile.t-s { --t-bg: #EFEFF4; --t-ink: #44506B; }   /* Speak   — slate, NOT gold: see quiz.js */

.tqe-ic { display: flex; align-items: center; justify-content: center; }
.tqe-ic svg { width: 26px; height: 26px; display: block; }
/* ⚠ A SQUARE TILE HAS VERTICAL ROOM, SO THE NAME MAY WRAP AND THE TYPE STAYS READABLE.
   While the tile was a 2:1 rectangle there was only one line's worth of height, so the font had
   to shrink until "Listening comprehension" fitted on one — the owner's own fallback ("unless it
   collapses to multiple rows in which case reduce until it fits on one line"). Square tiles
   remove that constraint: at a 390px viewport the tile is ~124px tall and the content needs
   ~68px for an icon and TWO lines, so wrapping costs nothing and the 10.5px step is gone.
   ⛔ NOT `white-space: nowrap` — a learner who has raised their phone's text size should get
   more lines, not a clipped name, which is the case the owner explicitly allowed. */
.tqe-nm {
  font-size: 12px; font-weight: 600; line-height: 1.2; color: currentColor;
  letter-spacing: -0.005em;
  /* ⚠ `anywhere`, not `break-word`, and the difference is the whole point HERE (it is the other
     way round on .tq-sheet). `anywhere` also shrinks the MIN-CONTENT contribution, which is what
     the `1fr` grid track is sized from — so the name can no longer decide how wide the block is.
     ⛔ Still not `nowrap`: "a learner who has raised their phone's text size should get more
     lines, not a clipped name" is the owner's own allowance, recorded above. */
  overflow-wrap: anywhere;
}
.tqe-sc { font-size: 11px; font-weight: 700; opacity: .72; font-variant-numeric: tabular-nums; }

/* ══════════════════════════════════════════════════════════════════════════════════════════
   ⭐ THE THREE-QUIZ VARIANT (§9.2a) — grammar units and playlists. Vocab Trainer is
   topic-only, so these render THREE, and a 2-column grid of square tiles with three in it
   leaves a visible empty quarter that reads as a missing feature.
   Owner, 2026-09-21: "for desktop, three squares across, horizontally (for playlists) cos
   right now its 4 squares in a horizontal line for desktop on topics. so its uniform …
   all these configurations make best use of the layout (phone tall, desktop wide)."
       phone   → three full-width HORIZONTAL BARS, stacked
       ≥700px  → three SQUARES across, exactly as the topic block does four

   ⛔⛔ THE ONE REAL TRAP IS `aspect-ratio: 1 / 1` ON .tqe-tile. Drop the grid to one column
   and each tile becomes a FULL-WIDTH SQUARE — three of them stacked is a screen and a half,
   which is the identical failure to the four-square stack the no-one-column rule below was
   written for. A column-count change ALONE looks finished and is wrong. The aspect ratio and
   the flex direction are both part of the variant: a square wants its icon ABOVE its label, a
   bar wants them side by side.
   ⚠ The spacing needs no new value — ONE `gap` governs both axes, so a single-column grid
   inherits the identical "little moat" the 2×2 has, at every breakpoint, for free. */
/* ⭐⭐ …AND THE SAME BARS ARE HOW THE FOUR-TILE BLOCK SURVIVES OS TEXT SCALING (2026-09-22).
   ⚠⚠ NOT OVERFLOW — SHAPE. With `min-width: 0` on the tile the 2×2 stops bursting the page, and
   what is left is worse to look at than the overflow was: measured at 320@2.0× the tiles are
   78px wide and 183px tall, and "Listening comprehension" renders as "Liste / ning / comp /
   rehen / sion". A square tile's width is half the block's, and the block is 80% of a page whose
   own padding is in rem — so at 2× the width available per tile SHRINKS while the type doubles.
   No amount of wrapping fixes a 78px square; the tile has to stop being one.
   ✅ So it borrows the variant the owner already approved for three-quiz units: icon beside the
   label, full width, stacked. `.bars` is set by quiz.js from `--te-ui-raw` (mountButton →
   applyEntryShape), because CSS cannot query text inflation — a media query sees the viewport,
   which textZoom does not change.
   ⛔ ONE BLOCK FOR BOTH VARIANTS. The §9.2a rule ("a variant class, never a second block")
   applies to the RULES as much as the markup: a second copy of these declarations is a control
   that gets added to one shape and silently not the other. */
.tq-entry.three .tqe-grid,
.tq-entry.bars  .tqe-grid { grid-template-columns: 1fr; }
.tq-entry.three .tqe-tile,
.tq-entry.bars  .tqe-tile {
  aspect-ratio: auto;
  flex-direction: row;
  justify-content: flex-start;
  gap: 12px;
  min-height: 54px;
  padding: 11px 15px;
  text-align: left;
}
.tq-entry.three .tqe-nm,
.tq-entry.bars  .tqe-nm { font-size: 13.5px; }
.tq-entry.three .tqe-ic svg,
.tq-entry.bars  .tqe-ic svg { width: 24px; height: 24px; }
/* ⚠ A BAR HAS A LINE'S WIDTH, SO THE NAME WRAPS AT SPACES AGAIN — put `break-word` back over
   .tqe-nm's `anywhere`. `anywhere` exists only to stop a WORD sizing a grid TRACK, and a
   single-column grid has no track to size. Leaving it on would break "comprehension" mid-word
   inside a bar that has room for it. */
/* ⚠ AND IT IS A FLEX ITEM IN A ROW NOW, so it needs the same `min-width: 0` the square tile
   needed as a grid item — measured: without it the bar was 163px holding 253px of name. Two
   different layouts, one cause (an item is floored at its longest word), two places to fix it. */
.tq-entry.three .tqe-nm,
.tq-entry.bars  .tqe-nm { overflow-wrap: break-word; min-width: 0; }
/* ⛔ THE BLOCK GIVES UP ITS 80% AT LARGE TEXT, and only then. The 80% is the owner's own
   ("reduce the width of the overall box ... maybe by like 20%") and was asked for at default
   size, where the block has width to spare. Under inflation it is the opposite problem: the
   page's own padding grows with the text while the block takes a fixed SHARE of what is left,
   so the bar ends up ~160px holding 24px type. The narrowing is a cosmetic preference; fitting
   the words is not. ⚠ Scoped to `.bars`, so nothing changes at default size. */
.tq-entry.bars { width: 100%; }

/* ⚠ Three columns would make the squares tiny on a phone; two is the shape he asked for and it
   holds at every width. Only the icon shrinks. */
/* ⚠ Only the icon and the name step down on a small phone; two columns hold at every width. */
@media (max-width: 420px) {
  .tqe-ic svg { width: 23px; height: 23px; }
  .tqe-nm { font-size: 11.5px; }
}

/* ⚠ The primary action on the block, so it is sized like one. 15px was small enough that the
   owner noticed it on a phone before anything else — and on iOS a control under 16px also makes
   Safari zoom the viewport on focus, which is the same threshold topics-page.css fights for the
   search field. 17px clears both. */
.tqe-go {
  width: 100%; background: var(--tqe-ink); color: #fff; border: 0;
  border-radius: var(--radius-md, 10px); padding: 15px;
  /* ⛔⛔ NOT THE `font` SHORTHAND. It read `font: 600 15px/1.2 inherit`, and `inherit` is not a
     legal font-family inside the shorthand — so the browser threw the WHOLE declaration away and
     the button fell back to the UA default of 13.3px. That is why it looked tiny on a phone;
     changing 15 to 17 in the shorthand would have done nothing at all. Longhand, so each part
     stands on its own. */
  font-family: inherit; font-weight: 600; font-size: 17px; line-height: 1.2;
  letter-spacing: -0.005em; cursor: pointer;
}
@media (hover: hover) {
  .tqe-go:hover { filter: brightness(1.08); }
}
.tqe-note { margin: 9px 0 0; font-size: 11.5px; color: var(--text-tertiary, #9A9A9A); text-align: center; }

/* ⛔⛔ NO ONE-COLUMN FALLBACK. This rule was correct for the ORIGINAL entry tiles — wide rows
   with the name and score side by side, which did wrap badly at 360px. Against the square 2×2 it
   was a disaster: `aspect-ratio: 1` on a full-width tile makes a ~350px square, and four of them
   stack into 1400px of vertical wall. The owner saw it on both phones.
   ⚠ The lesson, not the fix: a responsive override survives the design it was written for. When
   a component is replaced, its media queries are part of the component.
   Two columns hold at every width — a 360px phone gives ~160px squares — and the 420px query
   above shrinks the icon and the name for exactly that case. */
@media (max-width: 460px) {
  .tq-entry { margin: 24px auto 32px; padding: 16px 14px 14px; }
  .tqe-grid { gap: 8px; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════
   ⛔⛔ THE DESKTOP LAYER. Owner, 2026-09-20: "the quiz bit on the topic page looks ridiculously
   huge on desktop ... when you go into quizzes or quiz menus, you get like a little phone shaped
   pop up screen ... it should never ever look like that. we dont have any other pages on the
   site that do that." Then, decisively: "the reading section has quizzes on desktop that look
   absolutely fine."
   ⚠⚠ THE CAUSE IS STRUCTURAL, NOT A WRONG NUMBER: before this block EVERY media query in this
   stylesheet was a `max-width` one. The arm was built phone-first and tuned phone-first all day,
   so "desktop" was just the phone layout with nothing overriding it — a 480px sheet marooned in
   a 1440px screen, and entry tiles that grow with their container and so reached 245px squares.
   ✅ THE NUMBERS COME FROM read.css, NOT FROM TASTE, because the owner named it as the thing
   that already looks right. Two conventions are borrowed:
     · `.read-wrap { max-width: 860px; padding: 2rem 1.5rem 4rem }` — the page is WIDE.
     · `.tq-choices { max-width: 480px; margin: 0 auto }` — but the interactive grids inside it
       are CAPPED AND CENTRED, so controls never stretch to the full width.
   That pairing is the whole trick, and it is why the reading quiz reads as a desktop page rather
   than either a phone strip or a stretched form. A wide surface with narrow, centred controls.
   ⚠ This is the ONLY `min-width` block in the file. Put desktop rules HERE rather than adding a
   second one, or the two will drift exactly as the duplicated .qpick-big blocks did.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
/* ⛔⛔ 481px, NOT 700 — THE TWO REGIMES MUST MEET WITH NO GAP. The full-bleed phone rules stop
   at 480px and the page treatment used to start at 700, so **481–699px got NEITHER** and fell
   back to the base styles: a 480px card with a dark scrim and rounded corners, i.e. exactly the
   modal look this replaced. That band is not hypothetical — it is a half-screen browser window, a
   small laptop, a landscape phone, and an iPad in split view.
   ⚠⚠ THE GENERAL TRAP: a `max-width` set and a `min-width` set do not cover the range unless
   their edges touch. Nothing warns you; the band simply renders the unstyled base. Whenever one
   of these numbers changes, change the other in the same edit.
   ⚠ The WIDE-SCREEN-only rules stay in the 700px block below — four tiles across needs the room.
   ⚠ No upper bound is needed: `max-width: 820px` on a centred column behaves the same at 1440 as
   at 3440, which is how `.read-wrap` has always worked.
   ⚠⚠ 480.02px, NOT 481px, AND THE FRACTION IS DELIBERATE. A viewport is not always an integer —
   browser zoom and fractional device-pixel ratios produce widths like 480.5, which satisfies
   NEITHER `max-width: 480px` NOR `min-width: 481px`, leaving a sub-pixel band that renders the
   unstyled base. 0.02px is the conventional epsilon: small enough that 480px itself still gets
   the phone rules, large enough to clear any real fractional width above it. */
@media (min-width: 480.02px) {
  /* ⛔⛔ THE QUIZ IS A PAGE, NOT A DIALOG — and since 2026-09-21 it genuinely IS one.
     Owner, 2026-09-20, after the first desktop pass only widened the card: "you get like a little
     phone shaped pop up screen ... it should never ever look like that. we dont have any other
     pages on the site that do that", and then "the reading section has quizzes on desktop that
     look absolutely fine ... the independent url thing only seems to matter for desktop."
     ⚠⚠ WIDENING THE CARD WAS NOT ENOUGH AND COULD NOT BE. Measured on the live site at 1280×551:
     the sheet was correctly 820px with its controls at 480, and it STILL read as a modal, because
     two things say "dialog" regardless of width — a dark scrim behind it, and being TALLER than
     the viewport so it scrolls inside itself (650 > 551) while the page sits frozen underneath.
     ✅ THE 820/480 PAIRING IS THE PART THAT WAS RIGHT AND IT IS UNCHANGED — a wide surface with a
     narrow centred column is `.read-wrap`'s model, the thing the owner pointed at. What has gone
     is the scrim, the self-scrolling box and the fill-the-viewport height, because the quiz is a
     view on the topic page now and the document does the scrolling.
     ⛔ `min-height: 100vh` IS DELIBERATELY ABSENT HERE. On a phone it survives (see the max-width
     block) because the flexible gaps need slack to grow into. On desktop the sheet is a wide
     column whose content is short, and stretching it to the viewport is what made the whole
     screen read as a modal in the first place. The two are not the same decision.
     ✅ AND THIS NEEDED NO NEW ROUTE. A real /quiz URL would have to solve where the topic DATA
     comes from: the quiz reads window.ThaiEarTopic off the page it is mounted on, and decision 24
     rejected a side-car JSON per unit. A view on the topic page has the data already. */
  .tq-page { padding: 0; }
  .tq-sheet {
    max-width: 820px; padding: 2rem 1.5rem 3rem;
    border: 0; border-radius: 0;
  }

  /* ⚠ …and the controls stay narrow and centred inside it — the .tq-choices convention. Without
     this a four-row picker stretches to 760px and each row becomes a letterbox. At 500px the cap
     is simply a no-op, which is why it can live in this block rather than the wide one. */
  .tq-fill > .tq-head,
  .tq-fill > .qpick,
  .tq-fill > .tq-minor,
  .tq-fill > .tq-results { width: 100%; max-width: 480px; margin-left: auto; margin-right: auto; }
  .qpick-big { gap: 11px; }
  .qpick-big button { padding: 11px 15px; }
  .qpick-big .nm { font-size: 15px; }
  .qpick-big .ds { font-size: 12px; }

  /* the sheet no longer needs to fill the viewport, so the flexible gaps stay modest */
  .tq-fill:not(.tq-fill-res) > .tq-gap { max-height: 30px; }
  /* ⛔⛔ THE SPACE UNDER THE TIGER, HALVED (owner, 2026-09-22): "the 'my results' sits right at
     the bottom of the page on mobile, almost falling off and not visible on first arrival ... there's
     a good centimeter or so between stay sharp (the css) and the line above the 'back to topic'
     pill. that space can maybe be halved."
     ⚠ IT WAS THREE THINGS STACKED, not one: this gap's 20px cap, plus .tq-minor's 10px margin and
     9px padding below - 39px, which at phone DPI is his centimetre. Halving any one of them alone
     would have moved barely 5px, which is how a spacing complaint gets "fixed" twice.
     ⚠ AND THE GAPS ARE FLEXIBLE, so this is a CAP and not a size: on a tall screen the slack is
     still distributed, and on the short screen he is describing the whole block now fits where it
     did not. That is the actual complaint - "My results" is only at the bottom edge because the
     content overflows and the auto margins have nothing left to centre with. */
  .tq-fill:not(.tq-fill-res) > .tq-gap-s { max-height: 10px; }
  .tq-mascot img { width: 168px; }

}

/* ⚠ WIDE SCREENS ONLY — rules that need the room, kept out of the 481px block on purpose.
   FOUR ACROSS, not 2×2: the same move read.css makes with `.read-path` (3 up, 2 under 800px) and
   `.final-grid` (4 up, 2 under 800px). A square tile's height is set by its width, so on a wide
   box a 2×2 becomes two rows of very large squares — that IS "ridiculously huge". Four in a row
   keeps each square small and fills the width honestly.
   ⛔ It must NOT drop to 481px: four tiles across a 500px window would be ~105px each, smaller
   than the 2×2 they replace, which is the opposite of the point. */
@media (min-width: 700px) {
  .tq-entry { width: 100%; max-width: 640px; }
  .tqe-grid { grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 14px; }
  .tqe-nm { font-size: 11.5px; }
  .tqe-ic svg { width: 24px; height: 24px; }
  /* ⭐ THREE ACROSS, AND BACK TO SQUARES — "so its uniform" (owner, 2026-09-21). The bar
     treatment is the PHONE answer to a tall viewport; a wide one has room for the same square
     tiles the four-quiz block uses, so every declaration the bar variant set on a phone is
     explicitly undone here rather than left to cascade. ⛔ Do not shorten this to the
     grid-template-columns line alone — the aspect-ratio and flex-direction overrides above are
     not media-scoped, so without these three tiles stay bar-shaped on desktop. */
  /* ⚠ `:not(.bars)` — a grammar unit read at large system text carries BOTH classes, and this
     block sits later in the file at equal specificity, so without it the desktop reset would
     silently put that unit back to squares. `.bars` is a text-inflation state, not a width one:
     it holds at every viewport, which is exactly why it cannot be undone in a width query. */
  .tq-entry.three:not(.bars) .tqe-grid { grid-template-columns: repeat(3, 1fr); }
  .tq-entry.three:not(.bars) .tqe-tile {
    aspect-ratio: 1 / 1;
    flex-direction: column;
    justify-content: center;
    gap: 6px;
    min-height: 0;
    padding: 8px 6px;
    text-align: center;
  }
  .tq-entry.three:not(.bars) .tqe-nm { font-size: 11.5px; }
}

/* ⭐ The นะ/คะ hint under the Builder's model answer (QUIZ_REFINEMENT.md W25). Same tint as the
   chip strip; a 12px/px gutter, never rem (CLAUDE.md trap 18). */
.t-hint { margin: 12px 0 2px; padding: 10px 12px; border-radius: var(--radius-md, 10px);
  background: var(--accent-light, #ECEBFA); color: var(--text-secondary, #5A5A5A);
  font-size: calc(13px * var(--tq-scale, 1)); line-height: 1.5; }
.t-hint b { color: var(--accent, #261B65); font-family: var(--font-thai, 'Sarabun', sans-serif); font-weight: 600; }
.t-hint b.t-hint-rom { font-family: inherit; }
.t-hint-k { display: block; font-size: calc(11px * var(--tq-scale, 1)); font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; color: var(--accent, #261B65); margin: 0 0 3px; }
.t-hint-tl { color: var(--text-tertiary, #9A9A9A); }
