/*! EliteXSAT design system | © 2026 EliteXSAT. All rights reserved. | https://elitexsat.com */

/* ==========================================================================
   Apex SAT Prep — Design System
   Clean, academic, trustworthy. Indigo primary + color-coded sections.
   Custom CSS (no build step). Tokens → Base → Layout → Components →
   App shell → Test engine → Animations → Responsive → Reduced motion.
   ========================================================================== */

/* ---------- Design Tokens ---------- */
:root {
  /* Brand (EliteXSAT crimson — matched to the logo) */
  --brand-50:  #fff1f2;
  --brand-100: #ffe1e3;
  --brand-200: #fec7cb;
  --brand-300: #fb9aa2;
  --brand-400: #f56673;
  --brand-500: #e83d4b;
  --brand-600: #d11d2b;
  --brand-700: #b0121f;
  --brand-800: #8f131d;
  --brand-900: #76151c;
  /* Crimson as a LINK colour, i.e. as body text — which is the one job --brand-600 cannot
     keep across both themes. It measures 5.00:1 on --bg here, but only 3.28:1 on the dark
     --bg, under the 4.5:1 floor a sentence-level link owes. Same split --exam-line and
     --ring already make: one value per theme, so neither has to compromise for the other. */
  --brand-link: #d11d2b;

  /* Neutrals (slate) */
  --ink:        #0f172a;
  --ink-2:      #1e293b;
  --muted:      #475569;
  /* Both of these carry real sentences — the 600-cap explanation, the trademark notice,
     the copyright line — so both are held to the 4.5:1 body-text floor on --bg AND on
     --surface. #64748b measured 4.45:1, which fails by 0.05; #94a3b8 measured 2.40:1,
     which is not readable at all. Darkened to the nearest passing value, hue kept. */
  /* Not 4.51 exactly: this token also sits on the 6% band washes the Reading shelf and the
     Question Bank panels share, which are darker than --surface and pushed it to 4.40 on
     the Hard tint. A threshold-exact grey fails the moment it lands on any tint, so this
     carries headroom instead: 4.53 on the darkest of them, 4.97 on plain --surface. */
  --muted-2:    #617188;
  --faint:      #647388;   /* 4.51:1 on --bg, 4.83:1 on --surface */
  --bg:         #f6f7fb;
  --surface:    #ffffff;
  --surface-2:  #f1f5f9;
  --surface-3:  #e9edf5;
  --border:     #e6e8f0;
  --border-2:   #d4d9e6;

  /* The Reading Book page only. It was tried on the exam passage pane too and pulled
     back out: during a timed test the page should stay neutral, with no tint the
     student has to read through. Long-form reading is the one place it earns its keep. */
  --paper:      #fbfaf7;
  --paper-edge: #f3efe6;

  /* Section colors */
  --rw:     #7c3aed;   /* Reading & Writing — violet */
  --rw-bg:  #f5f3ff;
  --math:   #0891b2;   /* Math — cyan */
  --math-bg:#ecfeff;

  /* Semantic */
  --ok:     #059669;
  --ok-bg:  #ecfdf5;
  --ok-bd:  #a7f3d0;
  --bad:    #e11d48;
  --bad-bg: #fff1f2;
  --bad-bd: #fecdd3;
  --warn:   #d97706;
  --warn-bg:#fffbeb;
  --warn-bd:#fde68a;   /* completes the ok/bad/warn triple; several rules hard-coded this */

  --pick:   #3b5bdb;   /* answer-choice "selected" blue (replaces the red) */
  /* Outline on an exam card nobody has opened yet. Same blue as --pick today, but its
     own token on purpose: they mean different things (a chosen answer vs. an untouched
     test) and only ever share a value by coincidence, so either can move alone. */
  --exam-line: #3b5bdb;
  --pick-bg:#eef2ff;

  /* Effects */
  /* The focus ring is a non-text indicator, so WCAG 2.2 SC 1.4.11 puts a 3:1 floor on it.
     At .26 the crimson painted #f3c4c8 over white — 1.55:1, barely visible, and a keyboard
     user could not tell where they were. The hue is unchanged; only the alpha moved. */
  --ring: 0 0 0 4px rgba(209, 29, 43, .64);   /* paints ~3.0:1 on --surface and --bg */
  --shadow-xs: 0 1px 2px rgba(15, 23, 42, .06);
  --shadow-sm: 0 1px 3px rgba(15, 23, 42, .08), 0 1px 2px rgba(15, 23, 42, .04);
  --shadow-md: 0 6px 16px -4px rgba(15, 23, 42, .12), 0 2px 6px -2px rgba(15, 23, 42, .07);
  --shadow-lg: 0 18px 40px -12px rgba(15, 23, 42, .18), 0 6px 14px -8px rgba(15, 23, 42, .10);
  --shadow-brand: 0 12px 28px -8px rgba(209, 29, 43, .42);

  /* Radii — one step per value. --r-xs was a second name for --r-sm's 7px, so the scale
     read as five steps while behaving as four; its single user now says --r-sm. */
  --r-sm: 7px;
  --r:    10px;
  --r-lg: 12px;
  --r-xl: 16px;
  /* Was --pill, which was a lie: a pill is fully rounded and this is a 10px corner. The
     segmented control and the range chips are the users, so the name says chip. Truly
     circular things take --circle. */
  --r-chip: 10px;
  --circle: 999px;   /* truly circular elements (spinner, dots) opt into this */

  /* Layout */
  /* The container widths were spread across the static pages as inline max-widths —
     1560, 960, 900, 820, 760, 660, 640 — several of them layered on top of .container,
     which meant editing this token changed nothing on those pages. The whole set lives
     here now. */
  --container: 1140px;
  --container-xl: 1560px;
  --gutter: 24px;
  /* The content column every text page shares: about, pricing, contact, changelog,
     privacy, terms. One value so the six line up exactly — open any two in adjacent tabs
     and the text starts and ends on the same pixel.
     This was briefly a character-based measure (70ch, ~755px) on the reasoning that
     about.html ran 90 characters to the line. Eljan chose a single uniform width instead:
     matching pages matter more here than the last few characters of line length. */
  --container-prose: 900px;
  --header-h: 102px;

  /* Type */
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-ui: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  --font-read: "Source Serif 4", Georgia, "Times New Roman", serif;

  /* Motion — One UI-inspired: fluid, soft landing, gentle overshoot */
  --ease: cubic-bezier(.4, .8, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-spring: cubic-bezier(.34, 1.46, .5, 1);   /* soft overshoot for entrances */
  --ease-smooth: cubic-bezier(.33, 1, .68, 1);
  --ease-settle: cubic-bezier(.26, 1.12, .42, 1);
  --t-fast: 170ms;
  --t: 280ms;
  --t-slow: 480ms;
  --t-linger: 615ms;
}

/* ---------- Reset / Base ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* 100vh on a phone is the height with the address bar HIDDEN, which is not the height
     the page opens at — so a short page started out taller than the visible area and
     scrolled for no reason, then jumped as the bar collapsed. dvh tracks the real box.
     The plain vh line stays first as the fallback for browsers without dvh; on any
     desktop the two are identical, so nothing there moves. */
  min-height: 100vh;
  min-height: 100dvh;
  overflow-x: hidden;
}

h1, h2, h3, h4 { line-height: 1.15; font-weight: 600; color: var(--ink); letter-spacing: -.01em; }
/* Headings only — never body prose. .h1/.h2 are clamp()ed, so their line breaks move
   with the viewport and a heading regularly dropped its last word onto a line of its
   own; balance spreads the words evenly across the lines instead.
   Deliberately NOT `p { text-wrap: pretty }`: that would reach the exam passages and
   the Reading Book chapters, where the browser has to re-break a very large block, and
   a timed test is the last place to spend layout time on an orphan. Browsers already
   cap balance at a few lines, so headings cost nothing. Unsupported browsers ignore it. */
h1, h2, h3, h4, .h1, .h2, .h3, .lead, .brand-slogan { text-wrap: balance; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; }
ul { list-style: none; padding: 0; }
:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--r-sm); }

/* The main content of every page is preceded by a header — on app.html a sidebar with six
   nav items, a theme control and a collapse button — and a keyboard user had to Tab past
   all of it on every single page. This link is the first thing in the tab order and the
   only thing that reveals itself on focus.
   z-index 300 sits above .toast-wrap (200), the highest value in the scale, so it cannot
   surface underneath anything. */
.skip-link {
  position: absolute; top: -80px; left: 12px; z-index: 300;
  padding: 11px 20px; border-radius: var(--r);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--border-2); box-shadow: var(--shadow-md);
  font-weight: 600; font-size: .92rem;
  transition: top var(--t-fast) var(--ease);
}
.skip-link:focus { top: 12px; }
/* The targets take tabindex="-1" so they can receive focus; without this every browser
   would draw a focus ring around the whole page body after the jump. */
[id="main"]:focus, #app:focus { outline: none; }

/* ---------- Layout helpers ---------- */
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.container-xl { max-width: var(--container-xl); }
/* Every text page. .legal opts in through the same token rather than this class, because
   those two pages carry their own padding — see the note there. */
.container-prose { max-width: var(--container-prose); }
.section { padding-block: 88px; }
.stack { display: flex; flex-direction: column; }
.row { display: flex; align-items: center; }
.between { justify-content: space-between; }
.center { justify-content: center; align-items: center; }
.wrap { flex-wrap: wrap; }
.gap-1 { gap: 6px; } .gap-2 { gap: 10px; } .gap-3 { gap: 16px; }
.gap-4 { gap: 24px; } .gap-5 { gap: 36px; } .gap-6 { gap: 52px; }
.grow { flex: 1; } .shrink-0 { flex-shrink: 0; }
.hidden { display: none !important; }
/* Text for assistive tech only — carries meaning that is otherwise shown by colour or
   shape alone. Not display:none, which screen readers skip. */
.a11y-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.text-center { text-align: center; }
.muted { color: var(--muted); }
.faint { color: var(--faint); }
.mono { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }

/* ---------- Typography scale ---------- */
.display {
  font-family: var(--font-display);
  font-weight: 500;
  font-optical-sizing: auto;
  letter-spacing: -.02em;
  line-height: 1.05;
  font-size: clamp(2.5rem, 1.6rem + 4.2vw, 4.4rem);
}
/* .h1 used to be the one heading left in the UI face while .h2 below it got the
   display face — the largest type on the page was the least characteristic. The
   display face now runs .display → .h1 → .h2 and stops; .h3 and .h4 are UI type.
   Set on the classes, never on the h1/h2 elements: views.js writes <h2 class="h3">
   in 21 places, and an element-level rule would drag those into the serif. */
.h1 { font-family: var(--font-display); font-weight: 500; font-size: clamp(1.9rem, 1.4rem + 2.2vw, 2.9rem); letter-spacing: -.02em; }
.h2 { font-family: var(--font-display); font-weight: 500; font-size: clamp(1.6rem, 1.2rem + 1.6vw, 2.3rem); letter-spacing: -.015em; }
.h3 { font-family: var(--font-ui); font-size: 1.3rem; }   /* explicit: the serif stops above this line */
.lead { font-size: clamp(1.05rem, 1rem + .4vw, 1.22rem); color: var(--muted); line-height: 1.6; }
.eyebrow {
  font-size: .8rem; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
  color: var(--brand-600);
}
.small { font-size: .875rem; }
.tiny { font-size: .78rem; }

/* ---------- Buttons ---------- */
.btn {
  --btn-bg: var(--surface);
  --btn-fg: var(--ink);
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  padding: 11px 20px; border-radius: var(--r-chip);
  font-weight: 600; font-size: .95rem; white-space: nowrap;
  background: var(--btn-bg); color: var(--btn-fg);
  border: 1px solid var(--border);
  transition: background var(--t-fast) var(--ease), color var(--t-fast),
              border-color var(--t-fast), box-shadow var(--t) var(--ease),
              transform var(--t-fast) var(--ease);
  user-select: none;
}
.btn:hover { box-shadow: var(--shadow-md); }
.btn:active { transform: translateY(1px); }
.btn svg { width: 18px; height: 18px; }
.btn-lg { padding: 14px 28px; font-size: 1.02rem; }
.btn-sm { padding: 8px 14px; font-size: .85rem; }
.btn-block { display: flex; width: 100%; }

.btn-primary {
  --btn-bg: var(--brand-600); --btn-fg: #fff; border-color: transparent;
  box-shadow: var(--shadow-sm);
}
.btn-primary:hover { --btn-bg: var(--brand-700); box-shadow: var(--shadow-brand); }
.btn-dark { --btn-bg: var(--ink); --btn-fg: #fff; border-color: transparent; }
.btn-dark:hover { --btn-bg: #000; }
.btn-ghost { --btn-bg: transparent; border-color: transparent; }
.btn-ghost:hover { --btn-bg: var(--surface-2); box-shadow: none; }
.btn-outline { --btn-bg: transparent; border-color: var(--border-2); }
.btn-outline:hover { --btn-bg: var(--surface); border-color: var(--brand-300); }
.btn-success { --btn-bg: var(--ok); --btn-fg: #fff; border-color: transparent; box-shadow: var(--shadow-sm); }
.btn-success:hover { --btn-bg: #047857; box-shadow: var(--shadow-md); }
.btn-danger { --btn-bg: var(--bad); --btn-fg: #fff; border-color: transparent; }
.btn[disabled] { opacity: .55; pointer-events: none; }

/* Footer social icons (landing page) — plain logos, NO box/bubble; colour-only hover, no movement */
.social-row { display: flex; align-items: center; gap: 20px; margin-top: 18px; }
/* The glyph stays 24px; the hit box is 44px. It measured 24x34 — the smallest target on
   the site, in a footer people reach for on a phone. The row's 20px gap keeps the two
   enlarged boxes clearly apart, so no negative margin is needed to fake the old spacing. */
.social-ico { display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; color: var(--ink-2); transition: color var(--t-fast); }
.social-ico:hover, .social-ico:active { color: var(--brand-600); }
.social-ico svg { width: 24px; height: 24px; }

/* Landing header — just the centered brand + slogan (top nav bar removed) */
.brand-header { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 20px; padding: clamp(64px, 13vh, 130px) 20px clamp(56px, 10vh, 96px); }
.brand-hero .brand-logo { height: clamp(74px, 11vw, 96px); }
/* The wordmark is the landing page's <h1> — the page had no h1 at all, so its outline
   started at h2. line-height and letter-spacing are pinned here because the global
   `h1, h2, h3, h4` rule would otherwise pull them to 1.15 / -.01em and reflow the mark. */
.brand-hero .brand-name { font-size: clamp(2.6rem, 6.2vw, 3.6rem); line-height: 1.6; letter-spacing: -.0056em; font-weight: 800; }
.brand-slogan { max-width: 620px; margin: 0; color: var(--ink-2); font-size: clamp(1.1rem, 2.4vw, 1.35rem); line-height: 1.5; }
.brand-slogan b { color: var(--ink); font-weight: 600; }
.brand-slogan .wink { opacity: .55; }
/* The slogan turns on one word; set that word in the display face so it carries.
   Fraunces has a smaller x-height than Inter, so it needs a nudge up to sit level
   with the sans around it — matching px would read as a smaller word. */
.brand-slogan i, .foot-slogan i {
  font-family: var(--font-display); font-style: italic; font-weight: 600;
  font-size: 1.1em; letter-spacing: -.005em;
}
.brand-slogan i { color: var(--brand-700); }
/* "mistakes" is the emotional centre of the slogan and it carried --brand-700 into the
   dark theme unchanged, measuring 2.72:1 on the near-black background. At 23.8px/600 it
   is not "large text", so it owes the full 4.5:1. Lifted like --exam-line above. */
[data-theme="dark"] .brand-slogan i, [data-theme="dark"] .foot-slogan i { color: #da555e; }
.brand-cta { margin-top: 10px; }

/* Feature card bullet list — 4 points per card */
.feat-list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 9px; }
.feat-list li { position: relative; padding-left: 20px; color: var(--ink-2); font-size: .96rem; line-height: 1.45; }
.feat-list li::before { content: ""; position: absolute; left: 4px; top: .55em; width: 6px; height: 6px; border-radius: 50%; background: var(--brand-600); }
/* icon + title side by side (title beside the logo) */
.feat-head { display: flex; align-items: center; gap: 14px; margin-bottom: 4px; }
.feat-head .feature-icon { margin: 0; flex: 0 0 auto; }
.feat-head .h3 { margin: 0; }

/* Answer keys — the four features are lettered A–D so the block reads as one
   multiple-choice question. Shape, radius and border are the exam engine's own
   .ch-key scaled up (28px/7px/1.5px → 46px/11px/2px), so it is the same object the
   student taps during a test. Each head is a link; hovering fills its key in. */
a.feat-head { cursor: pointer; }
.bubble {
  width: 46px; height: 46px; flex: 0 0 auto;
  border-radius: 11px; display: grid; place-items: center;
  /* Type matches .ch-key too, not just the box: UI face at 700, and the exam's own
     letter-to-box ratio (.88rem in 28px ≈ .503) held at 46px. The display face was
     tried here and read as a logo — this has to read as the key itself. */
  font-family: var(--font-ui); font-size: 1.44rem; font-weight: 700; line-height: 1;
  color: var(--bub); border: 2px solid var(--bub); background: transparent;
  transition: background var(--t) var(--ease), color var(--t) var(--ease);
}
a.feat-head:hover .bubble { background: var(--bub); color: #fff; }
a.feat-head:hover .h3 { color: var(--bub); }
.feat-head .h3 { transition: color var(--t-fast); }
.fh-a { --bub: var(--brand-600); }
.fh-b { --bub: var(--rw); }
.fh-c { --bub: var(--math); }
.fh-d { --bub: var(--ok); }

/* The four landing feature cards.
   Only the heading was ever a link, so the bullet list — the other ~85% of the card —
   was dead space that gave no sign anything here could be clicked. The ::after stretches
   the heading's own <a> over the whole card, which keeps the markup honest (the list stays
   outside the anchor, so a screen reader still reads "Digital SAT, link" and not four
   sentences of it) while making the entire surface the target.
   The hover selector carries `.reveal` on purpose, and it is not decoration: the shared
   `.card-hover:hover` further up has exactly the same specificity as `.reveal.in
   { transform: none }` far below it, and the later rule wins — a card that is both would
   take the shadow and never the lift. The extra class breaks that tie. */
.feat-card { position: relative;
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease), border-color var(--t); }
/* z-index is load-bearing, not tidiness: the ::after belongs to the heading, which is the
   card's FIRST child, so the bullet list that follows it paints on top and swallowed every
   click over the lower ~85% of the card — exactly the area this was added to cover.
   .feat-card is position:relative with z-index:auto, so it forms no stacking context and
   this 1 stays local to the card. */
.feat-card a.feat-head::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.feat-card.reveal:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--brand-200); }
[data-theme="dark"] .feat-card.reveal:hover { border-color: var(--brand-700); }

/* ---------- Cards & surfaces ---------- */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-sm);
}
.card-pad { padding: 26px; }
.card-hover {
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease),
              border-color var(--t);
  cursor: pointer;
}
.card-hover:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--brand-200); }

.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); }

/* ---------- Badges / chips ---------- */
.badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px; border-radius: var(--r-chip);
  font-size: .76rem; font-weight: 600; letter-spacing: .01em;
  background: var(--surface-2); color: var(--muted); border: 1px solid var(--border);
}
.badge svg { width: 13px; height: 13px; }
/* The .dex-badges pair that used to sit here sized the catalog card's "Version X" chip.
   That chip is gone — the month is the card's title now and the form letter is .tc-ver —
   and nothing else ever carried the class, so both rules were dead. */
.badge-ok   { background: var(--ok-bg); color: var(--ok); border-color: var(--ok-bd); }
.badge-bad  { background: var(--bad-bg); color: var(--bad); border-color: var(--bad-bd); }
.badge-brand{ background: var(--brand-50); color: var(--brand-700); border-color: var(--brand-200); }
.badge-warn { background: var(--warn-bg); color: var(--warn); border-color: #fde68a; }
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }

/* ---------- Forms ---------- */
.field { display: flex; flex-direction: column; gap: 7px; }
.label { font-size: .85rem; font-weight: 600; color: var(--ink-2); }
.input {
  width: 100%; padding: 12px 14px; border-radius: var(--r-sm);
  border: 1px solid var(--border-2); background: var(--surface);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
  font-size: .98rem;
}
.input::placeholder { color: var(--faint); }
.input:focus { outline: none; border-color: var(--brand-400); box-shadow: var(--ring); }
.input[aria-invalid="true"] { border-color: var(--bad); }
.hint { font-size: .8rem; color: var(--muted-2); }
.field-error { font-size: .8rem; color: var(--bad); font-weight: 500; }

/* ---------- Brand mark ----------
   The floating glass nav these lived under (.site-header / .nav-shell / .nav-links) is
   gone: the app navigates from the sidebar and landing.html has its own .brand-header.
   The brand lockup below is still shared by both, plus the sidebar and the auth card. */
.brand { display: inline-flex; align-items: center; gap: 12px; font-weight: 800; letter-spacing: -.02em; flex-shrink: 0; }
.brand-logo {
  /* The mark is square, so the aspect ratio is declared here rather than repeated as
     width/height on the eight places that build this tag in JS. The header keeps its
     height while the image decodes instead of reflowing around it. */
  height: 48px; width: auto; aspect-ratio: 1; display: block;
  filter: drop-shadow(0 6px 10px rgba(176, 18, 31, .28));
  transition: transform var(--t) var(--ease-spring);
}
.brand:hover .brand-logo { transform: rotate(-5deg) scale(1.07); }
.brand-name { font-size: 1.32rem; color: var(--ink); }
.brand-name b { color: var(--brand-600); font-weight: 800; }
.nav-link {
  padding: 9px 16px; border-radius: var(--r-chip); font-weight: 500; font-size: .96rem; color: var(--muted);
  transition: color var(--t-fast), background var(--t) var(--ease);
}
.nav-link:hover { color: var(--ink); background: var(--surface-2); }
.nav-link.active { color: var(--brand-700); background: var(--brand-50); }
.icon-btn {
  width: 46px; height: 46px; border-radius: 50%; display: grid; place-items: center; flex-shrink: 0;
  border: 1px solid var(--border-2); color: var(--ink-2); background: var(--surface);
  transition: transform var(--t) var(--ease-spring), border-color var(--t-fast), color var(--t-fast), box-shadow var(--t);
}
.icon-btn:hover { border-color: var(--brand-300); color: var(--brand-600); transform: translateY(-2px); box-shadow: var(--shadow-md); }
.icon-btn svg { width: 21px; height: 21px; }

/* ---------- The dashed rule ---------- */
/* One definition, every user. The pattern is a single 40px tile with a 26px dash
   centred in it, laid down with `round` so a whole number of tiles fits the width and
   both ends finish on the same margin — a repeating-linear-gradient would tile from
   the left and leave a half-cut dash on the right.
   Longhands on purpose: the `background` shorthand resets size and repeat, so any rule
   that restates this with the shorthand silently undoes it.
   It marks one thing throughout — where content stops and fine print or the next
   question begins. .qr-rule sits inside a test at full strength; everything else is
   .38 so it reads as texture rather than an alert. */
.qr-rule,
.rule,
.site-footer::before,
.lib-legal::before,
.qb-legal::before,
.foot-bar::before {
  background-image: linear-gradient(90deg, transparent 0 17.5%, var(--rule-c, var(--border-2)) 17.5% 82.5%, transparent 82.5%);
  background-size: var(--rule-tile, 32px) 100%;
  background-repeat: round;
}
/* Dividers are hairlines in the border grey, not brand crimson — they separate, they
   do not announce. --border-2 rather than --border: a dashed line lays down about a
   third less ink than a solid one, so it needs the next step up to read as present.
   Opacity is gone; the colour carries the weight now. */
.rule { height: 1px; border: none; flex-shrink: 0; }
.rule-center { width: min(640px, 92%); margin: 26px auto 0; }

/* Every page-foot variant draws it as a top edge. .foot-bar is the copyright strip the
   marketing pages used to set with an inline border-top. */
.site-footer::before,
.lib-legal::before,
.qb-legal::before,
.foot-bar::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px; }
.foot-bar { position: relative; margin-top: 40px; padding-top: 24px; }

/* The College Board trademark notice, repeated at the foot of all five static pages.
   It carried `text-align:justify` inline on every one of them. Justify with no
   hyphenation widens the WORD GAPS to fill each line, and this paragraph is a wall of
   long words — "registered", "affiliation", "endorsement" — so a handful of gaps
   absorbed all the slack and vertical rivers opened straight through it. At .tiny/.faint
   size that is the least readable text on the page getting the least readable setting.
   Left-aligned here, and the five inline copies retired with it. */
.foot-trademark { margin-top: 14px; line-height: 1.65; }

.site-footer { position: relative; border-top: none; background: var(--surface); padding-block: 56px 36px; }
.footer-grid { display: grid; grid-template-columns: 1.6fr repeat(3, 1fr); gap: 40px; }
.footer-grid h4 { font-size: .82rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted-2); margin-bottom: 14px; }
.footer-grid a { display: block; padding: 5px 0; color: var(--muted); font-size: .92rem; transition: color var(--t-fast); }
.footer-grid a:hover { color: var(--brand-600); }

/* The footer's first column — logo, wordmark, slogan — is the same markup on all five
   marketing pages, and it was carrying four inline styles on each of them: twenty copies
   of the same four numbers. Editing the footer meant editing five files and hoping they
   stayed in step. */
.foot-brand { margin-bottom: 14px; }
.foot-brand .brand-logo { height: 54px; }
.foot-slogan { max-width: 300px; }
.foot-wink { opacity: .6; }

/* ---------- Static marketing pages (about / pricing / contact / changelog) ---------- */
/* These four are the same document shape — back link, title, lead, then prose — and each
   one spelled that rhythm out inline, with slightly different numbers every time. Stated
   once here so they cannot drift apart again.
   The :not() list is load-bearing: `.static-page p` is more specific than `.lead`, so
   without it this rule would overwrite the lead's own colour and size. */
.static-page > .h1 { margin: 0 0 16px; }
.static-page > .lead { margin: 0 0 32px; }
.static-page h3, .static-page .h3 { margin: 0 0 10px; }
.static-page p:not(.lead):not(.tiny) { margin: 0 0 28px; color: var(--ink-2); line-height: 1.7; }
/* Repeats the :not() chain deliberately. Written as a plain `p:last-child` it scored one
   class lower than the rule above and lost — a dead rule that looked like a live one. */
.static-page p:not(.lead):not(.tiny):last-child { margin-bottom: 0; }
/* Centred variant — contact.html */
.static-page.text-center > .lead { margin-inline: auto; }

/* "← Back to home". Layout only: the marketing pages colour it .tiny.faint and the legal
   pages colour it crimson, and those stay each page's business. */
.backlink { display: inline-block; margin-bottom: 22px; }

/* The address line under the contact buttons. */
.contact-handles { margin: 18px auto 0; }

/* A centred heading + lead above a section's real content. landing.html is the only user
   today; it is a class rather than three inline rules because the next page that grows a
   section head should look identical without anyone having to copy numbers across. */
.section-intro { max-width: 760px; margin: 0 auto 40px; }
.section-intro .h2 { margin: 0 0 14px; }

/* app.html's <noscript>. It used a system-ui font inline on the theory that the stylesheet
   might not be there — but the stylesheet is what is rendering this rule, so if it were
   missing the whole message would be unstyled anyway. Plain page type instead. */
.noscript-note { padding: 40px; text-align: center; }

/* Telegram and Instagram brand colours, fed to .btn through its own --btn-bg/--btn-fg
   hooks. These are fixed external brand values, not theme colours, so they do not belong
   in :root next to tokens that flip between light and dark — but they are constants, and
   constants do not belong inline on one page either. */
.btn-telegram  { --btn-bg: #229ED9; --btn-fg: #fff; border-color: transparent; }
.btn-instagram { --btn-bg: #E1306C; --btn-fg: #fff; border-color: transparent; }

/* ---------- Avatar ---------- */
.avatar {
  width: 38px; height: 38px; border-radius: 11px; flex-shrink: 0;
  background: linear-gradient(135deg, var(--brand-400), var(--brand-700));
  color: #fff; display: grid; place-items: center; font-weight: 600; font-size: .9rem;
}
.avatar-lg { width: 64px; height: 64px; font-size: 1.4rem; }
.set-container .avatar-lg { width: 48px; height: 48px; font-size: 1.05rem; border-radius: 12px; }
.avatar-sa { width: 38px; height: 38px; font-size: .82rem; }   /* the sidebar account tile */
.avatar-run { width: 30px; height: 30px; border-radius: 9px; font-size: .78rem; }   /* the runners' bottom bar */
/* With a photo the gradient behind the initials would show through the corners, so it
   goes; object-fit keeps a non-square source from stretching even though the file was
   already cropped square on upload — a photo restored from an older record may not be. */
.avatar.has-photo { background: none; overflow: hidden; }
.avatar.has-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Country flags. Images rather than emoji: regional-indicator flag emoji do not render
   on Windows, where they fall back to the two letters. The border keeps a white flag
   (Japan, Poland) from vanishing into a white card. */
.flag { display: inline-block; width: 20px; height: 15px; object-fit: cover; vertical-align: -2px;
  border-radius: 2px; border: 1px solid var(--border); margin-right: 7px; flex: none; }
/* Shown instead of the image when the CDN is unreachable. */
.flag-code { display: inline-block; font-size: .7rem; font-weight: 700; letter-spacing: .03em;
  color: var(--muted-2); margin-right: 7px; }
.set-country { display: flex; align-items: center; gap: 0; }
.set-country .input { padding: 7px 10px; font-size: .92rem; max-width: 260px; }

/* ---------- Leaderboard ---------- */
/* One row shape for every place, #1 through the end of the list — no separate podium
   card for the top three. Rank, meter length and position already say who is ahead;
   a special card for three of the ten rows said it twice and looked like a different
   component bolted onto the same board. */
/* Each row is its own card now, not a divider-separated slice of one big card — a
   shared border between two rows read as one fused block once the top three also had
   their own outline color to show, so a real gap replaces the border-top divider. */
/* Sized for the XP board's 50 places: a 36px avatar and modest padding put a row at
   54px, so the list is a scan rather than a scroll. */
.lb-list { display: flex; flex-direction: column; gap: 6px; background: transparent; border: none; box-shadow: none; }
.lb-row { display: flex; align-items: center; gap: 12px; padding: 8px 16px; font-size: 1rem;
  border: 1px solid var(--border); border-radius: var(--r); background: var(--surface); }
.lb-row.is-me { background: var(--brand-50); border-color: var(--brand-200); }
.lb-row .avatar-sa { width: 36px; height: 36px; font-size: .8rem; }
.lb-row .badge { padding-top: 1px; padding-bottom: 1px; }
/* The place, with any movement since yesterday stacked under it — in the same column,
   so the arrow costs the name no width on a phone. */
.lb-rank { flex: none; display: inline-flex; flex-direction: column; align-items: center; justify-content: center; width: 28px;
  line-height: 1.15; font-weight: 700; color: var(--muted-2); font-variant-numeric: tabular-nums; }
/* Mixed toward --ink so the green and red clear 4.5:1 at this size in either theme;
   the arrow carries the direction for anyone who cannot tell the two apart. */
.lb-move { display: block; margin-top: 2px; font-size: .66rem; font-weight: 700; line-height: 1; white-space: nowrap; }
.lb-move.up { color: color-mix(in srgb, var(--ok) 78%, var(--ink)); }
.lb-move.down { color: color-mix(in srgb, var(--bad) 78%, var(--ink)); }
.lb-move.new { color: var(--muted-2); font-weight: 600; }
/* "120 XP to #6" on your own row. The footnote under the board says the same thing,
   so on a phone, where the name needs the room, this one steps aside. */
.lb-gap { flex: none; font-size: .82rem; font-weight: 600; color: var(--brand-link); white-space: nowrap; }
@media (max-width: 620px) { .lb-gap { display: none; } }
.lb-row .lb-name { flex: 1; min-width: 0; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A fixed column, right-aligned, so the figures line up down the board. */
.lb-row .lb-days { flex: none; display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; min-width: 80px;
  font-weight: 700; color: var(--warn); font-variant-numeric: tabular-nums; }
.lb-row .lb-days svg { width: 16px; height: 16px; }
/* The only ornament on an otherwise identical row: a crown standing in for the "1" a
   little larger than the digit it replaces, the same gold/silver/bronze read on the
   rank number for 2nd and 3rd, a faint wash across the whole row, and that color
   carried by the row's own border instead of the neutral default — :not(.is-me) so
   being "you" always wins over a medal treatment on the same row. The border stays
   1px like every other row's, so the top three are not a pixel taller. */
.lb-crown { display: block; width: 18px; height: 18px; color: #b8952f; }
.lb-list > .lb-row:nth-child(1) .lb-rank { color: #b8952f; }
.lb-list > .lb-row:nth-child(2) .lb-rank { color: #7d8a9d; }
.lb-list > .lb-row:nth-child(3) .lb-rank { color: #a4622c; }
.lb-list > .lb-row:nth-child(1):not(.is-me) { background: color-mix(in srgb, #b8952f 4%, var(--surface)); border-color: #b8952f; }
.lb-list > .lb-row:nth-child(2):not(.is-me) { background: color-mix(in srgb, #7d8a9d 4%, var(--surface)); border-color: #7d8a9d; }
.lb-list > .lb-row:nth-child(3):not(.is-me) { background: color-mix(in srgb, #a4622c 4%, var(--surface)); border-color: #a4622c; }
/* A meter bar, sized against the board leader's number — the same information the
   digits already carry, just readable at a glance while scanning. */
.lb-meter { flex: none; display: inline-block; width: 170px; height: 5px;
  background: var(--surface-3); border-radius: var(--circle); overflow: hidden; }
/* The meter plus the rank/avatar/figure columns no longer fits a phone-width row
   without crushing the name to nothing, so it gives back width below 480px —
   the same floor the segmented controls already shrink their padding at. */
@media (max-width: 480px) {
  .lb-row { gap: 8px; padding-left: 10px; padding-right: 10px; }
  .lb-meter { width: 44px; }
  .lb-row .lb-days { min-width: 0; }
}
.lb-meter-fill { display: block; height: 100%; width: 0; background: var(--brand-500);
  border-radius: var(--circle); transition: width .6s var(--ease-out); }
.lb-row.is-me .lb-meter-fill { background: var(--brand-600); }

/* Past the top ten, "you" used to be a footnote sentence below the fold. This pins
   your row to the bottom of the screen instead, styled like the list above it, so
   finding your own rank never depends on how far you have to scroll. */
.lb-pin { position: sticky; bottom: max(16px, env(safe-area-inset-bottom, 0px)); z-index: 5; margin-top: 16px; }
.lb-pin-card { display: flex; align-items: center; gap: 12px; padding: 12px 16px;
  background: var(--brand-50); border: 1px solid var(--brand-200); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg); }
.lb-pin-card .lb-rank { color: var(--brand-link); font-weight: 800; }
.lb-pin-card .lb-name { flex: 1; min-width: 0; font-weight: 700; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-pin-card .lb-days { flex: none; display: inline-flex; align-items: center; gap: 6px; font-weight: 800;
  color: var(--warn); font-variant-numeric: tabular-nums; }
.lb-pin-card .lb-days svg { width: 16px; height: 16px; }

/* Streak block on the Account page. Grey until today is practised, amber once it is —
   the same rule the sidebar chip follows, so the colour means "done" in both places. */
.streak-panel { display: flex; align-items: center; gap: 12px; padding-bottom: 6px; }
.streak-panel-flame { display: inline-flex; flex: none; color: var(--muted-2); transition: color var(--t); }
.streak-panel-flame svg { width: 30px; height: 30px; }
.streak-panel.lit .streak-panel-flame { color: var(--warn); }
.streak-panel.streak-out .streak-panel-flame { color: var(--muted-2); opacity: .8; }
.streak-panel-num { font-family: var(--font-display); font-size: 1.9rem; font-weight: 500; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
.streak-panel-lab { font-size: .84rem; color: var(--muted-2); margin-top: 3px; }

/* Photo row at the top of the Account card. The controls sit beside the picture rather
   than under it — the avatar is only 64px tall, so a stacked label + buttons + hint cost
   three rows of height to say what the button already says. */
.set-photo { display: flex; align-items: center; gap: 12px;
  padding-bottom: 8px; margin-bottom: 0; }
.set-photo .avatar-lg { border: 1px solid var(--border); }
.set-photo-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }

/* ---------- Progress ---------- */
.bar { height: 8px; border-radius: var(--r-chip); background: var(--surface-3); overflow: hidden; }
.bar > span { display: block; height: 100%; border-radius: var(--r-chip); background: var(--brand-500);
  transition: width var(--t-slow) var(--ease-out); }
.bar.rw > span { background: var(--rw); }
.bar.math > span { background: var(--math); }
.bar.ok > span { background: var(--ok); }

/* ---------- Toast ---------- */
.toast-wrap { position: fixed; bottom: max(22px, env(safe-area-inset-bottom, 0px)); left: 50%; transform: translateX(-50%); z-index: 200;
  display: flex; flex-direction: column; gap: 10px; align-items: center; pointer-events: none; }
.toast {
  pointer-events: auto;
  display: flex; align-items: center; gap: 11px; padding: 12px 18px;
  background: var(--ink); color: #fff; border-radius: var(--r-chip);
  box-shadow: var(--shadow-lg); font-size: .92rem; font-weight: 500;
  animation: toastIn var(--t) var(--ease-out);
}
.toast svg { width: 18px; height: 18px; flex-shrink: 0; }
.toast.ok svg { color: #6ee7b7; }
.toast.bad svg { color: #fda4af; }

/* ---------- Center notice (auto-advance) ----------
   The regular toast sits bottom-center, easy to miss right when the screen just changed
   underneath it. This one sits dead center instead, for the one case that needs it. No
   backdrop — it must not block the new module's questions, only announce over them
   briefly. */
.center-notice-wrap { position: fixed; inset: 0; z-index: 210; display: grid; place-items: center;
  pointer-events: none; padding: 24px; }
.center-notice {
  display: flex; align-items: center; gap: 12px; padding: 16px 26px;
  background: var(--ink); color: #fff; border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg); font-size: 1rem; font-weight: 600;
  animation: centerNoticeIn var(--t) var(--ease-out);
}
.center-notice svg { width: 20px; height: 20px; flex-shrink: 0; color: #6ee7b7; }
@keyframes centerNoticeIn { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: scale(1); } }

/* ---------- Modal ---------- */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 100; background: rgba(15, 23, 42, .5);
  backdrop-filter: blur(3px); display: grid; place-items: center; padding: 24px;
  animation: fadeIn var(--t) var(--ease);
}
.modal {
  background: var(--surface); border-radius: var(--r-xl); box-shadow: var(--shadow-lg);
  width: 100%; max-width: 440px; padding: 30px; animation: modalIn var(--t) var(--ease-out);
}

/* Full-question review popup (opened from the answer-key tiles) — ~80% of the screen.
   svh, not dvh: this is a FIXED height, and dvh would resize the popup underneath the
   student every time the address bar moved. svh is the viewport with the browser UI at
   its largest, so the popup always fits and never changes size once open. Identical to
   vh on desktop. */
/* Near-full-screen, because this is where a student studies a mistake: the question, its
   figure, the four choices and the explanation all have to be on screen AT ONCE. Capped
   at 1800px so the two columns below stay a readable line length on a wide monitor. */
.modal.modal-review { width: 96vw; height: 94vh; height: 94svh; max-width: 1800px; padding: 20px 24px; display: flex; flex-direction: column; }

/* A modal's action row is its last child. Let it wrap instead of overflowing: a
   three-way dialog (the exam exit prompt) does not fit on one line, and on a phone
   no dialog's buttons do. Stacked order stays Keep / Save / Delete, so the
   destructive choice is last and hardest to hit by accident. */
.modal > .row:last-child { flex-wrap: wrap; }
@media (max-width: 560px) {
  .modal > .row:last-child { flex-direction: column; align-items: stretch; }
  .modal > .row:last-child .btn { width: 100%; justify-content: center; }
}
/* The BODY no longer scrolls — each column does, so the figure on the left stays put
   while the explanation on the right is read.
   🔴 A flex column, and .rv-split takes the REMAINING height (flex:1 + min-height:0) —
   NOT height:100%. The meta line is a sibling above it, so 100% is the body's full height
   PLUS that line: the columns overhung by ~50px and `overflow:hidden` quietly cut the
   bottom off both, including content their own scrollbars could then never reach. */
.modal-review .modal-body { flex: 1 1 auto; overflow: hidden; min-height: 0;
  display: flex; flex-direction: column; }
/* 🔴 The split is NOT the body's child: views.js repaints it inside a [data-rv-body]
   wrapper. A plain block there grew to the full content height, so .rv-split's flex:1
   did nothing, neither column ever overflowed, and the body's overflow:hidden cut off
   choices C/D with no scrollbar (2026-10-14). The wrapper must pass the height down. */
.modal-review [data-rv-body] { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.rv-split { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: 0 28px; flex: 1 1 auto; min-height: 0; }
.rv-col { min-height: 0; overflow-y: auto; padding-bottom: 10px; }
.rv-col-q { padding-right: 26px; border-right: 1px solid var(--border); }
/* ONE scroller per column. The passage keeps its own height cap only when stacked
   (below), where it shares a scroller with everything else. */
.rv-split .rv-passage { max-height: none; overflow: visible; }
.rv-meta { display: flex; align-items: center; gap: 8px 14px; flex-wrap: wrap; font-size: .82rem; color: var(--muted-2); padding-bottom: 12px; margin-bottom: 16px; border-bottom: 1px solid var(--border); }
.rv-meta .rv-state { font-weight: 700; }
.rv-passage { background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; margin-bottom: 18px; line-height: 1.6; max-height: 42vh; max-height: 42svh; overflow-y: auto; }
.rv-prompt { font-size: 1.05rem; line-height: 1.6; margin-bottom: 20px; }
.rv-prompt svg, .rv-prompt img, .rv-prompt table { max-width: 100%; height: auto; }
.rv-opts { display: flex; flex-direction: column; gap: 10px; margin-bottom: 22px; }
.rv-opt { display: flex; align-items: center; gap: 12px; padding: 11px 14px; border: 1.5px solid var(--border); border-radius: 12px; background: var(--surface); }
.rv-opt .rv-badge { flex: none; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; font-weight: 700; font-size: .95rem; background: var(--surface-2); color: var(--muted-2); border: 1px solid var(--border); }
.rv-opt .rv-badge svg { width: 17px; height: 17px; }
.rv-opt .rv-opt-txt { flex: 1; min-width: 0; line-height: 1.5; }
.rv-opt .rv-lab { font-size: .68rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted-2); margin-bottom: 2px; }
.rv-opt .rv-grid-ans { font-size: 1.1rem; font-weight: 700; }
.rv-opt .rv-tag { flex: none; font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 3px 9px; border-radius: 999px; }
.rv-opt.ok { border-color: var(--ok); background: var(--ok-bg); }
.rv-opt.ok .rv-badge { background: var(--ok); color: #fff; border-color: var(--ok); }
.rv-opt.ok .rv-tag { background: var(--ok); color: #fff; }
.rv-opt.bad { border-color: var(--bad); background: var(--bad-bg); }
.rv-opt.bad .rv-badge { background: var(--bad); color: #fff; border-color: var(--bad); }
.rv-opt.bad .rv-tag { background: var(--bad); color: #fff; }
.rv-explain { border-top: 1px solid var(--border); padding-top: 16px; line-height: 1.6; }
.rv-explain h4 { margin: 0 0 8px; font-size: 1.02rem; }
.rv-exp-summary { margin-bottom: 6px; }
.rv-why { margin-top: 14px; display: flex; flex-direction: column; gap: 8px; }
.rv-why-title { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; font-weight: 700; color: var(--muted-2); }
.rv-why-row { display: flex; gap: 10px; align-items: flex-start; padding: 9px 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2); }
.rv-why-row.ok { border-color: var(--ok); background: var(--ok-bg); }
.rv-why-badge { flex: none; display: inline-flex; align-items: center; gap: 3px; font-weight: 800; font-size: .82rem; min-width: 30px; }
.rv-why-badge svg { width: 15px; height: 15px; }
.rv-why-row.ok .rv-why-badge { color: var(--ok); }
.rv-why-row.bad .rv-why-badge { color: var(--bad); }
.rv-why-txt { line-height: 1.5; font-size: .92rem; }

/* Not enough width for two columns — stack them, and hand the scrolling back to the body
   so there is ONE scroller rather than two short ones fighting over a phone screen. */
@media (max-width: 900px) {
  /* flex:none — stacked, the split takes its natural height and the BODY scrolls it. */
  .rv-split { grid-template-columns: 1fr; gap: 0; flex: 0 0 auto; }
  .rv-col { overflow: visible; padding-bottom: 0; }
  .rv-col-q { padding-right: 0; border-right: none; border-bottom: 1px solid var(--border);
    margin-bottom: 18px; padding-bottom: 4px; }
  .modal-review .modal-body { overflow-y: auto; padding-right: 8px; }
  .modal-review [data-rv-body] { flex: 0 0 auto; }
  .rv-split .rv-passage { max-height: 42vh; max-height: 42svh; overflow-y: auto; }
}
/* The phone case, which is where the old vh actually bit: at 88vh the popup was taller
   than the visible area whenever the address bar was showing, so its footer — and the
   button that closes it — sat below the fold. */
@media (max-width: 720px) {
  .modal.modal-review { width: 94vw; height: 88vh; height: 88svh; padding: 16px; }
  .rv-passage, .rv-split .rv-passage { max-height: 34vh; max-height: 34svh; }
}

/* ---------- App shell ---------- */
/* Same dvh swap as body, and this is the one that showed: the app shell is what a student
   sits inside for a whole module, so every address-bar collapse re-ran this calc and
   shifted the page under them. vh first as the fallback; identical on desktop. */
.app-main { min-height: calc(100vh - var(--header-h));
            min-height: calc(100dvh - var(--header-h)); padding-block: 40px 80px; }
.page-head { margin-bottom: 28px; }
/* Reading library header — text left, band filter right (slim two-column, like the reader) */
.lib-head { display: flex; justify-content: space-between; align-items: center; gap: 22px 28px; flex-wrap: wrap; }
.lib-head-text { flex: 1 1 360px; min-width: 0; }
.lib-head-text .lead { margin-bottom: 0; }
.lib-head-filter { flex-shrink: 0; }
/* No colour of its own — Reading and Vocabulary take the same ink as every other page
   title from .qb-h1-brand, and their icons the same crimson from .qb-h1 svg. */
.lib-title { margin: 0; display: flex; align-items: center; gap: .32em; }

/* SAT Vocab decks — plan-style columns (Noun / Verb / Adjective), --dc = accent, --dcb = tint */
.svd { padding: 0; overflow: hidden; display: flex; flex-direction: column; border-top: 3px solid var(--dc); }
.svd-head { text-align: center; padding: 20px 18px 16px; background: var(--dcb); }
.svd-title { margin: 0; font-size: 1.35rem; font-weight: 800; letter-spacing: -.01em; color: var(--dc); }
.svd-count { margin-top: 4px; font-size: .85rem; font-weight: 600; color: var(--muted); }
/* A deck card is a header and a button, nothing between them. The word-chip preview
   (.svd-body / .svd-wide / .svd-more) was removed along with these rules: a wall of
   chips is not something a student can study from, and it pushed Start Practice down
   the page. .svd-chip survives because the Transitions list still builds its clickable
   words from it. */
.svd-chip { font-size: .84rem; font-weight: 600; color: var(--ink-2); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 999px; padding: 5px 11px; }
/* Start Practice sits left, the deck's progress fills the space to its right.
   margin-top:auto pins it to the bottom: with the body gone nothing else grows to fill
   the card, so without it the button would ride up under the header whenever a
   neighbouring card's title wrapped and stretched the whole row. */
.svd-foot { padding: 18px; border-top: 1px solid var(--border); margin-top: auto;
            display: flex; align-items: center; gap: 14px; }
.svd-foot .btn { min-width: 160px; flex: none; }
.svd-prog { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.svd-prog-n { font-size: .78rem; font-weight: 700; color: var(--muted-2); text-align: right; }
/* The fill takes the deck's own colour, the same --dc the card header uses. */
.svd-prog .bar > span { background: var(--dc, var(--brand-600)); }
@media (max-width: 520px) {
  .svd-foot { flex-direction: column; align-items: stretch; }
  .svd-foot .btn { width: 100%; }
  .svd-prog-n { text-align: center; }
}
/* Runs the full content width rather than a narrow measure. Boxed at 820px it read as
   a stray column with the rule stopping short of everything above it; spread out, the
   notice and its rule line up with the cards they belong to. */
.lib-legal { position: relative; margin-top: 46px; padding-top: 20px; color: var(--muted-2); font-size: .82rem; line-height: 1.65; }
.lib-legal p { margin: 0 0 6px; }
.lib-legal-copy { color: var(--faint); font-weight: 500; margin-bottom: 0; }
@media (max-width: 760px) { .lib-head { align-items: stretch; } .lib-head-filter { width: 100%; } }
.grid { display: grid; gap: 22px; }
.grid > * { min-width: 0; }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1100px) { .grid-4 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 820px)  { .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px)  { .grid-4 { grid-template-columns: minmax(0, 1fr); } }
.grid-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@media (max-width: 1500px) { .grid-5 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 1100px) { .grid-5 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 820px)  { .grid-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px)  { .grid-5 { grid-template-columns: minmax(0, 1fr); } }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-stats { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@media (max-width: 1240px) { .grid-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* ---- The dashboard's headline strip ----
   One panel of cells instead of a grid of separate cards (Eljan, 2026-09-14: compact).
   The hairlines are the 1px grid gap showing the border colour through, so they follow
   the cells wherever the row wraps. The per-card basis line ("vs your previous sitting",
   "all time"), the "Latest" word and the icons go — the basis is said once under the
   strip — and the numbers step down a size. Cell counts: 6 (full view), 3 (one section: no Accuracy there),
   3 (Question Bank); a count never leaves a lone cell on a row. */
.stat-strip { display: grid; gap: 1px; background: var(--border); border: 1px solid var(--border);
  border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow-sm); margin-bottom: 10px; }
.stat-strip-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.stat-strip-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 1180px) { .stat-strip-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .stat-strip-6 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
                             .stat-strip-3 { grid-template-columns: minmax(0, 1fr); } }
.stat-strip > .stat-card { border: 0; border-radius: 0; box-shadow: none; padding: 14px 16px; min-width: 0; }
.stat-strip .stat-card .row.between > svg { display: none; }
.stat-strip .stat-card .label { font-size: .76rem; }
.stat-strip .stat-card .value { font-size: 1.6rem; margin-top: 4px; }
.stat-strip .sc-lead { gap: 6px; }
.stat-strip .sc-delta { font-size: .76rem; }
.stat-strip .sc-foot { margin-top: 5px; gap: 4px 10px; }
.stat-strip .sc-foot-b { font-size: .74rem; }
.stat-strip .sc-latest, .stat-strip .sc-sub { display: none; }
.stat-strip .cw-bar { margin-top: 8px; height: 5px; }
.stat-strip .cw-counts { margin-top: 6px; gap: 3px 10px; }
.stat-strip .cw-n { font-size: .74rem; gap: 4px; }
.stat-strip .cw-n b { font-size: .86rem; }
.stat-strip .cw-n .ac-badge { width: 15px; height: 15px; }
.stat-strip .cw-n .ac-badge svg { width: 9px; height: 9px; }
/* In a strip cell the words go to the hover title and the screen reader — the tick, the
   cross and the grey dot carry them — so correct · wrong · blank fit on one line. */
.stat-strip .cw-word { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.stat-strip .cw-counts { flex-wrap: nowrap; gap: 10px; }

.stat-card { padding: 22px; }
.stat-card .label { font-size: .82rem; color: var(--muted-2); font-weight: 500; }
.stat-card .value { font-family: var(--font-display); font-size: 2.1rem; font-weight: 500; line-height: 1; margin-top: 8px; }
.stat-card .value small { font-size: 1rem; color: var(--faint); font-family: var(--font-ui); }
/* Score card: the latest score leads, its change against the sitting before it sits
   beside it, and Best / Average step down to a footnote. Best-and-Average side by side
   gave equal weight to a souvenir and to a number that describes no single moment. */
.sc-lead { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.sc-delta { font-size: .84rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* --ok reads 3.8:1 on a light card, and 13px bold needs 4.5 — so the rise is drawn a
   shade deeper in light mode and left bright on dark, where it already clears it. */
.sc-delta-up { color: #047857; }
:root[data-theme="dark"] .sc-delta-up { color: #34d399; }
.sc-delta-down { color: var(--bad); }
/* --bad is 3.7:1 on a dark card — the drop is lifted the same way the rise is deepened. */
:root[data-theme="dark"] .sc-delta-down { color: #fb7185; }
.sc-delta-flat, .sc-delta-none { color: var(--faint); }
.sc-foot { display: flex; align-items: baseline; gap: 14px; margin-top: 8px; flex-wrap: wrap; }
.sc-foot-b { font-size: .78rem; color: var(--muted-2); font-variant-numeric: tabular-nums; }
.sc-foot-b b { color: var(--ink-2, var(--ink)); font-weight: 700; }

.qb-marked-note { margin-bottom: 26px; color: var(--muted); font-size: .88rem; display: flex; align-items: center; gap: 7px; }
.qb-marked-note strong { color: var(--ink); font-variant-numeric: tabular-nums; }
.qb-marked-note svg { color: #7c5cff; }

.qb-thin { margin-top: 14px; }
.qbf-loading { display: block; padding: 14px 16px; background: var(--surface); }
/* Your tests / Your mocks: one row per sitting in the same two-column panel (compact,
   2026-09-14). The whole row is the link to the score report. */
.yt-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 14px;
  padding: 10px 16px; background: var(--surface); color: inherit; text-decoration: none; min-width: 0;
  transition: background .15s; }
.yt-row:hover { background: var(--surface-2); }
.yt-row:focus-visible { outline: 2px solid var(--brand-600); outline-offset: -2px; }
.yt-main { min-width: 0; }
.yt-title { display: flex; align-items: center; gap: 8px; font-weight: 650; font-size: .92rem; color: var(--ink); line-height: 1.25; min-width: 0; }
.yt-ver { font-size: .7rem; font-weight: 600; color: var(--muted-2); border: 1px solid var(--border); border-radius: 99px; padding: 1px 7px; white-space: nowrap; }
.yt-meta { font-size: .74rem; color: var(--muted-2); margin-top: 2px; font-variant-numeric: tabular-nums; }
.yt-scores { display: flex; align-items: center; gap: 14px; }
.yt-score { display: flex; flex-direction: column; align-items: flex-end; min-width: 38px; line-height: 1.1; }
.yt-score b { font-size: .95rem; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.yt-score span { font-size: .64rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted-2); margin-top: 2px; }
.yt-total b { color: var(--brand-600); font-weight: 800; font-size: 1.02rem; }
@media (max-width: 420px) { .yt-scores { gap: 10px; } .yt-score { min-width: 32px; } }
.qb-thin > summary { cursor: pointer; color: var(--muted-2); font-size: .9rem; padding: 6px 0; }
.qb-thin > summary:hover { color: var(--ink); }

/* ---------- Score-over-time chart (dashboard) ---------- */
.trend-card { overflow: hidden; padding: 18px 20px 14px; margin-bottom: 24px; }
/* Title left, switches + target right, on ONE row; wraps under on a narrow card. */
.tr-head { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 10px 16px; }
.tr-head-text { min-width: 0; }
.tr-sub { margin: 3px 0 0; }
.tr-sub b { color: var(--ink); font-variant-numeric: tabular-nums; }
.tr-head-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.tr-wrap { margin-top: 8px; }
.tr-inner { position: relative; }
.tr-svg { width: 100%; height: auto; display: block; overflow: visible; }
/* A phone cannot hold thirty legible bars at once, so the chart keeps its bar width and the
   card scrolls sideways under it. The SVG is sized by HEIGHT here — with width:100% it
   would shrink straight back to the viewport and there would be nothing to scroll. */
.tr-scroll { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; scrollbar-width: thin; }
.tr-scroll .tr-inner { display: inline-block; min-width: 100%; }
.tr-scroll .tr-svg { width: auto; height: 210px; max-width: none; }
/* On a phone the box is ~380 units wide instead of 780, so every unit is worth roughly
   half as much on screen — the type is stepped up to land at the same read size. */
.tr-narrow .tr-ylab { font-size: 15px; }
.tr-narrow .tr-xlab { font-size: 15px; }
.tr-narrow .tr-target-lab { font-size: 15px; }
/* Dashed, so the grid stays behind the bars instead of competing with them. */
.tr-grid { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 5; }
.tr-ylab { fill: var(--faint); font-size: 11px; font-variant-numeric: tabular-nums; }
.tr-xlab { fill: var(--muted-2); font-size: 11.5px; }
.tr-hit { cursor: pointer; }
.tr-target { stroke: var(--faint); stroke-width: 1.5; stroke-dasharray: 6 5; }
.tr-target-lab { fill: var(--muted-2); font-size: 11px; font-weight: 600; letter-spacing: .02em; }
/* Stacked bars: Verbal on the floor, Math on its shoulders, the two summing to the total.
   The axis runs from zero here (not 400) so each segment's height is its own score. */
.tr-bar { transition: opacity .15s; }
.tr-bar-rw { fill: var(--rw); }
.tr-bar-math { fill: var(--math); }
.tr-barhit:hover .tr-bar { opacity: .82; }
.tr-barhit:focus { outline: none; }
.tr-barhit:focus-visible .tr-bar { opacity: .82; stroke: var(--ink); stroke-width: 2; }
/* Shown under All, where the change chips have nothing to compare against. */
.cmp-hint { margin: 0 0 26px; display: flex; align-items: baseline; gap: 7px; }
.cmp-hint b { color: var(--ink-2, var(--ink)); }
.cmp-hint svg { color: var(--brand-600); flex: none; }
/* Filled roundels rather than bare dots — at this size the two readings need to be told
   apart at a glance, and a tick and a cross do that faster than two colours.
   #10b981 is only 2.5:1 on a light card, so light mode takes the deeper green. */
.ac-badge { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; flex: none; color: #fff; }
.ac-badge svg { width: 15px; height: 15px; stroke-width: 3; }
.ac-badge-ok { background: #059669; }
.ac-badge-bad { background: var(--bad); }
:root[data-theme="dark"] .ac-badge-ok { background: #34d399; color: #06281d; }
:root[data-theme="dark"] .ac-badge-bad { background: #fb7185; color: #3d0713; }

/* Accuracy — the headline card: the rate large, the correct / wrong counts under it */
.cw-counts { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 18px; margin-top: 12px; }
.cw-n { display: inline-flex; align-items: center; gap: 7px; font-size: .86rem; color: var(--muted-2); font-weight: 500; }
.cw-n b { font-size: 1.15rem; font-weight: 800; color: var(--ink); font-variant-numeric: tabular-nums; }
.cw-n .ac-badge { width: 20px; height: 20px; }
.cw-n .ac-badge svg { width: 12px; height: 12px; }
.cw-dot { width: 9px; height: 9px; border-radius: 50%; background: #64748b; flex: none; }
:root[data-theme="dark"] .cw-dot { background: #94a3b8; }
.cw-bar { display: flex; height: 6px; border-radius: 99px; overflow: hidden; margin-top: 10px; background: var(--surface-2); }
.cw-bar i { display: block; height: 100%; }
.cw-ok { background: #059669; }
.cw-bad { background: var(--bad); }
/* Left blank: neither a mistake nor a mark, so neither red nor green. */
.cw-omit { background: #64748b; }
:root[data-theme="dark"] .cw-ok { background: #34d399; }
:root[data-theme="dark"] .cw-bad { background: #fb7185; }
:root[data-theme="dark"] .cw-omit { background: #94a3b8; }

/* The legend row: the three series named and switched from one place, under the title. */
.tr-legend { display: flex; flex-wrap: wrap; gap: 8px; }
/* Legend doubles as the on/off switch for each line. */
.tr-leg { display: inline-flex; align-items: center; gap: 7px; padding: 4px 10px; border-radius: var(--r-chip);
  border: 1px solid var(--border); background: transparent; color: var(--muted-2); font-size: .84rem; font-weight: 600;
  cursor: pointer; transition: color .15s, border-color .15s, opacity .15s; }
.tr-leg:hover { color: var(--ink); }
.tr-leg.on { color: var(--ink); border-color: var(--border-2, var(--border)); }
.tr-leg:not(.on) { opacity: .5; }
.tr-leg:not(.on) .tr-leg-dot { background: var(--faint) !important; }
.tr-leg-dot { width: 9px; height: 9px; border-radius: 50%; }
.tr-target-chip { display: inline-flex; align-items: center; gap: 7px; padding: 4px 10px; border-radius: var(--r-chip);
  border: 1px dashed var(--border); background: transparent; color: var(--muted-2); font-size: .84rem; font-weight: 600; cursor: pointer; }
.tr-target-chip:hover { color: var(--ink); border-color: var(--brand-600); }
/* Tooltip: a small titled card, not a dark pill of run-together text. The numbers are the
   thing being asked for, so they sit in their own right-aligned column. */
.tr-tip { position: absolute; left: 0; top: 0; transform: translate(-50%, calc(-100% - 12px)); pointer-events: none;
  min-width: 168px; background: var(--surface); color: var(--ink); border: 1px solid var(--border);
  padding: 11px 13px; border-radius: 12px; box-shadow: var(--shadow-lg); z-index: 3; white-space: nowrap; }
.tt-title { font-size: .84rem; font-weight: 700; color: var(--ink); margin-bottom: 2px; }
.tt-sub { font-size: .74rem; color: var(--muted-2); margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
.tt-title + .tt-row { margin-top: 8px; }
.tt-row { display: flex; align-items: center; gap: 8px; padding: 2px 0; font-size: .8rem; color: var(--muted-2); }
.tt-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.tt-dot-none { background: transparent; }
.tt-dot.tr-dot-rw { background: var(--rw); }
.tt-dot.tr-dot-math { background: var(--math); }
.tt-k { flex: 1; }
.tt-v { margin-left: 18px; color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
.tt-foot { margin-top: 6px; padding-top: 7px; border-top: 1px solid var(--border); color: var(--ink); font-weight: 600; }
.tt-foot .tt-k { padding-left: 17px; }

/* ---------- Accuracy rows (domain → skill, dashboard) ---------- */
/* Compact (2026-09-14): one column per section, side by side; each opens on its own. */
.acc-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; align-items: start; }
.acc-cols-1 { grid-template-columns: minmax(0, 1fr); }
@media (max-width: 900px) { .acc-cols { grid-template-columns: minmax(0, 1fr); } }
.acc-col-h { display: flex; align-items: center; gap: 7px; padding: 10px 12px 4px; font-size: .72rem; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; color: var(--muted-2); }
.acc-col-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.acc-list { padding: 0 4px 4px; }
.acc-group + .acc-group { border-top: 1px solid var(--border); }
.acc-head { display: flex; align-items: center; gap: 4px; width: 100%; background: transparent; border: 0;
  padding: 8px 10px; text-align: left; cursor: pointer; color: inherit; border-radius: var(--r-chip); }
.acc-head:hover:not(:disabled) { background: var(--surface-2); }
.acc-head:disabled { cursor: default; }
.acc-caret { width: 16px; height: 16px; color: var(--faint); flex: none; transition: transform .18s var(--ease-out); }
.acc-caret-sp { width: 16px; flex: none; }
.acc-group.open .acc-caret { transform: rotate(90deg); }
/* FIXED last two columns. Each row is its own grid, so an `auto` column sized itself to
   that row's text — "2/88" is narrower than "4/185" and those rows' bars started further
   right. Fixed widths line every meter up whatever the numbers say. */
.acc-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(64px, 30%) 46px;
  align-items: center; gap: 12px; flex: 1; min-width: 0; }
.acc-name { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.acc-name > span:first-child { font-weight: 600; color: var(--ink); font-size: .9rem; line-height: 1.25; }
.acc-meta { font-size: .74rem; color: var(--muted-2); font-variant-numeric: tabular-nums; line-height: 1.3; }
.acc-meter { height: 7px; border-radius: 99px; background: rgba(127, 127, 127, .16); overflow: hidden; }
.acc-meter i { display: block; height: 100%; border-radius: 99px; transition: width .5s var(--ease-out); }
.acc-num { font-weight: 800; font-size: .9rem; font-variant-numeric: tabular-nums; text-align: right; }
/* An open domain's skills sit in their own inset panel, not as more rows in the list —
   flush with the domains they read as more domains. Tinted, edged in the section colour,
   unheaded — the open domain row above it names it. */
.acc-kids { margin: 0 8px 10px 26px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px;
  background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--kids-accent, var(--border));
  border-radius: 12px; animation: accKidsIn .18s var(--ease-out); }
@keyframes accKidsIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .acc-kids { animation: none; } }
.acc-kids .acc-row { grid-template-columns: minmax(0, 1fr) minmax(56px, 28%) 44px; }
.acc-kids .acc-name > span:first-child { font-weight: 500; font-size: .86rem; color: var(--ink-2, var(--ink)); }
.acc-kids .acc-note { padding: 2px 0; }
.acc-row-empty .acc-name > span:first-child { color: var(--muted-2); }
/* The open domain's own row reads as the panel's title bar. */
.acc-group.open > .acc-head .acc-name > span:first-child { color: var(--ink); }
.acc-group.open > .acc-head .acc-caret { color: var(--kids-accent, var(--muted-2)); }
@media (max-width: 720px) {
  .acc-row { grid-template-columns: minmax(0, 1fr) 46px; grid-template-areas: "name num" "meter meter"; gap: 6px 10px; }
  .acc-name { grid-area: name; } .acc-num { grid-area: num; } .acc-meter { grid-area: meter; }
  .acc-kids { margin: 0 6px 12px 14px; padding-inline: 12px; }
  .acc-kids .acc-row { grid-template-columns: minmax(0, 1fr) 44px; }
}
/* Accuracy / Time switch and the time mode of the same rows */
.acc-seg { flex: none; }
.acc-seg .pill-opt { gap: 6px; }
.acc-seg-ic { width: 14px; height: 14px; flex: none; }
.acc-meter-time { position: relative; overflow: visible; }
.acc-pace { position: absolute; top: -4px; bottom: -4px; width: 2px; margin-left: -1px; border-radius: 1px;
  background: var(--ink); opacity: .55; }
.acc-num-none { color: var(--faint); }
.acc-over { color: #b45309; font-weight: 600; }
:root[data-theme="dark"] .acc-over { color: #fbbf24; }
.acc-note { display: flex; align-items: center; gap: 8px; padding: 6px 2px; margin: 0; }
.acc-empty { display: flex; gap: 12px; align-items: flex-start; }
.acc-empty > svg { width: 20px; height: 20px; flex: none; color: var(--faint); margin-top: 2px; }

/* "Where your time goes" — the section split above the time list */
.time-split { margin-bottom: 12px; padding: 14px 16px; }
.ts-head { display: flex; align-items: center; gap: 10px; font-size: .95rem; color: var(--ink); line-height: 1.35; }
.ts-head > svg { width: 18px; height: 18px; flex: none; color: var(--faint); }
.ts-bar { display: flex; height: 10px; border-radius: 99px; overflow: hidden; margin: 10px 0 12px;
  background: var(--surface-2); }
.ts-bar i { display: block; height: 100%; transition: width .5s var(--ease-out); }
.ts-bar i:first-child { border-radius: 99px 0 0 99px; }
.ts-bar i:last-child { border-radius: 0 99px 99px 0; }
.ts-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px 24px; }
.ts-head + .ts-tiles { margin-top: 14px; }
.ts-tile { min-width: 0; }
.ts-tile-top { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: .9rem; color: var(--ink); }
.ts-dot { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.ts-share { margin-left: auto; font-weight: 800; font-variant-numeric: tabular-nums; }
.ts-big { font-size: 1.25rem; font-weight: 800; line-height: 1.15; margin: 4px 0 2px; font-variant-numeric: tabular-nums; color: var(--ink); }
.ts-avg { color: var(--ink); font-variant-numeric: tabular-nums; }

/* score ring */
.ring { --p: 0; --size: 132px; --bw: 11px; width: var(--size); height: var(--size); position: relative; flex-shrink: 0; }
.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring .track { fill: none; stroke: var(--surface-3); stroke-width: var(--bw); }
.ring .fill { fill: none; stroke: var(--brand-500); stroke-width: var(--bw); stroke-linecap: round;
  transition: stroke-dashoffset 1s var(--ease-out); }
.ring .label { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; }
.ring .label b { font-family: var(--font-display); font-size: 1.9rem; font-weight: 500; line-height: 1; }
.ring .label span { font-size: .72rem; color: var(--muted-2); }

/* ---------- Test catalog cards ---------- */
.test-card { padding: 24px; display: flex; flex-direction: column; gap: 16px; height: 100%; }

/* Every exam card gets a coloured outline, so the catalog reads as a set. It is blue
   rather than brand crimson: with 91 cards on screen the outline was the largest block
   of red in the app, and crimson here collides with the red that means "wrong" — the
   .test-card-done note below records the same problem being patched one case at a time.
   Blue leaves amber and green free to mean progress. */
/* --card-line is the card's identity colour, declared once and read by everything that
   has to match it: the outline, and the letter box in the corner of the head. The state
   modifiers below only re-point this variable — nothing has to remember to restate the
   border and the box separately, which is exactly how those two would drift apart. */
.test-card { --card-line: var(--exam-line); border-color: var(--card-line); }
/* .card-hover:hover repaints the border, so every state has to win it back. One rule
   using the variable, instead of one per state. */
.test-card:hover { border-color: var(--card-line); }
/* The card's own action takes the card's colour. A crimson button sitting inside a blue
   outline read as two systems on one card. Resume (amber) and Retake (green) are left
   alone — those colours already carry a state. */
.test-card .btn-primary { --btn-bg: var(--exam-line); --ring: 0 0 0 4px rgba(59, 91, 219, .62); }
.test-card .btn-primary:hover { --btn-bg: color-mix(in srgb, var(--exam-line) 82%, #000);
  box-shadow: var(--shadow-md); }

/* The one test the student left unfinished switches the whole card to amber — outline,
   tint and button together — so it is the single thing that looks different in a grid
   of otherwise identical blue-outlined cards. */
.test-card-resume { --card-line: var(--warn); background: color-mix(in srgb, var(--warn) 7%, var(--surface)); }
.btn-resume { --btn-bg: var(--warn); --btn-fg: #fff; border-color: transparent; box-shadow: var(--shadow-sm); }
.btn-resume:hover { --btn-bg: #b45309; box-shadow: var(--shadow-md); }

/* A test the student has already completed (Retake). Same treatment as the resume card,
   in green — without this the default .test-card rule above leaves it with the outline
   of every untouched card, so nothing else on it would hint that it is done. */
.test-card-done { --card-line: var(--ok); background: color-mix(in srgb, var(--ok) 7%, var(--surface)); }
/* A form outside a free account's library. Quieter than the others but NOT disabled —
   the click is what explains the padlock, and a card nobody can press teaches nothing.
   The title stays at full strength: the student has to be able to read which exam it is
   to know what they would be unlocking. */
.test-card-locked { border-style: dashed; background: var(--surface); }
.test-card-locked h3 { color: var(--ink); }
.test-card-locked .small.muted { opacity: .7; }
/* Where "Show me the free tests" lands you. A ring in the card's own colour that fades out
   on its own — the student needs to know WHICH card the button meant, and then needs it to
   stop shouting. animation, not a transition, so it plays once and cleans up after itself.
   Held to opacity/box-shadow: nothing here moves, so scrolling past it stays smooth. */
@keyframes cardFound {
  0%   { box-shadow: 0 0 0 0 var(--card-line); }
  18%  { box-shadow: 0 0 0 5px color-mix(in srgb, var(--card-line) 45%, transparent); }
  100% { box-shadow: 0 0 0 0 rgba(0,0,0,0); }
}
.test-card.card-found { animation: cardFound 1.6s var(--ease) both; }
@media (prefers-reduced-motion: reduce) {
  /* The ring still appears — it is the answer to "which one?" — it simply does not pulse. */
  .test-card.card-found { animation: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--card-line) 45%, transparent); }
}
.tc-lock { gap: 5px; }
.tc-lock svg { width: 12px; height: 12px; }
.test-card .tc-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.test-card h3 { font-size: 1.18rem; }

/* Catalog card head: the month on the left, the form letter alone on the right.
   It used to be a "Version B" chip stacked ABOVE the month, which spent the card's most
   prominent slot on the word "Version" — the one word that is identical on every card —
   and pushed the thing a student actually scans for down a line. The month leads now and
   the letter is a quiet mark in the corner.
   Baseline rather than flex-start: these are two runs of text on one line, and .tc-title
   is a flex box, so it hands up its own first item's baseline — the h3's. */
/* The letter, the month and the optional Pro badge are DIRECT children here — an earlier
   pass wrapped two of them in a flex div, and a flex container does not pass its text
   baseline up: it synthesises one from its box, which left the letter sitting 2px off the
   month. Real siblings baseline-align against each other properly.
   justify-content is reset because .tc-top's space-between would push a three-child row
   apart and strand the month in the middle. */
/* Centre, not baseline: the row is now a box, a line of type and a pill, and only one of
   the three has a text baseline worth aligning to. */
.test-card .tc-head { align-items: flex-start; justify-content: flex-start; gap: 10px; }
/* The month grows and pushes the marks to the right edge. flex:1 on the title rather than
   margin-left:auto on the marks column: auto would work too, but only until something
   else lands in this row. */
.test-card .tc-head h3 { flex: 1; min-width: 0; }
/* The Pro badge rides on the M2 line, not in the head. Stacked under the letter it added
   its own height to every locked card; here it costs nothing, because the two M2 lines
   beside it are already taller than the badge. The head keeps the letter alone.

   The M2 text grows, so the badge lands on the right edge — directly under the letter box
   above it. Both marks in one right-hand column; the card's text all starts at one x. */
.test-card .tc-detail { display: flex; align-items: flex-start; gap: 10px; margin-top: 12px; }
.test-card .tc-detail .tc-m2 { flex: 1; min-width: 0; margin-top: 0; }
/* Nudged clear of the letter box above it. The two marks sit in the same right-hand column
   one row apart, so with the rows flush they read as one stacked pair with no air between
   them — 12px on the row plus 3px here is the gap that separates them without opening a
   hole in the middle of a 187px card. */
.test-card .tc-detail .tc-lock { flex: none; margin-top: 3px; }
/* --muted, not --muted-2. At 16.8px/700 this sits just under the 18.66px that would let it
   claim "large text", so it owes the full 4.5:1 on all four card states — plain, green
   completed, amber resume, locked — in both themes.
   --muted-2 does clear that, but its worst case is 4.57:1 on the green card, seven
   hundredths of headroom. The note on --muted-2 in the token block was written after
   exactly that kind of threshold-exact value failed the moment it landed on a tint, so
   this follows the same rule and takes --muted: 6.60:1 at its worst, and still plainly
   secondary to the --ink month beside it. */
/* The version letter, boxed, in the top-right corner — a quiet mark that tells the forms
   of one sitting apart without competing with the month for the start of the line.

   The box takes --card-line, so it is blue on an untouched card, amber on the one being
   resumed and green on a completed one: the letter and the outline around it read as the
   same object rather than two decisions. Because it inherits the variable there is nothing
   to keep in sync — a new card state only has to re-point --card-line.

   Shape, radius, border weight and the letter-to-box ratio are the exam engine's own
   .ch-key — 28px / 7px / 1.5px, .88rem at 700 — the same object the student taps during a
   test, and what .bubble on the landing page is scaled from. A lettered square in this app
   means one thing and looks one way; only the colour is local, so the mark belongs to its
   card while the geometry belongs to the site.

   A FIXED square, not padding around the glyph: the letters run A–H and their natural
   widths differ by about 2px, so a shrink-to-fit box would be a slightly different size on
   every card and the right edge of the grid would go ragged. */
/* No align-self: .tc-head aligns to flex-start, which pins the box to the TOP right corner
   whether the month runs to one line or two. Centring it would have it drift down the card
   on the longer titles ("December 2025" wraps) and sit at a different height per card. */
.test-card .tc-ver {
  flex: none;
  display: grid; place-items: center;
  width: 28px; height: 28px;
  border: 1.5px solid var(--card-line); border-radius: 7px;
  font-size: .88rem; font-weight: 700; line-height: 1;
  color: var(--card-line);
}
/* Head and detail line share one wrapper so the card's 16px column gap still falls
   between the block and the button, not between the month and the line under it. */
.test-card .tc-m2 { margin-top: 4px; }
/* Score chips for Recent-activity cards — R&W + Math + Total (sum) */
.score-chips { display: flex; gap: 8px; }
.score-chip { flex: 1; min-width: 0; text-align: center; padding: 8px 4px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--border); }
.score-chip-total { background: var(--brand-50); border-color: var(--brand-200); }
.score-chip .sc-num { font-size: 1.25rem; font-weight: 800; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
.score-chip-total .sc-num { color: var(--brand-600); }
.score-chip .sc-lab { margin-top: 3px; font-size: .64rem; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }

/* ======================================================================
   TEST ENGINE
   ====================================================================== */
/* The explicit height is not redundant with inset: 0. A position:fixed box resolves its
   edges against the LAYOUT viewport, and iOS Safari sizes that to the large viewport — the
   height with the address bar hidden. So `bottom: 0` landed below the visible area
   whenever the bar was showing, and the last flex child down there is .engine-bottom:
   the Back / Next buttons and the question navigator. During a timed test.
   With a height set, top/height win and `bottom` is ignored, so the runner ends exactly
   where the screen does. dvh rather than svh because .engine-body is overflow:hidden with
   its own inner scrollers — the document never scrolls in here, so the bar does not move
   and there is no resize to flinch at. On desktop dvh == vh == inset:0; nothing changes. */
.engine { position: fixed; inset: 0; height: 100dvh; z-index: 80; background: var(--surface); display: flex; flex-direction: column; }

/* ---------- Red means one thing in here ----------
   Brand crimson (#d11d2b) and the "wrong answer" red (#e11d48) are indistinguishable
   from memory, and the runner was spending crimson on chrome: passage bullets, the
   navigator's current cell, every active annotation tool, the Next button, the focus
   ring. A student glancing at any of it reads "error" for a beat before reading what
   it is. So the brand ramp is remapped to ink for the length of a test — one override
   here instead of fifteen edits downstream — and --bad keeps the colour to itself.
   The precedent is --pick: the selected-answer blue exists because it, too, used to
   be red. Outside .engine the brand ramp is untouched. */
.engine {
  --brand-50:  var(--surface-2);
  --brand-100: var(--surface-2);
  --brand-200: var(--border-2);
  --brand-300: var(--border-2);
  --brand-400: var(--muted-2);
  --brand-500: var(--ink-2);
  --brand-600: var(--ink-2);
  --brand-700: var(--ink);
  --shadow-brand: var(--shadow-md);
  --ring: 0 0 0 4px rgba(59, 91, 219, .62);   /* focus agrees with the selected-answer blue */
}
/* Same inversion as the button below: the badge keeps its --ink slab, and only the
   numeral flips to --bg. The crimson the dark theme used here existed solely because
   #fff on a near-white --ink was unreadable. (A --surface-3 tile was tried first and
   measured 4.18:1 against --ink — under AA for 14px bold. This measures ~9:1.) */
[data-theme="dark"] .engine .q-num { color: var(--bg); }
/* The one active-tool tint that hard-codes crimson rather than reading the ramp. */
.engine .qr-tool2[data-annotate].on { background: var(--surface-2); }
/* .btn-primary paints --brand-600 behind #fff text. Remapped to --ink-2 that is fine
   in the light theme and white-on-near-white in the dark one, so the dark theme takes
   the inverse: light slab, dark label. */
[data-theme="dark"] .engine .btn-primary { --btn-fg: var(--bg); }
/* The pages set viewport-fit=cover, so on a notched phone the viewport really does extend
   under the notch and the home indicator — and .engine is position:fixed inset:0, so these
   two bars sat in exactly those zones. The clock was under the notch and the Back/Next row
   under the home indicator, during a timed test. The insets resolve to 0px everywhere else. */
.engine-top {
  height: calc(60px + env(safe-area-inset-top, 0px)); padding-top: env(safe-area-inset-top, 0px);
  flex-shrink: 0; border-bottom: 2px dashed var(--border-2);
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding-left: max(22px, env(safe-area-inset-left, 0px));
  padding-right: max(22px, env(safe-area-inset-right, 0px));
}
.engine-top .et-title { font-weight: 600; font-size: .98rem; }
.engine-top .et-title small { display: block; color: var(--muted-2); font-weight: 500; font-size: .76rem; }
.timer {
  display: inline-flex; align-items: center; gap: 8px; padding: 7px 16px; border-radius: var(--r-chip);
  background: var(--surface-2); font-variant-numeric: tabular-nums; font-weight: 700; font-size: 1.05rem;
  border: 1px solid var(--border); transition: background var(--t), color var(--t);
}
.timer svg { width: 17px; height: 17px; }
/* Five minutes left. The old rule faded the whole clock to 55% opacity, on a loop, for
   the rest of the module — it dimmed the digits at the exact moment they matter most and
   never stopped. The warn colour is the lasting signal; the ring is the announcement, so
   it rings three times and rests, and the number stays fully legible throughout. */
@keyframes timerWarn {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--warn) 26%, transparent); }
}
.timer.warn { background: var(--warn-bg); color: var(--warn); border-color: #fde68a; animation: timerWarn 1.6s var(--ease) 3; }

.engine-body { flex: 1; overflow: hidden; display: grid; grid-template-columns: 1fr 1fr; }
.engine-body.single { grid-template-columns: 1fr; max-width: 820px; margin-inline: auto; width: 100%; }
.pane { overflow-y: auto; padding: 34px 40px 60px; }
.pane-left { border-right: 1px solid var(--border); }
.passage { font-family: var(--font-read); font-size: 1.06rem; line-height: 1.75; color: var(--ink-2); overflow-wrap: break-word; }
.passage p { margin-bottom: 16px; }
/* "Student notes" bullet lists inside questions — the global `ul{list-style:none}` reset
   strips the markers, making the notes blend together; restore bullets + spacing here. */
.passage ul, .q-prompt ul, .qr-result ul { list-style: disc; padding-left: 1.4em; margin: 6px 0 14px; }
.passage ul li, .q-prompt ul li, .qr-result ul li { margin-bottom: 9px; padding-left: 3px; line-height: 1.55; }
/* Bullets sit inside prose the student is reading closely; they mark items, they are
   not an accent. Muted keeps them legible without pulling the eye off the sentence. */
.passage ul li::marker, .q-prompt ul li::marker, .qr-result ul li::marker { color: var(--muted-2); }
.passage .figure { margin: 18px 0; padding: 18px; background: var(--surface-2); border-radius: var(--r); border: 1px solid var(--border); }

.q-num { width: 30px; height: 30px; border-radius: 8px; background: var(--ink); color: #fff; display: grid; place-items: center; font-weight: 700; font-size: .9rem; flex-shrink: 0; }
.q-prompt { font-size: 1.12rem; font-weight: 500; line-height: 1.55; margin-bottom: 22px; color: var(--ink); }
.q-prompt.read { font-family: var(--font-read); font-weight: 400; }

/* answer choices */
.choices { display: flex; flex-direction: column; gap: 12px; }
.choice {
  display: flex; align-items: flex-start; gap: 14px; padding: 15px 17px;
  border: 1.5px solid var(--border-2); border-radius: var(--r); background: var(--surface);
  text-align: left; width: 100%;
  /* opacity is in here so ruling a choice out reads as the choice receding, rather than
     half the row blinking to a new value between two frames */
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease), opacity var(--t-fast) var(--ease-out);
}
.choice:hover { border-color: var(--warn); background: var(--warn-bg); }
.choice .ch-key {
  width: 28px; height: 28px; flex-shrink: 0; border-radius: 7px; border: 1.5px solid var(--border-2);
  display: grid; place-items: center; font-weight: 700; font-size: .88rem; color: var(--muted);
  transition: all var(--t-fast);
}
.choice .ch-text { padding-top: 2px; line-height: 1.45; }
.choice.selected { border-color: var(--pick); background: var(--pick-bg); box-shadow: 0 0 0 1px var(--pick) inset; }
.choice.selected .ch-key { background: var(--pick); color: #fff; border-color: var(--pick); }
.choice.eliminated { opacity: .42; }
.choice.eliminated .ch-text { text-decoration: line-through; }
/* Cross-out toggle on each choice. The glyph stays small, but the TAP TARGET must not
   be — 42px (46 on touch-sized screens), pulled into the row's padding so the choice
   does not grow taller. Was a bare 15px icon: far too fiddly to hit on a phone. */
.choice .elim-x {
  margin-left: auto; align-self: center; flex: 0 0 auto;
  width: 42px; height: 42px; margin-right: -13px; margin-block: -9px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--circle); color: var(--muted-2);
  transition: background var(--t-fast), color var(--t-fast), transform var(--t-fast);
}
.choice .elim-x:hover { background: var(--surface-3); color: var(--ink); }
.choice .elim-x:active { transform: scale(.9); }
.choice .elim-x svg { width: 19px; height: 19px; }
@media (max-width: 900px) { .choice .elim-x { width: 46px; height: 46px; margin-right: -14px; } }
/* review states */
.choice.correct { border-color: var(--ok); background: var(--ok-bg); }
.choice.correct .ch-key { background: var(--ok); color: #fff; border-color: var(--ok); }
.choice.wrong { border-color: var(--bad); background: var(--bad-bg); }
.choice.wrong .ch-key { background: var(--bad); color: #fff; border-color: var(--bad); }
/* The graded choice announces itself once when a Check lands. The verdict used to just
   exist in the next frame — the whole stage was replaced, so nothing could transition.
   A ring grows out of the choice's own border and fades; box-shadow only, so the row
   never moves and the choices below it never reflow. */
@keyframes verdictLand {
  from { box-shadow: 0 0 0 0 var(--verdict); }
  to   { box-shadow: 0 0 0 12px transparent; }
}
.answer-settle .choice.correct { --verdict: color-mix(in srgb, var(--ok) 60%, transparent); }
.answer-settle .choice.wrong { --verdict: color-mix(in srgb, var(--bad) 60%, transparent); }
.answer-settle .choice.correct,
.answer-settle .choice.wrong { animation: verdictLand .44s var(--ease-out); }

/* graded picks keep their tint on hover (don't flash orange); once solved the whole set locks */
.choice.correct:hover { border-color: var(--ok); background: var(--ok-bg); }
.choice.wrong:hover { border-color: var(--bad); background: var(--bad-bg); }
.choice.locked, .choice:disabled { cursor: default; }
.choice.locked:not(.correct):not(.wrong) { opacity: .5; }
.choice.locked:not(.correct):not(.wrong):hover { border-color: var(--border-2); background: var(--surface); }

/* grid-in (student produced response) */
.grid-in { max-width: 168px; }
.grid-in .input { font-size: 1.15rem; font-weight: 600; text-align: center; font-variant-numeric: tabular-nums; letter-spacing: .04em; padding: 9px 12px; }
.grid-in-note { margin-top: 10px; font-size: .82rem; color: var(--muted-2); }

/* Desmos calculator (Math section) — a resizable split panel. Opening it docks a big
   Desmos calculator on the LEFT (.calc-pane) and slides the question to the RIGHT; the
   .calc-split divider in the middle resizes both. The pane is a flow child of
   .engine-stage with a solid bg + z-index 6, so it sits cleanly over the watermark. */
.engine-stage.calc-open { flex-direction: row; }
.engine-stage.calc-open > .qr-stage-inner { min-width: 0; }
.engine-stage.calc-open .qr-wm-logo { display: none; }   /* hide the half-covered centre logo while split */
.calc-pane { flex: 0 0 var(--calc-w, 50%); min-width: 320px; display: flex; flex-direction: column;
  background: var(--surface); position: relative; z-index: 6; }
.calc-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 8px 8px 14px;
  background: var(--surface-2); border-bottom: 1px solid var(--border); user-select: none; }
.calc-title { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; font-size: .92rem; color: var(--ink); }
.calc-title svg { width: 17px; height: 17px; color: var(--brand-600); }
.calc-close { display: inline-grid; place-items: center; width: 30px; height: 30px; border: none; background: none; border-radius: 7px; cursor: pointer; color: var(--muted-2); }
.calc-close:hover { background: var(--surface); color: var(--ink); }
.calc-close svg { width: 17px; height: 17px; }
.calc-body { flex: 1; min-height: 0; }
.calc-offline { padding: 26px 20px; text-align: center; color: var(--muted-2); font-size: .9rem; line-height: 1.6; }
/* draggable divider between the calculator and the question */
/* touch-action: none is what makes it draggable BY FINGER. Without it the browser owns
   the gesture: it reads the drag as a scroll, takes the pointer away mid-drag (a
   pointercancel the JS then has to clean up after), and the divider does not move. */
.calc-split { flex: 0 0 8px; align-self: stretch; position: relative; background: var(--border); cursor: col-resize; z-index: 6; touch-action: none; }
.calc-split::before { content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 3px; height: 44px; border-radius: 3px; background: var(--muted-2); opacity: .5; }
.calc-split::after { content: ""; position: absolute; top: 0; bottom: 0; left: -5px; right: -5px; cursor: col-resize; }
.calc-split:hover, .calc-dragging .calc-split { background: var(--brand-600); }
.calc-split:hover::before, .calc-dragging .calc-split::before { background: #fff; opacity: .9; }
.calc-dragging, .calc-dragging * { cursor: col-resize !important; user-select: none; }
@media (max-width: 720px) {
  .engine-stage.calc-open { flex-direction: column; }
  .calc-pane { flex: 1 1 auto; min-width: 0; border-bottom: 1px solid var(--border); }
  .calc-split { display: none; }
}

/* stacked fraction (rendered in Math content) */
.frac { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; vertical-align: middle;
  margin: 0 .12em; font-size: .82em; line-height: 1.05; position: relative; top: -.04em; }
.frac-n { display: block; padding: 0 .32em; }
.frac-d { display: block; padding: 0 .32em; border-top: 1.4px solid currentColor; }
/* An exponent that is itself a fraction — 100(0.76)^(t/22.96). <sup> already shrinks
   the text, so the fraction inside it must not shrink again to .82em on top of that,
   and its rule needs to stay visible at that size. */
/* .frac carries vertical-align:middle, which CANCELS the <sup> raise: the
   fraction then sits on the baseline and 2^(t/850) reads as 2 x (t/850) —
   a power that looks like a multiplication. -.55em puts it back up at 7.1px,
   matching the 7.0px a plain exponent gets, with no change to line height. */
sup .frac { font-size: 1em; margin: 0 .06em; top: -.55em; }
sup .frac-n, sup .frac-d { padding: 0 .18em; }
sup .frac-d { border-top-width: 1.2px; }   /* thinner than the 1.4px body rule, but 1px reads as faint at exponent size */

/* square root: a √ sign + an overline (vinculum) over the radicand.
   Kept as plain INLINE (not inline-flex) so the radicand sits on the text baseline —
   inline-flex synthesises a baseline that drops the whole radical ~0.6em below the line. */
.sqrt { display: inline; white-space: nowrap; }
.sqrt-sign { display: inline-block; line-height: 1; transform: scaleY(1.3); transform-origin: bottom; margin-right: 1px; }
.sqrt-rad { border-top: 1.5px solid currentColor; padding: 2px 3px 0 2px; }
/* A radicand that is ITSELF a stacked fraction (√(a/b), e.g. an answer choice like
   √((3a-2c)/(6ac²))) is taller than a plain-text radicand, but .sqrt-rad's own
   border-top sits at ordinary text height — it ends up drawn through the numerator
   instead of over the whole fraction, and the denominator spills out past the bottom
   of its own box. Scoped to :has(.frac) so the far more common plain-text radical
   (√(2k+7), the vast majority of radicals in the corpus) renders exactly as before. */
.sqrt-rad:has(.frac) { display: inline-flex; align-items: flex-end; border-top: none;
  position: relative; padding: 6px 3px 0 2px; }
.sqrt-rad:has(.frac)::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
  border-top: 1.5px solid currentColor; }
/* scaleY() only stretches the PAINTED glyph — it never touches the layout box or the
   baseline .sqrt-sign aligns on, so scaling it up while it stays baseline-aligned next to
   an inline-flex .sqrt-rad left the sign's own bottom sitting well above the fraction's
   bottom: a visible gap, not a joined radical. Making .sqrt itself the flex container and
   pinning both children to align-items:flex-end gives sign and rad the SAME bottom edge
   before any scaling happens, so scaleY (still anchored at that shared bottom via
   transform-origin) grows the glyph upward to meet the vinculum instead of floating loose. */
.sqrt:has(.sqrt-rad .frac) { display: inline-flex; align-items: flex-end; }
.sqrt:has(.sqrt-rad .frac) .sqrt-sign { transform: scaleY(2.05); }
/* A root INDEX (the small n in "the nth root of x", <sup>n</sup> immediately before a
   √) sits flush against a plain exponent sup's baseline with nothing to tell them apart
   — a student can misread "^(2n)√(3x^n+40)" as another exponent instead of a root index.
   Pulling it in to touch the radical (real nth-root notation nests the index into the
   sign's own notch) is the cheapest way to make it read as PART OF the √, not a power. */
sup:has(+ .sqrt) { margin-right: -.22em; }

/* ===== Practice Score Report (results view) — College Board–style report layout ===== */
.sr-wrap { max-width: 1440px; }
/* .sr-head / .sr-title / .sr-disclaimer were deleted with the markup they styled — the
   report opens straight on the card now. */

/* ===================================================== THE PRINTED SCORE SHEET
   One A4 page, built only to be photographed by html2canvas and thrown away
   (pdfSheet() in views.js). It follows the official College Board practice score
   report — the layout a student and a tutor can already read without being taught.

   Rules this block lives by:
   1. NO var(). The app's tokens follow the theme, and a dark-theme token printed on
      html2pdf's white page is white-on-white. Every colour here is a literal.
   2. NO color-mix / oklch / relative colours — html2canvas cannot compute them and
      silently renders black.
   3. Fixed geometry. 794x1122px is A4 at 96dpi, so the capture maps onto the page
      with no scaling, and `overflow: hidden` means a second page cannot exist.
   Near-black on white with hairline rules, like the source document; the only colour
   is the X in the wordmark, so the sheet reads as a record rather than as marketing. */
.pdfsheet { position: relative; width: 794px; height: 1122px; overflow: hidden;
  box-sizing: border-box; padding: 40px 44px 30px; background: #ffffff; color: #10151f;
  font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  font-variant-numeric: tabular-nums; -webkit-font-smoothing: antialiased; }

.ps-top { display: flex; justify-content: space-between; align-items: center; gap: 30px; }
.ps-brand { display: flex; align-items: center; gap: 11px; }
.ps-logo { width: 46px; height: 46px; object-fit: contain; display: block; }
.ps-wordmark { font-size: 21px; font-weight: 700; letter-spacing: -0.02em; color: #10151f; }
.ps-wordmark b { color: #d11d2b; font-weight: 800; }
.ps-name { font-size: 12.5px; color: #10151f; }
.ps-name b { font-weight: 700; }

.ps-title { font-size: 34px; font-weight: 800; line-height: 1.12; letter-spacing: -0.02em;
  margin: 14px 0 22px; color: #10151f; }

/* The card. 8px radius and a hairline, as on the source sheet. */
.ps-card { border: 1px solid #d8dde5; border-radius: 8px; overflow: hidden; }
.ps-card-head { display: flex; justify-content: space-between; align-items: baseline;
  padding: 15px 20px; border-bottom: 1px solid #d8dde5; }
.ps-exam { font-size: 19px; font-weight: 800; margin: 0; letter-spacing: -0.01em; }
.ps-date { font-size: 13px; color: #10151f; white-space: nowrap; }

/* Three columns, ruled apart, and NOT equal: the room follows the content. Section
   scores are two short stacks — a number and a range — and at an even third they left a
   quarter of the column blank, while Score Details, which carries a header row plus two
   sub-columns of its own, was tight enough to break "Reading and Writing" over two lines
   and wrap "Incorrect Answers: 41". So section scores are capped and the slack goes to
   details. The cap is on a CLASS, not nth-child: on a section-only attempt this column
   is not rendered at all, and a positional rule would then shrink Score Details instead. */
.ps-grid { display: flex; align-items: stretch; border-bottom: 1px solid #d8dde5; }
.ps-col { flex: 1 1 0; min-width: 0; padding: 14px 18px 16px; }
.ps-col:first-child { flex: 0 0 178px; }
.ps-col-sec { flex: 0 0 176px; }
.ps-bord { border-left: 1px solid #d8dde5; }
.ps-lab { font-size: 12px; font-weight: 800; letter-spacing: .01em; margin-bottom: 10px; }
.ps-sub { font-size: 11.5px; font-weight: 700; margin-bottom: 2px; }

.ps-num-lg { font-size: 52px; font-weight: 800; line-height: 1; letter-spacing: -0.03em; margin-bottom: 6px; }
.ps-num-md { font-size: 36px; font-weight: 800; line-height: 1; letter-spacing: -0.02em; }
.ps-num-sm { font-size: 26px; font-weight: 800; line-height: 1; letter-spacing: -0.02em; }
.ps-sec-row { display: flex; align-items: center; gap: 10px; }
.ps-range { font-size: 10px; line-height: 1.35; color: #10151f; }
.ps-range b { font-weight: 700; }

/* Dotted rules, the source's own separator — a solid line here would read as a second
   card edge inside the column. */
.ps-dots { border-top: 2px dotted #b9c1cd; margin: 14px 0 12px; }
.ps-dots-sm { margin: 12px 0 10px; }
.ps-note { font-size: 10.5px; line-height: 1.45; color: #10151f; }
.ps-note b { font-weight: 700; }

.ps-rule { height: 1px; background: #d8dde5; margin: 14px 0 12px; }
.ps-mini-lab { font-size: 10.5px; font-weight: 700; line-height: 1.25; }
.ps-mini-tot { font-size: 10.5px; line-height: 1.45; color: #10151f; }
.ps-right { margin-left: auto; text-align: right; }
.ps-two { display: flex; gap: 22px; }
.ps-mini { flex: 1 1 0; min-width: 0; }
.ps-mini-head { font-size: 11.5px; font-weight: 700; margin-bottom: 4px; }
.ps-mini .ps-sec-row { margin-bottom: 5px; }

/* Knowledge and skills: one box per question, filled where it was answered right. */
/* Spacing here is what fills the page. Putting every domain on one row cut ~170px of
   wrapped rows out of the block and left the card floating well above the footer, so
   the rhythm was opened up to take that room back: the eight domains now sit on a
   generous, even beat instead of a tight block with a hole under it. */
.ps-ks { padding: 18px 20px 22px; }
.ps-ks-title { font-size: 13px; font-weight: 800; letter-spacing: .01em; margin: 0 0 4px; }
.ps-ks-sub { font-size: 10.5px; color: #4a5568; margin: 0 0 18px; }
.ps-ks-grid { display: flex; gap: 34px; }
.ps-ks-col { flex: 1 1 0; min-width: 0; }
.ps-ks-h { font-size: 13px; font-weight: 800; margin-bottom: 15px; }
.ps-dom { margin-bottom: 55px; }
.ps-dom:last-child { margin-bottom: 0; }
/* Name and raw count on one line, the bar under it. The weighting line that used to sit
   between them is gone with its markup. */
.ps-dom-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  margin-bottom: 6px; }
.ps-dom-name { font-size: 11px; font-weight: 700; line-height: 1.3; }
.ps-dom-frac { font-size: 11px; font-weight: 700; white-space: nowrap; }
/* One bar, full width, with its percentage inside. Every domain is the same length, so
   the eight can be read against each other at a glance and all eight figures stack into
   a column on the right. (It replaced a box-per-question grid: that made every domain a
   different length, and the rows never lined up.) */
.ps-bar { position: relative; height: 21px; border: 1.5px solid #10151f;
  box-sizing: border-box; overflow: hidden; }
.ps-bar-fill { position: absolute; left: 0; top: 0; bottom: 0; background: #10151f; display: block; }
.ps-bar-pct { position: absolute; right: 7px; top: 0; line-height: 18px;
  font-size: 10.5px; font-weight: 700; color: #10151f; }
/* Once the fill runs under the label, the label has to invert or it disappears. */
.ps-bar-pct.on-fill { color: #ffffff; }
/* Untested domain: no border either — an empty bordered bar reads as a score of zero. */
.ps-bar-none { border: 0; height: auto; }
.ps-bar-none .ps-bar-pct { position: static; font-size: 10px; font-weight: 500; color: #4a5568; }

/* Pinned to the foot of the sheet. The disclaimer is small print, and small print goes
   under the thing it qualifies — it used to sit in the top-right corner, above the
   score. max-width keeps it near 75 characters a line; at full width, 10.5px type runs
   to ~120 and reads as a wall. */
.ps-foot { position: absolute; left: 44px; right: 44px; bottom: 24px; }
.ps-foot-rule { height: 1px; background: #d8dde5; margin-bottom: 12px; }
.ps-disclaimer { font-size: 10.5px; line-height: 1.55; color: #4a5568; margin: 0 0 11px; max-width: 470px; }
.ps-disclaimer b { color: #10151f; font-weight: 600; }
.ps-foot-row { display: flex; justify-content: space-between; font-size: 10.5px;
  font-weight: 600; color: #4a5568; }

.sr-card { border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; background: var(--surface); margin-bottom: 26px; }
/* align-items: center now, not baseline: the right side carries a button as well as the
   date, and a baseline row would hang it below the exam name's text line. */
.sr-card-head { display: flex; justify-content: space-between; align-items: center; gap: 12px 18px; padding: 20px 26px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.sr-exam { font-size: 1.5rem; font-weight: 800; margin: 0; }
.sr-head-right { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.sr-date { color: var(--muted); font-weight: 600; white-space: nowrap; }
/* On a phone the head stacks, and a full-width button under a stacked date reads as a
   third row of chrome before the score — keep it beside the date, sized to its label. */
@media (max-width: 560px) { .sr-card-head { gap: 10px 14px; } .sr-head-right { width: 100%; justify-content: space-between; } }

.sr-grid { display: grid; grid-template-columns: 1fr 1fr 1.15fr; }
.sr-col { padding: 22px 26px; }
.sr-bord { border-left: 1px solid var(--border); }
.sr-label { font-size: .82rem; font-weight: 800; letter-spacing: .03em; margin-bottom: 12px; }
.sr-bignum { font-size: 4.2rem; font-weight: 800; line-height: .95; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.sr-bignum-sm { font-size: 3rem; }
.sr-range { font-size: .82rem; color: var(--ink-2); margin-top: 8px; }
.sr-rangenote { font-size: .78rem; color: var(--muted); margin-top: 18px; line-height: 1.5; }
.sr-sec { margin-bottom: 16px; }
.sr-sub { font-weight: 700; margin-bottom: 6px; }
.sr-sec-row { display: flex; align-items: center; gap: 14px; }
.sr-sec-row .sr-range { border-left: 1px solid var(--border); padding-left: 14px; margin-top: 0; }
.sr-mini-lab { font-size: .82rem; font-weight: 600; color: var(--ink-2); border-left: 1px solid var(--border); padding-left: 14px; line-height: 1.3; }
.sr-mini-tot { font-size: .8rem; color: var(--muted); line-height: 1.5; }
.sr-detail-rule { border-top: 1px dashed var(--border); margin: 16px 0; }
.sr-two { display: flex; gap: 26px; flex-wrap: wrap; }
.sr-mini-head { font-weight: 700; margin-bottom: 8px; }
.sr-mini-row { display: flex; align-items: center; gap: 12px; }
.sr-mini-num { font-size: 2.2rem; font-weight: 800; font-variant-numeric: tabular-nums; }
@media (max-width: 820px) {
  .sr-grid { grid-template-columns: 1fr; }
  .sr-bord { border-left: none; border-top: 1px solid var(--border); }
}

/* knowledge & skills — wrapped in a card ("window") to match the other report sections */
.sr-ks { margin-bottom: 26px; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--surface); padding: 22px 26px; }
.sr-ks-title { font-size: 1.05rem; font-weight: 800; letter-spacing: .03em; margin: 0 0 4px; }
.sr-ks-sub { color: var(--muted); font-size: .9rem; margin-bottom: 18px; }
.sr-ks-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 28px 36px; }
/* Math (right) column hugs the window's right edge: right-align its content and move the count BEFORE the bricks so the LAST brick touches the edge */
.sr-ks-col:last-child { text-align: right; }
.sr-ks-col:last-child .sr-boxes { justify-content: flex-end; }
.sr-ks-col:last-child .sr-dom-frac { order: -1; margin: 0 8px 0 0; }
/* A section-practice report has ONE column, and that column is also :last-child — so the
   mirroring above pushed a lone Reading and Writing block against the right edge with an
   empty half beside it. The mirror only means anything when there are two blocks facing
   each other; with one, the grid collapses to a single column and it reads left like the
   rest of the report. :has() rather than a class so the report stays declarative — the
   layout follows from how many columns were rendered, not from a flag passed down. */
.sr-ks-grid:not(:has(.sr-ks-col + .sr-ks-col)) { grid-template-columns: minmax(0, 1fr); }
.sr-ks-grid:not(:has(.sr-ks-col + .sr-ks-col)) .sr-ks-col:last-child { text-align: left; }
.sr-ks-grid:not(:has(.sr-ks-col + .sr-ks-col)) .sr-ks-col:last-child .sr-boxes { justify-content: flex-start; }
.sr-ks-grid:not(:has(.sr-ks-col + .sr-ks-col)) .sr-ks-col:last-child .sr-dom-frac { order: 0; margin: 0 0 0 6px; }
@media (max-width: 820px) { .sr-ks-grid { grid-template-columns: 1fr; } .sr-ks-col:last-child { text-align: left; } .sr-ks-col:last-child .sr-boxes { justify-content: flex-start; } .sr-ks-col:last-child .sr-dom-frac { order: 0; margin: 0 0 0 6px; } }
.sr-ks-h { font-size: 1.05rem; font-weight: 800; margin-bottom: 14px; }
.sr-dom { margin-bottom: 16px; }
.sr-dom-name { font-weight: 700; }
.sr-dom-meta { font-size: .82rem; color: var(--muted); margin: 1px 0 7px; }
/* one box per question; wraps onto multiple lines so wide domains never overflow the card */
.sr-boxes { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.sr-box { width: 22px; height: 10px; border: 1.5px solid var(--ink); border-radius: 3px; background: transparent; flex: 0 0 auto; }
.sr-box.fill { background: var(--ink); }
.sr-dom-frac { font-size: .8rem; font-weight: 700; color: var(--muted); margin-left: 6px; flex: 0 0 auto; white-space: nowrap; }
.sr-dom-none { font-size: .82rem; color: var(--faint); }
@media (max-width: 560px) { .sr-boxes { flex-wrap: wrap; } }

/* color-coded answer key (grouped by section) */
.sr-akey { padding: 22px 26px; }
.sr-akey-sec { margin-top: 18px; }
.sr-akey-label { font-size: 1rem; font-weight: 800; margin-bottom: 12px; }
.sr-akey-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 4px; }
.sr-legend { display: flex; gap: 14px; }
/* Own line, after the legend — the head is a wrapping space-between row, so without this
   the hint would become a third column and squeeze the legend. */
.sr-ak-hint { flex-basis: 100%; order: 3; margin: 2px 0 0; }
.sr-leg { display: inline-flex; align-items: center; gap: 5px; font-size: .82rem; font-weight: 600; }
.sr-leg svg { width: 14px; height: 14px; }
.sr-leg.ok { color: var(--ok); }
.sr-leg.bad { color: var(--bad); }
.sr-akey-grid { display: grid; grid-template-columns: repeat(15, minmax(0, 1fr)); gap: 9px; margin-top: 16px; }
@media (max-width: 1200px) { .sr-akey-grid { grid-template-columns: repeat(10, minmax(0, 1fr)); } }
@media (max-width: 820px)  { .sr-akey-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); } }
@media (max-width: 520px)  { .sr-akey-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.sr-ak { display: flex; flex-direction: column; align-items: center; gap: 1px; padding: 8px 4px 7px; border: 1.5px solid var(--border); border-radius: 10px; background: var(--surface); cursor: pointer; transition: transform var(--t-fast), box-shadow var(--t-fast); }
.sr-ak:hover { transform: translateY(-2px); box-shadow: var(--shadow-sm); }
.sr-ak-n { font-size: .72rem; font-weight: 700; color: var(--muted-2); }
.sr-ak-ans { font-size: 1.35rem; font-weight: 800; line-height: 1.1; }
.sr-ak.ok { border-color: var(--ok); background: var(--ok-bg); }
.sr-ak.ok .sr-ak-ans { color: var(--ok); }
.sr-ak.bad { border-color: var(--bad); background: var(--bad-bg); }
.sr-ak.bad .sr-ak-ans { color: var(--bad); }
.sr-ak.skip { border-color: var(--faint); background: var(--surface-2); }
.sr-ak.skip .sr-ak-n { color: var(--faint); }
.sr-ak.skip .sr-ak-ans { color: var(--muted-2); }
/* Time spent on the question, under its key letter. Muted, so the grid still reads as an
   answer key first; slower than the section's pace goes amber-ink so those stand out. */
.sr-ak-t { font-size: .68rem; font-weight: 600; color: var(--muted-2); font-variant-numeric: tabular-nums; line-height: 1.2; }
.sr-ak-t.slow, .sr-ak-slowtxt { color: #b45309; font-weight: 700; }
:root[data-theme="dark"] .sr-ak-t.slow, :root[data-theme="dark"] .sr-ak-slowtxt { color: #fbbf24; }
.sr-akey-label { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 14px; }
.sr-akey-time { display: inline-flex; align-items: center; gap: 5px; font-size: .8rem; font-weight: 600; color: var(--muted-2); font-variant-numeric: tabular-nums; }
.sr-akey-time svg { width: 13px; height: 13px; align-self: center; }
.rv-time { display: inline-flex; align-items: center; gap: 5px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.rv-time svg { width: 14px; height: 14px; }
.rv-time.slow { color: #b45309; }
:root[data-theme="dark"] .rv-time.slow { color: #fbbf24; }
.rv-time-pace { font-weight: 500; color: var(--muted-2); }
.sr-leg.skip { color: var(--muted-2); }
.sr-leg .leg-dot { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }

/* engine bottom bar */
.engine-bottom {
  height: calc(66px + env(safe-area-inset-bottom, 0px)); padding-bottom: env(safe-area-inset-bottom, 0px);
  flex-shrink: 0; border-top: 1px solid var(--border); background: var(--surface);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 14px;
  padding-left: max(22px, env(safe-area-inset-left, 0px));
  padding-right: max(22px, env(safe-area-inset-right, 0px));
  box-shadow: 0 -4px 16px -10px rgba(15,23,42,.2);
}
.engine-bottom > .nav-pill { justify-self: center; }
/* Who is sitting the test. It carries the FULL name now, so the NAME is what gives way
   when the bar runs out of room — the cell itself keeps the inline min-width that matches
   the Back/Next cell opposite, or the navigator between them stops being centred. */
.qr-user-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* On a phone there is room for the tile or the name, not both; the photo is the quicker
   read and the name is one tap away on the account page. */
@media (max-width: 560px) { .qr-user-name { display: none; } }
.qbank-runner .engine-bottom { border-top: none; }
.nav-pill {
  display: inline-flex; align-items: center; gap: 10px; padding: 9px 18px; border-radius: var(--r-chip);
  background: var(--ink); color: #fff; font-weight: 600; font-size: .92rem;
  transition: background var(--t-fast);
}
/* Was `#000` — a magic value barely distinct from the --ink base, and the dark theme's
   later `.nav-pill` rule has the same specificity, so it cancelled this outright: the
   exam's navigator button had no hover response at all in dark mode. */
.nav-pill:hover { background: var(--ink-2); }
[data-theme="dark"] .nav-pill:hover { background: var(--brand-700); }
.nav-pill svg { width: 16px; height: 16px; }

/* question navigator popover */
.navigator { position: absolute; bottom: 74px; left: 50%; transform: translateX(-50%); width: min(540px, 92vw);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow-lg);
  padding: 20px; z-index: 90; animation: popIn var(--t) var(--ease-out); }
.nav-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(46px, 1fr)); gap: 9px; margin-top: 14px; }
.nav-cell {
  aspect-ratio: 1; border-radius: 9px; border: 1.5px solid var(--border-2); background: var(--surface);
  font-weight: 700; font-size: .9rem; color: var(--muted); display: grid; place-items: center; position: relative;
  transition: all var(--t-fast);
}
.nav-cell:hover { border-color: var(--brand-400); }
/* Answered is a TINTED tile — pale blue fill, blue border, blue number — the same
   shape as the Question Bank's difficulty tiles. It was a solid brand-red fill, which
   left the red marked-for-review bookmark invisible on top of it. */
.nav-cell.answered { background: var(--pick-bg); color: var(--pick); border-color: var(--pick); }
.nav-cell.current { box-shadow: 0 0 0 3px var(--brand-200); border-color: var(--brand-600); }
/* marked for review — drawn by the .nc-flag bookmark badge below, not a dot */
.nav-legend { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 16px; font-size: .8rem; color: var(--muted); }

/* Question Bank navigator: 10 per row, ~5 rows then vertical scroll; cells tinted by
   difficulty (easy green / medium yellow / hard red) with a status badge in the corner. */
.qbank-runner .navigator { width: min(580px, 94vw); }
.qbank-runner .nav-grid {
  grid-template-columns: repeat(10, 1fr); gap: 8px;
  max-height: 258px; overflow-y: auto; overflow-x: hidden;
  padding: 8px 10px 8px 6px; scrollbar-width: thin; scrollbar-gutter: stable;
}
/* Module-review navigator: fixed 9 per row, centered, no inner scroll —
   R&W 27 → 3 tidy rows of 9; Math 22 → 9 + 9 + a centered 4. */
.qbank-runner .nav-grid.nav-review {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;
  max-width: 500px; margin: 14px auto 0; max-height: none; overflow: visible; padding: 0;
}
.qbank-runner .nav-grid.nav-review .nav-cell { width: 46px; height: 46px; aspect-ratio: auto; flex: 0 0 auto; }
.qbank-runner .nav-cell { font-size: .8rem; border-radius: 8px; border-width: 1.5px; overflow: visible; }
.qbank-runner .nav-cell.nc-easy   { background: var(--ok-bg);   border-color: var(--ok);   color: var(--ok); }
.qbank-runner .nav-cell.nc-medium { background: var(--warn-bg); border-color: var(--warn); color: var(--warn); }
.qbank-runner .nav-cell.nc-hard   { background: var(--bad-bg);  border-color: var(--bad);  color: var(--bad); }
.qbank-runner .nav-cell:hover { filter: brightness(.96); }
.qbank-runner .nav-cell.current { box-shadow: 0 0 0 2px var(--ink); border-color: var(--ink); }
.qbank-runner .nav-cell .nc-badge {
  position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; border-radius: 50%;
  display: grid; place-items: center; border: 2px solid var(--surface); box-shadow: 0 1px 2px rgba(0,0,0,.18);
}
.qbank-runner .nc-badge svg { width: 9px; height: 9px; color: #fff; stroke-width: 3.2; }
.qbank-runner .nc-badge.nc-ok   { background: var(--ok); }
.qbank-runner .nc-badge.nc-bad  { background: var(--bad); }
/* Marked for review — a bare red bookmark tucked into the tile's corner (no circle,
   no dot), the same mark as the "Mark for Review" button. Question Bank puts it top-
   LEFT (the ok/bad status badge owns the right); the Digital SAT navigator and module
   review put it top-RIGHT via .nc-flag-r. The corner offsets stay put while the size
   grows, so the mark expands INWARD over the tile rather than drifting off it. */
.qbank-runner .nc-badge.nc-flag {
  background: none; border: none; box-shadow: none;
  width: 19px; height: 24px; top: -6px; left: -4px; right: auto;
}
.qbank-runner .nc-badge.nc-flag svg {
  width: 19px; height: 24px; color: var(--bad); fill: var(--bad); stroke: var(--bad);
  stroke-width: 1.5; stroke-linejoin: round;
}
.qbank-runner .nc-badge.nc-flag.nc-flag-r { left: auto; right: -4px; }
.qbank-runner .qbank-legend { align-items: center; gap: 12px 16px; }
.qbank-runner .nc-swatch { display: inline-block; width: 13px; height: 13px; border-radius: 4px; vertical-align: -2px; margin-right: 3px; border: 1.5px solid; }
.qbank-runner .nc-swatch.nc-easy   { background: var(--ok-bg);   border-color: var(--ok); }
.qbank-runner .nc-swatch.nc-medium { background: var(--warn-bg); border-color: var(--warn); }
.qbank-runner .nc-swatch.nc-hard   { background: var(--bad-bg);  border-color: var(--bad); }
.qbank-runner .nc-badge.nc-legend-badge { position: static; width: 15px; height: 15px; vertical-align: -3px; margin-right: 3px; border: none; }
.qbank-runner .nc-badge.nc-flag.nc-legend-badge { width: 16px; height: 20px; }
.qbank-runner .nc-badge.nc-flag.nc-legend-badge svg { width: 16px; height: 20px; }
.nav-legend span { display: inline-flex; align-items: center; gap: 6px; }

/* break / module-review screens */
.center-screen { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; overflow-y: auto; padding: 30px; }
/* .inner centres via auto margins when it fits, and top-aligns + SCROLLS when it is taller than the
   viewport — so on small/short screens the "Continue to next module" buttons are never clipped off. */
.center-screen .inner { max-width: 560px; text-align: center; margin-block: auto; }
.big-icon { width: 76px; height: 76px; border-radius: 20px; display: grid; place-items: center; margin: 0 auto 22px; }
.big-icon svg { width: 38px; height: 38px; }

.seg { display: inline-flex; padding: 4px; background: var(--surface-2); border-radius: var(--r-chip); border: 1px solid var(--border); }
.seg button { padding: 7px 16px; border-radius: var(--r-chip); font-weight: 600; font-size: .86rem; color: var(--muted); transition: all var(--t-fast); }
.seg button.active { background: var(--surface); color: var(--brand-700); box-shadow: var(--shadow-xs); }

/* ===== AUTH — centered, minimal ===== */
.auth-wrap {
  min-height: 100dvh;
  display: flex; align-items: flex-start; justify-content: center;
  padding: clamp(40px, 10vh, 120px) 20px 56px;
}
.auth-card { width: 100%; max-width: 420px; text-align: center; }
.auth-brand {
  display: inline-flex; align-items: center; justify-content: center; gap: 14px;
  margin-bottom: 34px; text-decoration: none;
}
.auth-brand .brand-logo { height: 60px; }
.auth-wordmark {
  font-family: var(--font-display); font-weight: 800;
  letter-spacing: -.03em; line-height: 1; color: var(--ink);
  font-size: clamp(2.2rem, 1.7rem + 2.2vw, 3rem);
}
.auth-wordmark b { color: var(--brand-600); font-weight: 800; }
.auth-title {
  font-family: var(--font-display); font-weight: 600;
  letter-spacing: -.02em; line-height: 1.15;
  font-size: clamp(1.55rem, 1.25rem + 1.1vw, 2rem);
  margin-bottom: 8px;
}
/* balance: a centred line that wraps is split into even lines, never a full one
   with a single word left under it. */
.auth-sub { color: var(--muted); margin-bottom: 28px; text-wrap: balance; }
.auth-form { text-align: left; }
/* First-run profile setup (views.js welcome) — the auth card with a photo tile and a
   country field. The tile keeps the avatar's own rounded-square shape at 96px, so what
   is picked here is recognisably the thing that shows up in the sidebar afterwards. */
.wel-photo { display: flex; flex-direction: column; align-items: center; gap: 14px; margin-bottom: 24px; }
.wel-avatar { display: block; padding: 0; border-radius: 28px; cursor: pointer; }
.wel-avatar .avatar { width: 96px; height: 96px; font-size: 2rem; border-radius: 28px;
  border: 1px solid var(--border); transition: border-color var(--t-fast), box-shadow var(--t-fast); }
.wel-avatar:hover .avatar { border-color: var(--brand-300); box-shadow: var(--shadow-md); }
.wel-photo-actions { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; }
.wel-country { display: flex; align-items: center; gap: 10px; }
.wel-country .input { flex: 1 1 auto; min-width: 0; }
.wel-flag:empty { display: none; }
.wel-flag .flag, .wel-flag .flag-code { margin: 0; }
.wel-flag .flag { width: 28px; height: 21px; vertical-align: middle; }
.wel-err { text-align: left; margin-top: 12px; }
.wel-done { margin-top: 22px; }
.wel-note { margin-top: 14px; }
@media (pointer: coarse) { .wel-photo-actions .btn-sm { min-height: 44px; } }
.pass-wrap { position: relative; }
.pass-wrap .input { padding-right: 46px; }
/* Hide the browser's built-in password reveal/clear icon (Edge/IE) so only our custom eye shows.
   Kept as separate rules: an unknown pseudo-element makes a browser drop only that one rule. */
.pass-wrap .input::-ms-reveal { display: none; }
.pass-wrap .input::-ms-clear { display: none; }
.pass-wrap .input::-webkit-credentials-auto-fill-button { visibility: hidden; }
/* Global safety net: kill the native reveal/clear on EVERY password field (reset page, etc.)
   so the browser's built-in eye never doubles up with our custom one. */
input[type="password"]::-ms-reveal { display: none; }
input[type="password"]::-ms-clear { display: none; }
input[type="password"]::-webkit-credentials-auto-fill-button { visibility: hidden; }
.pass-eye {
  position: absolute; top: 50%; right: 6px; transform: translateY(-50%);
  width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 8px; color: var(--muted); background: transparent; cursor: pointer;
  transition: color var(--t-fast), background var(--t-fast);
}
.pass-eye:hover { color: var(--ink); background: var(--surface-2); }
.pass-eye svg { width: 18px; height: 18px; }
/* Turnstile bot check — usually renders nothing, so the slot must not leave a gap */
/* 🔴 THE SLOT MUST HOLD ITS HEIGHT BEFORE THE WIDGET ARRIVES.
   mountTurnstile() fetches challenges.cloudflare.com on demand and then polls every
   100ms for it, so the widget lands hundreds of ms after the form has painted — and it
   sits directly ABOVE the submit button. The old `:empty { display: none }` gave the
   slot zero height until then, so its arrival shoved the whole button row down 73px.
   Cloudflare's own Web Analytics recorded that as the site's single biggest layout
   shift: `form.auth-form > button.btn-block.btn-lg.btn-primary`, 397 of the CLS samples
   in 24h, with the site at 51% POOR on CLS (measured 2026-09-04).
   73px is the Turnstile container's real box (300x73, measured, no margin of its own).
   An empty slot can no longer collapse — which is the point — so turnstileSlot() in
   views.js emits nothing at all when there is no site key, rather than leaving a gap. */
.turnstile-slot { display: flex; justify-content: center; min-height: 73px; }

/* "or" divider + Google button on the sign-in / sign-up card */
.auth-or { display: flex; align-items: center; gap: 12px; margin: 18px 0 14px; color: var(--muted); font-size: .85rem; }
.auth-or::before, .auth-or::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.btn-google { display: flex; align-items: center; justify-content: center; gap: 10px;
  background: var(--surface); color: var(--ink); border: 1px solid var(--border); font-weight: 600; }
.btn-google:hover:not(:disabled) { background: var(--surface-2); }
.btn-google svg { flex: none; }

.auth-switch { margin-top: 22px; color: var(--muted); font-size: .96rem; }
.auth-switch a { color: var(--brand-600); font-weight: 600; }
.auth-switch a:hover { text-decoration: underline; }
.auth-note { margin-top: 16px; }
@media (max-width: 480px) { .auth-brand .brand-logo { height: 50px; } }

/* ===== SETTINGS (Account / Subscription / …) ===== */
.set-section { margin-bottom: 24px; }
.set-head { display: flex; align-items: center; gap: 9px; margin: 0 0 12px 2px; color: var(--ink); }
.set-head svg { width: 20px; height: 20px; flex: none; }
.set-row { padding: 14px 0; border-bottom: 1px solid var(--border); }
.set-row:first-child { padding-top: 2px; }
.set-row:last-child { padding-bottom: 2px; border-bottom: 0; }
.set-row .k { font-size: .82rem; color: var(--muted); margin-bottom: 4px; }
.set-row .v { font-size: 1.05rem; color: var(--ink); word-break: break-word; }
.plan-name { font-weight: 700; font-size: 1.12rem; color: var(--ink); }
.set-div { border: 0; border-top: 1px solid var(--border); margin: 18px 0; }
/* Account page, compact (2026-09-15). Scoped to .set-container. */
.set-container .set-section { margin-bottom: 16px; }
.set-container .set-head { margin: 0 0 8px 2px; }
.set-container .set-head h2 { font-size: 1.12rem; }
.set-container .set-head svg { width: 17px; height: 17px; }
.set-container .card-pad { padding: 16px 18px; }
.set-container .set-div { margin: 12px 0; }
/* Label left, value right, one line per fact. */
.set-container .set-row { display: flex; align-items: center; justify-content: space-between; gap: 6px 14px; flex-wrap: wrap; padding: 8px 0; }
.set-container .set-row .k { margin: 0; font-size: .84rem; }
.set-container .set-row .v { font-size: .94rem; text-align: right; min-width: 0; }
.set-container .set-country { justify-content: flex-end; }
.set-container .set-country .input { padding: 5px 8px; font-size: .88rem; max-width: 200px; }
.set-row-name .v { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex: 1 1 auto; }
.set-row-name .input { flex: 1 1 140px; min-width: 0; padding: 5px 8px; font-size: .9rem; }
.set-edit { padding: 4px 8px; }
.set-edit svg { width: 13px; height: 13px; }
.set-warn { margin: 8px 0 0; color: var(--ink-2); line-height: 1.5; }
.set-more { margin-top: 8px; }
.set-more summary { cursor: pointer; font-size: .78rem; font-weight: 600; color: var(--muted-2); padding: 4px 0; width: max-content; }
.set-more summary:hover { color: var(--ink); }
.set-more p { margin: 4px 0 0; line-height: 1.55; }
.set-container .plan-name { font-size: 1.02rem; }
/* No doubled gap where a column ends: the grid gap already separates the columns. */
.set-container .set-3col { margin-bottom: 16px; }
.set-container .set-col > .set-section:last-child { margin-bottom: 0; }
.set-container .quota-rule.set-more { margin-top: 10px; padding-top: 6px; }
/* A phone: the title says what each cleaner clears; the description would wrap to three lines. */
@media (max-width: 480px) { .clr-desc { display: none; } }
/* Compact is for a mouse: on a touchscreen the small controls keep a 44px target. */
@media (pointer: coarse) { .set-container .btn-sm { min-height: 44px; } .set-more summary { padding-block: 12px; } }

/* "Clear my data" — six independent cleaners. A grid rather than six stacked rows:
   as a list it ran the best part of a screen and pushed Delete account below the fold,
   which is a lot of scrolling for something you touch once. Tiles are equal height so
   the buttons line up, and the button sits at the bottom via margin-top:auto whatever
   the description wraps to. */
.set-col { min-width: 0; }
/* Account page: identity, streak and the board sit side by side across the top, and
   "Data & account" runs the full width underneath — it holds six tiles plus the
   destructive actions, so it was always the section that wanted the room. */
.set-3col { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
/* One grid, one set of lines (2026-10-02). The columns used to size to their own
   content, so three cards ended at three different heights and no row in one card sat
   level with a row in the next. Now:
     - the columns stretch, and the LAST card in each grows to the column's foot, so
       every card bottom lands on the same line;
     - the block above the rows (photo / streak number) is one height in both cards and
       every row is one height whether it holds a button, a select or plain text, so
       Name|Today, Country|Longest streak … read straight across;
     - the two disclosures are pushed to the foot of their cards and share one height
       and one rule above, so they sit level as well. */
.set-container { --set-top-h: 64px; --set-row-h: 44px; --set-more-h: 32px; }
.set-container .set-col { display: flex; flex-direction: column; }
.set-container .set-col > .set-section:last-child { flex: 1 1 auto; display: flex; flex-direction: column; }
.set-container .set-col > .set-section:last-child > .card,
.set-container .set-col .quota-card { flex: 1 1 auto; display: flex; flex-direction: column; }
.set-container .set-photo,
.set-container .streak-panel { min-height: var(--set-top-h); padding: 0 0 8px; box-sizing: border-box; flex: none; }
.set-container .set-row { min-height: var(--set-row-h); padding: 4px 0; box-sizing: border-box; flex: none; }
/* A wrapped control makes its row two lines tall and the pitch is gone again, so on a
   narrow column (a 1280px laptop: ~277px inside the card) the select gives up width
   and the photo buttons close up instead of dropping to a second line. */
.set-container .set-row > .set-country { flex: 1 1 0; min-width: 0; }
.set-container .set-country .input { flex: 0 1 200px; min-width: 0; }
@media (min-width: 761px) { .set-container .set-row > .set-email { flex: 1 1 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } }
.set-container .set-photo { gap: 10px; }
.set-container .set-photo-actions { gap: 4px; }
.set-container .set-photo-actions .btn { padding-inline: 10px; }
.set-container .set-photo-actions .btn-ghost { padding-inline: 8px; }
.set-container .set-col .set-more,
.set-container .set-col .quota-rule.set-more { margin-top: auto; padding-top: 0; border-top: 0; }
.set-container .set-col .quota-rule.set-more { border-top: 1px solid var(--border); }
.set-container .set-col .set-more > summary { line-height: var(--set-more-h); padding: 0; }
/* The quota bars end where a row's rule would be, so the last one keeps the same
   breathing room above the disclosure that a row gives its text. */
.set-container .set-col .quota-card > .quota-item:last-of-type { margin-bottom: 12px; }
/* Touch: the 44px targets (see the compact block) make the control rows taller, so the
   shared heights grow with them rather than letting those rows drift again. */
@media (pointer: coarse) { .set-container { --set-row-h: 54px; --set-more-h: 44px; } .set-container .set-col .set-more > summary { padding-block: 0; } }
@media (max-width: 1180px) { .set-3col { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 760px)  { .set-3col { grid-template-columns: minmax(0, 1fr); } }

/* Fixed counts rather than auto-fit. Six tiles want even rows — 3x2 or 2x3 — and auto-fit
   sizes to whatever the column happens to be, which on a wide screen gave four across and
   left a ragged 4+2. The right column is a known fraction of the viewport, so counting
   here is predictable. */
/* Compact (2026-09-15): the six cleaners are ROWS in one panel — icon, what it clears, its
   button — two columns read down on a wide screen, one on a narrow one. Hairlines are the
   1px gap showing the border colour, as on the dashboard lists. */
.clr-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: column;
  grid-template-rows: repeat(var(--clr-rows, 3), auto); gap: 1px; background: var(--border);
  border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
@media (max-width: 880px) { .clr-list { grid-template-columns: minmax(0, 1fr); grid-auto-flow: row; grid-template-rows: none; } }
.clr-card { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; align-items: center; gap: 12px;
  padding: 9px 14px; background: var(--surface-2); }
.clr-ic { width: 16px; height: 16px; flex: none; color: var(--faint); }
.clr-text { display: flex; flex-direction: column; min-width: 0; }
.clr-title { font-weight: 600; font-size: .9rem; line-height: 1.25; }
.clr-desc { font-size: .76rem; line-height: 1.35; color: var(--muted-2); }
.clr-btn { flex: none; }
/* The period picker inside the confirm dialog. Boxed off from the warning above it so
   it reads as a choice being made, not as more prose. */
.clr-range { border-top: 1px solid var(--border); padding-top: 14px; }
.clr-range .pill-seg { width: 100%; }
.clr-range .pill-opt { flex: 1; padding-inline: 4px; justify-content: center; }
/* Paint the selected pill directly instead of relying on .pill-thumb. The thumb is
   positioned from the active button's offsetWidth, which reads 0 inside a modal that
   has only just been inserted, so the highlight never appeared here. A plain background
   cannot get out of step with the selection. */
.clr-range .pill-opt.active { background: var(--brand-600); color: #fff; }
.clr-range .pill-thumb { display: none; }
/* No breakpoints needed — auto-fit above reflows from three to two to one on its own,
   in the two-column layout and in the stacked one alike. */

/* "How it works" explainer — stacked numbered paragraphs (Generate Mock) */
.mock-how { display: flex; flex-direction: column; gap: 20px; }
.mock-how-item { display: flex; align-items: flex-start; gap: 14px; }
.mh-num {
  flex: none; width: 28px; height: 28px; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--brand-700);
  font-weight: 700; font-size: .9rem; display: inline-flex; align-items: center; justify-content: center;
}
.mh-body { min-width: 0; }
.mh-title { font-weight: 700; color: var(--ink); margin-bottom: 4px; }
.mock-how-item p { margin: 0; color: var(--muted); line-height: 1.6; }

/* The three mocks on offer — Full, Math, Verbal — as three plain buttons abreast.
   Eljan asked for buttons, not cards: they are the same action three times over, so the
   row reads as one control with three answers rather than three things to compare. What
   each covers is the hover title; the "How it works" card underneath carries the detail,
   and repeating any of it here is what made the earlier tile version too tall. */
.mock-btns {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px; margin-bottom: 14px;
}
/* .btn is inline-flex and centres its own content; these only need to fill their column
   and to keep the label beside the icon when the column gets narrow. */
.mock-btns .btn { width: 100%; min-width: 0; gap: 8px; }
.mock-btns-foot { margin: 0; line-height: 1.55; }
/* One per line on a phone: three buttons across 375px leaves no room for the labels. */
@media (max-width: 560px) { .mock-btns { grid-template-columns: minmax(0, 1fr); } }

/* Generate Mock, two panels: the generated mock on the left, the mock-credit shop on
   the right (.mock-split-pair below). No max-width of its own: the page takes the app's
   normal container (1560px beside the sidebar), because two panels at 1180 left both
   cards cramped against a wall of empty page. Below 980px the two stack. */
.mock-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: start; }
@media (max-width: 980px) { .mock-split { grid-template-columns: minmax(0, 1fr); } }
/* One panel only (no mock credits on sale): a single centred column, capped so the
   cards do not run the full 1560px container. */
.mock-split-solo { grid-template-columns: minmax(0, 1fr); max-width: 860px; margin-inline: auto; }

/* ---- Mock credits ("pay and get a mock", views.js mockShopCard) ----
   The card is the mirror of "Generate a mock" and reuses its .mock-btns row, so the pair
   stays the same shape. Stretched to one height; each card centres its own content. */
.mock-split-pair { align-items: stretch; }
.mock-split-pair .mock-col { display: flex; flex-direction: column; }
.mock-split-pair .mock-col > .card { flex: 1; display: flex; flex-direction: column; justify-content: center; }
/* What it buys on the left, what it costs on the right — one button, one decision. */
.mshop-btn { justify-content: center; gap: 10px; }
.mshop-btn b { font-variant-numeric: tabular-nums; color: var(--brand-700); }
.mshop-btn b:empty { display: none; }
.mshop-btn:disabled b { color: inherit; }
.mshop-meter { text-align: left; margin-top: 2px; }
.mshop-meter .quota-item-top { margin-bottom: 7px; }
/* "How it works" under the pair: it has the whole container, so the steps sit three
   abreast instead of one 1500px-wide line each. */
@media (min-width: 720px)  { .mock-how-wide .mock-how { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 32px; } }
@media (min-width: 1180px) { .mock-how-wide .mock-how { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* Break-screen note: keep it on ONE line on normal screens; wrap only on narrow phones so it never clips */
.break-note { white-space: nowrap; }
@media (max-width: 700px) { .break-note { white-space: normal; max-width: 40ch; margin-inline: auto; text-wrap: balance; } }

/* empty state */
.empty { text-align: center; padding: 56px 24px; }
.empty .big-icon { background: var(--surface-2); color: var(--faint); }

/* skeleton */
.skel { background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
  background-size: 400% 100%; animation: shimmer 1.4s ease infinite; border-radius: var(--r-sm); }

/* ======================================================================
   ANIMATIONS
   ====================================================================== */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes toastIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
@keyframes modalIn { from { opacity: 0; transform: translateY(18px) scale(.97); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes popIn { from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes navDrop { from { opacity: 0; transform: translateY(-18px); } to { opacity: 1; transform: translateY(0); } }

.spinner { width: 20px; height: 20px; border: 2.5px solid var(--brand-200); border-top-color: var(--brand-600); border-radius: 50%; animation: spin .7s linear infinite; }
.btn .spinner { width: 15px; height: 15px; border-width: 2px; vertical-align: -2px; }

/* Score report notice when a module could not be marked (connection dropped mid-test). */
.sr-pending { border-color: var(--warn); background: color-mix(in srgb, var(--warn) 8%, var(--surface)); }

/* Veil shown while an exam's data is fetched (exams load on demand, not up front).
   app.js only mounts this after 220 ms, so a cached exam opens without a flash. */
.load-veil { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center;
  background: color-mix(in srgb, var(--bg) 72%, transparent); backdrop-filter: blur(3px);
  animation: fadeIn .18s var(--ease-out); }
.load-veil-box { display: flex; align-items: center; gap: 12px; padding: 16px 22px; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow-lg);
  color: var(--ink); font-weight: 600; font-size: .95rem; }

/* scroll reveal — One UI-style fluid rise with soft settle */
/* No .d1–.d4 stagger classes: they only ever ran on landing's feature grid, and a
   2×2 grid is not a sequence — dealing the cards out one by one implied an order
   the content does not have, and made the last card wait on the first. */
.reveal { opacity: 0; transform: translateY(28px); transition: opacity .65s var(--ease-out), transform .9s var(--ease-spring); }
.reveal.in { opacity: 1; transform: none; }

/* view transition for SPA */
.view-enter { animation: viewIn var(--t-slow) var(--ease-spring); }
@keyframes viewIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* 🔴 Moving to the next QUESTION is not a change of view. It used to replay viewIn:
   480ms, a 14px slide and a spring overshoot, applied to the whole two-pane body — so
   anyone stepping through questions faster than that (which is most people skimming for
   the ones they flagged) read a passage that was permanently mid-flight, and every step
   re-laid-out and re-composited a full screen of text. The change is confirmed with a
   short fade instead: opacity only, so nothing moves and nothing is laid out, and it is
   finished before the next click can land. Reduced motion still flattens it (2143). */
.q-enter { animation: qIn var(--t-fast) var(--ease-out); }
@keyframes qIn { from { opacity: 0; } to { opacity: 1; } }

/* ======================================================================
   LANDING-SPECIFIC
   ====================================================================== */
/* The old hero and its blurred colour blobs are gone with the section they served.
   Deleted rather than left dormant: a pair of 70px-blurred pastel circles behind a
   headline is the stock generated-landing background, and dead CSS creeps back. */

/* .pill-stat and .avatars went with the hero: a stat pill beside a row of overlapping
   blank avatar circles is social proof for users we never had. */

.feature-icon { width: 50px; height: 50px; border-radius: 13px; display: grid; place-items: center; margin-bottom: 16px; }
.feature-icon svg { width: 24px; height: 24px; }
.fi-ok { background: var(--ok-bg); color: var(--ok); }

/* The 3D-tilted question-preview card that used to sit in the hero is gone too —
   another stock landing device, and it outlived the section it belonged to.
   NOTE: .mock-how / .mock-how-item below are a different thing entirely; views.js
   renders them on the Generate Mock page. Do not sweep them up with these. */

/* .logo-strip is gone with the trust strip it styled — a row of logo wordmarks
   implying partners EliteXSAT does not have. */

/* The FAQ accordion styles went with the FAQ section. If one is ever written again,
   author it fresh rather than reviving this — the markup it assumed is long gone. */

/* .cta-band deleted with the closing CTA section. It was a 135deg crimson gradient
   slab — the one stock-gradient surface left on the site. */

/* ======================================================================
   RESPONSIVE
   ====================================================================== */
@media (max-width: 980px) {
  .footer-grid { grid-template-columns: 1fr 1fr; gap: 30px; }
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .section { padding-block: 60px; }
  .brand-logo { height: 38px; }
  .brand-name { font-size: 1.12rem; }
  .engine-body { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr); }
  .engine-body.two-pane { display: flex; flex-direction: column; }
  .pane-left { border-right: none; border-bottom: 1px solid var(--border); max-height: 42%; }
  .pane { padding: 22px 20px 48px; }
  .footer-grid { grid-template-columns: 1fr; }
  .grid-3, .grid-2, .grid-stats { grid-template-columns: minmax(0, 1fr); }
  .container { padding-inline: 18px; }
  .engine-top, .engine-bottom { padding-inline: 14px; }
  .engine-top .et-title small { display: none; }
  .btn-lg { padding: 13px 22px; }
}
@media (max-width: 460px) {
  .nav-pill span.label-full { display: none; }
  .timer { font-size: .95rem; padding: 6px 12px; }
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
  .reveal { opacity: 1; transform: none; }
}

/* print results */
@media print {
  .sidebar, .app-topbar, .engine-bottom, .engine-top, .btn, .seg { display: none !important; }
  body { background: #fff; }
  body.has-sidebar .app-main { padding-left: 0 !important; }
}

/* ======================================================================
   APP SIDEBAR — One UI grouped "windows"
   ====================================================================== */
body.has-sidebar { background:
   radial-gradient(1200px 600px at -10% -10%, rgba(209,29,43,.06), transparent 60%),
   radial-gradient(1000px 500px at 110% 10%, rgba(8,145,178,.05), transparent 55%), var(--bg); }
body.has-sidebar .app-main { padding-left: 260px; padding-top: 24px; }
/* App content hugs the sidebar (drop the big centering gap) and gets more width for 5-up grids */
body.has-sidebar .app-main .container { --container: 1560px; margin-inline: 0; }

.sidebar {
  position: fixed; left: 16px; top: 16px; bottom: 16px; width: 228px; z-index: 60;
  display: flex; flex-direction: column; gap: 14px; overflow-y: auto; overflow-x: hidden;
  padding-right: 2px; animation: navDrop .7s var(--ease-spring) both; scrollbar-width: thin;
}
/* Sidebar scrollbar — ALWAYS visible, its space ALWAYS reserved (Eljan, 2026-10-20, both
   EliteXSAT and EliteXChina). It used to show only on hover, so on a laptop short enough to
   make the sidebar scroll, nothing said there was more below. scrollbar-gutter: stable keeps
   the bar's width reserved even when the sidebar fits, so the sidebar is the same width on
   every screen and on both sites (a bar that appears only when needed made the windows a few
   px narrower on some screens than on others). Standard properties only: with scrollbar-color
   set, Chrome ignores the ::-webkit-scrollbar rules, so those are not used any more. */
.sidebar { scrollbar-gutter: stable; scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--muted-2) 55%, transparent) transparent; }
.sidebar:hover { scrollbar-color: var(--muted-2) transparent; }

.side-win {
  background: rgba(255, 255, 255, .72);
  backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, .9);
  border-radius: 16px; padding: 3px 9px;
  box-shadow: 0 14px 40px -18px rgba(15, 23, 42, .26), 0 3px 10px -6px rgba(15, 23, 42, .10);
}
.side-win-logo { padding: 12px 14px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.side-logo { display: flex; align-items: center; gap: 12px; cursor: pointer; text-decoration: none; border-radius: 12px;
  transition: transform .5s var(--ease-spring); transform-style: preserve-3d; }
/* Promote on hover, not permanently: an unconditional will-change kept a compositor
   layer alive on every screen for a transform that only runs while pointing at it. */
.side-logo:hover, .side-logo:focus-visible { will-change: transform; }
.side-logo:hover .brand-logo { filter: drop-shadow(0 8px 16px rgba(176, 18, 31, .5)); }
.side-logo:active { transform: scale(.96); }
/* No idle float here. An infinite decorative animation sat in the corner of every
   screen a student reads a timed question on, and it never stopped. */
.side-logo .brand-logo { height: 60px; width: auto; display: block; filter: drop-shadow(0 6px 12px rgba(176, 18, 31, .34)); }
.side-logo .brand-name { font-size: 1.42rem; font-weight: 800; letter-spacing: -.02em; color: var(--ink); }
.side-logo .brand-name b { color: var(--brand-600); }

.side-nav { display: flex; flex-direction: column; gap: 3px; position: relative; }
.side-pill { position: absolute; left: 0; right: 0; top: 0; height: 0; border-radius: 10px;
  background: var(--brand-600); box-shadow: var(--shadow-brand); z-index: 0; opacity: 0; pointer-events: none;
  transition: transform var(--t-slow) var(--ease-spring), height var(--t) var(--ease), opacity var(--t); }
.side-pill.show { opacity: 1; }
.side-item {
  display: flex; align-items: center; gap: 13px; padding: 11px 13px; border-radius: 10px;
  color: var(--muted); font-weight: 600; font-size: .93rem; cursor: pointer; width: 100%;
  text-align: left; border: none; background: transparent; position: relative; z-index: 1;
  transition: background var(--t) var(--ease), color var(--t-fast), transform var(--t-fast) var(--ease-spring);
}
.side-item svg { width: 21px; height: 21px; flex-shrink: 0; transition: transform var(--t) var(--ease-spring); }
.side-item:hover { background: var(--surface-2); color: var(--ink); }
.side-item:hover svg { transform: scale(1.12); }
.side-item:active { transform: scale(.97); }
.side-item.active { background: transparent; color: #fff; box-shadow: none; }
.side-item.active:hover { background: transparent; color: #fff; }
.side-item.active svg { color: #fff; }
/* Appearance (left) + Full screen (right) on one row, icons only. Each half is still a
   full .side-item, so it keeps the same height, hover pill and press response as every
   other row — only the caption and the horizontal padding come off. The hairline
   between them is what says "two controls" once the words are gone. */
.side-duo { display: flex; align-items: stretch; gap: 5px; }
.side-duo .side-item { flex: 1 1 0; justify-content: center; padding-inline: 0; gap: 0; }
.side-duo .side-item .si-label { display: none; }
/* Height strut. The caption's line box, not the 21px icon, is what sets the height of
   every other row — drop the caption and this row sits ~3px shorter than the stack it
   belongs to. An empty box of exactly that line height puts it back, and scales with
   the font instead of hard-coding the pixels. */
.side-duo .side-item::after { content: ""; width: 0; height: 1.6em; }
.side-duo .side-item + .side-item::before {
  content: ""; position: absolute; left: -3px; top: 22%; bottom: 22%; width: 1px;
  background: var(--border); z-index: 1;
}
.side-spacer { flex: 1 1 auto; min-height: 4px; }
/* Sidebar day counter, the first row of the streak window. Grey while the run is alive
   but today is still unclaimed, amber once it is — the colour is the reward, so it has
   to mean "done", not "exists". */
.streak-row { display: flex; align-items: center; gap: 13px; padding: 9px 13px 11px; }
/* The counters sit above the Leaderboard link, so the hairline goes under the PAIR — on
   the XP row, which is the last of them. Left on .streak-row it would also draw a rule
   between the day count and the XP count, splitting one block of "work done" into two. */
.side-win-streak .streak-row { border-bottom: none; margin-bottom: 0; }
.side-win-streak .xp-row { border-bottom: 1px solid var(--border); margin-bottom: 3px; }
.streak-flame { display: inline-flex; flex: none; color: var(--muted-2); transition: color var(--t); }
.streak-flame svg { width: 21px; height: 21px; }
.streak-row.lit .streak-flame { color: var(--warn); }
/* The day the run actually grows. The flame is a static state marker every other day of
   the week, so this is the one moment it earns motion: it flares once, warm, and settles
   into its lit colour. Bounded — nothing here loops. */
@keyframes streakFlare {
  0% { transform: scale(1); filter: none; }
  30% { transform: scale(1.34); filter: drop-shadow(0 0 9px color-mix(in srgb, var(--warn) 75%, transparent)); }
  100% { transform: scale(1); filter: none; }
}
.streak-grew .streak-flame { animation: streakFlare .68s var(--ease-out); }
.streak-text { font-size: .82rem; color: var(--muted); line-height: 1.25; min-width: 0; }
.streak-text b { font-size: 1.02rem; color: var(--ink); font-variant-numeric: tabular-nums; }
/* No run going. The flame is already grey here (only .lit turns it amber), but the count
   itself steps back too, so a live streak still reads louder than a dormant one — while
   the row stays plainly legible, which is the whole point of showing the zero. */
.streak-row.streak-none .streak-text b { color: var(--muted); }
/* Went out, but still relightable today. This is the one state that asks for something,
   so unlike the dormant zero above it does NOT step back: the number stays at full
   strength (it is the thing coming back) and the sub-line takes the brand colour so
   "relight today" reads as the call it is. The struck-through flame carries the rest. */
.streak-row.streak-out .streak-flame { color: var(--muted-2); opacity: .8; }
.streak-row.streak-out .streak-text small { color: var(--brand-link, var(--brand-600)); font-weight: 600; }
.streak-text small { font-size: .7rem; color: var(--muted-2); }

/* XP borrows .streak-row wholesale, so only the glyph is defined here. The bolt greys
   out at zero exactly as the flame does: the colour means "there is something here",
   not "this row exists". --brand-link, not --brand-600 — the latter sits at 3.28:1 on
   the dark surface, which is under AA for a 21px glyph that is carrying state. */
.xp-bolt { display: inline-flex; flex: none; color: var(--muted-2); transition: color var(--t); }
.xp-bolt svg { width: 21px; height: 21px; }
.xp-row.earning .xp-bolt { color: var(--brand-link); }
.xp-row:not(.earning) .streak-text b { color: var(--muted); }
/* The XP row is a <button> (it opens "How XP works"), so the browser's button styling has
   to be reset back to the row the day counter beside it still is — full width, left
   aligned, inherited type. Only the pointer and the hover say it is pressable. */
.xp-row { width: 100%; border: none; background: none; font: inherit; text-align: left; cursor: pointer;
  border-radius: 10px; transition: background var(--t); }
.side-win-streak .xp-row:hover, .side-win-streak .xp-row:focus-visible { background: var(--surface-2); }
/* The chevron only appears on hover/focus: at rest the row is a readout, and a permanent
   affordance next to the day counter would make the pair look like two different objects. */
.xp-more { margin-left: auto; display: inline-flex; color: var(--muted-2); opacity: 0;
  transform: translateX(-3px); transition: opacity var(--t), transform var(--t); }
.xp-more svg { width: 16px; height: 16px; }
.xp-row:hover .xp-more, .xp-row:focus-visible .xp-more { opacity: 1; transform: none; }
@media (pointer: coarse) { .xp-more { opacity: 1; transform: none; } }
body.sidebar-collapsed .xp-more { display: none; }

/* ---- "How XP works" dialog ----
   Totals, two short rate lists side by side, and the repeat ladders — sized to fit a
   720px laptop screen without scrolling. The max-height stays as a floor for short
   phones: .modal neither caps its height nor scrolls, so the CONTENT scrolls inside its
   own box, which keeps the title and the "Got it" button on screen. svh alongside vh:
   on a phone the toolbar counts. */
.xp-info { max-height: calc(100vh - 268px); max-height: calc(100svh - 268px); overflow-y: auto; min-height: 0; padding-right: 8px; }
.xp-tot { display: flex; align-items: baseline; gap: 10px 22px; flex-wrap: wrap;
  padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); }
.xp-tot-main b { font-size: 1.9rem; font-weight: 800; color: var(--ink); font-variant-numeric: tabular-nums; }
.xp-tot-main { font-size: .9rem; color: var(--muted); }
.xp-tot-side { font-size: .82rem; color: var(--muted); }
.xp-tot-side b { font-size: 1.1rem; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.xp-tot span { display: block; font-size: .72rem; color: var(--muted-2); }
.xp-board { margin-left: auto; align-self: center; display: inline-flex; align-items: center; gap: 2px;
  font-size: .84rem; font-weight: 700; color: var(--brand-link); white-space: nowrap; }
.xp-board svg { width: 15px; height: 15px; }
.xp-h { margin: 16px 0 4px; font-family: var(--font-ui); font-size: .95rem; font-weight: 700; color: var(--ink); }
.xp-p { line-height: 1.6; margin: 0 0 10px; }
/* The two places XP comes from, next to each other so the rates can be compared. */
.xp-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }
.xp-cap { margin-top: 6px; font-size: .72rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted-2); }
/* Rate rows, not a data table to study: hairlines between, figures right and tabular so
   they line up under each other. */
.xp-tbl { width: 100%; border-collapse: collapse; font-size: .84rem; }
.xp-tbl td { padding: 6px 0; border-bottom: 1px solid var(--border); color: var(--ink-2); }
.xp-tbl tr:last-child td { border-bottom: none; }
.xp-tbl td:last-child { text-align: right; white-space: nowrap; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums; }
/* The repeat ladders: one row per source, the same four columns for both. */
.xp-lad { display: grid; grid-template-columns: minmax(0, 1fr) repeat(4, 58px); gap: 6px; align-items: center; font-size: .84rem; }
.xp-lad-h { text-align: center; font-size: .72rem; font-weight: 600; color: var(--muted-2); }
.xp-lad-name b { display: block; font-weight: 600; color: var(--ink-2); }
.xp-lad-name small { display: block; font-size: .74rem; color: var(--muted-2); }
.xp-step { padding: 6px 0; text-align: center; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums;
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2); }
.xp-step.full { background: var(--brand-50); border-color: var(--brand-200); color: var(--brand-link); }
.xp-step.none { font-weight: 600; color: var(--muted-2); }
@media (max-width: 480px) {
  .xp-cols { grid-template-columns: 1fr; }
  .xp-cols .xp-col + .xp-col { margin-top: 10px; }
  .xp-lad { grid-template-columns: minmax(0, 1fr) repeat(4, 44px); gap: 4px; font-size: .8rem; }
}

/* Today's Digital SAT / Mock allowance, on the Account page's "Today's limit" card.
   Brand-coloured fill while a slot is open; once used up it turns the same amber the
   lit streak flame uses elsewhere, so "spent for today" reads as the same kind of
   state rather than inventing a second colour language for it. */
.quota-card .quota-item + .quota-item { margin-top: 12px; }
.quota-rule { line-height: 1.55; margin: 12px 0 0; padding-top: 10px; border-top: 1px solid var(--border); }
.quota-item-top { display: flex; align-items: baseline; justify-content: space-between;
  font-size: .86rem; color: var(--ink-2); margin-bottom: 6px; }
.quota-item-top b { font-size: .86rem; color: var(--ink); font-variant-numeric: tabular-nums; }
.quota-bar { height: 7px; border-radius: 3px; background: var(--border); overflow: hidden; }
.quota-bar-fill { height: 100%; border-radius: inherit; background: var(--brand-link);
  transition: width var(--t); }
.quota-item.quota-done .quota-bar-fill { background: var(--warn); }
.quota-item.quota-done .quota-item-top b { color: var(--warn); }

/* Unfinished tests on the Data page — up to three at once, so this is a list rather than
   the single gradient banner it replaced. A brand edge marks it as the one thing on the
   page that is still open, without shouting over the score cards under it. */
.resume-panel { border-left: 3px solid var(--brand-link); }
.resume-head { display: flex; align-items: center; gap: 11px; margin-bottom: 10px; }
.resume-ic { display: inline-flex; flex: none; color: var(--brand-link); }
.resume-ic svg { width: 21px; height: 21px; }
.resume-title { font-weight: 700; color: var(--ink); }
.resume-row { display: flex; align-items: center; justify-content: space-between; gap: 14px;
  flex-wrap: wrap; padding: 11px 0 0; margin-top: 11px; border-top: 1px solid var(--border); }
.resume-name { font-weight: 600; color: var(--ink); }
.resume-meta { margin-top: 2px; }

/* The weekly-reset line in the "clear my data" dialog. Empty until the answer arrives,
   so it collapses rather than reserving a blank row above the buttons. */
.clr-ration { line-height: 1.55; margin: 10px 0 0; }
.clr-ration:empty { display: none; }
.clr-ration.is-blocked { color: var(--bad); font-weight: 600; }
body.sidebar-collapsed .streak-text { display: none; }
body.sidebar-collapsed .streak-row { justify-content: center; padding: 10px 0; }

.side-account { display: flex; align-items: center; gap: 11px; padding: 9px 11px; border-radius: 10px; cursor: pointer; transition: background var(--t); width: 100%; border: none; background: transparent; text-align: left; position: relative; z-index: 1; }
.side-account:hover { background: var(--surface-2); }
.side-account.active, .side-account.active:hover { background: transparent; }
.side-account.active .sa-name, .side-account.active .sa-mail { color: #fff; }
.side-account .sa-name { font-weight: 600; font-size: .9rem; color: var(--ink); line-height: 1.1; }
.side-account .sa-mail { font-size: .72rem; color: var(--muted-2); }

/* The sidebar breathes with the window instead of snapping at a breakpoint.

   Its natural height is 739px, so any viewport under ~771px made it scroll — which is every
   1366x768 laptop, the commonest machine a student here owns. A max-height breakpoint fixed
   that but only at the one size it named: a 700px window and a 1400px window would have got
   identical spacing, one still cramped and the other wasting half its height.

   So every vertical measurement is a clamp on vh. It shrinks continuously as the window
   gets shorter and stops at a floor that is still comfortable to hit with a mouse; on a tall
   screen every value sits at its maximum, which is exactly what the fixed numbers used to
   be, so nothing changes for anyone who had room already.

   min-width keeps this off the phone drawer, where the sidebar is a full-height overlay and
   the viewport height means something different. */
@media (min-width: 901px) {
  .sidebar { gap: clamp(6px, 1.2vh, 14px); }
  .side-logo .brand-logo { height: clamp(34px, 6vh, 60px); }
  .side-win { padding-block: clamp(2px, 0.35vh, 3px); }
  .side-win-logo { padding-block: clamp(6px, 1.2vh, 12px); }
  .side-item { padding-block: clamp(6px, 1.05vh, 11px); }
  .side-account { padding-block: clamp(6px, 1.1vh, 9px); }
  .streak-row { padding-block: clamp(5px, 1vh, 9px) clamp(6px, 1.2vh, 11px); }
}

/* mobile top bar (only shows on narrow screens) */
.app-topbar { display: none; }

/* ======================================================================
   VOCABULARY FLASHCARD
   ====================================================================== */
.flash { perspective: 1600px; max-width: none; margin: 0 auto; }
.flash-inner { position: relative; width: 100%; min-height: 400px; transform-style: preserve-3d; transition: transform .6s var(--ease-spring); cursor: pointer; }
.flash.flipped .flash-inner { transform: rotateY(180deg); }
.flash-face { position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center;
  padding: 32px; border-radius: var(--r-xl); border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow-md); }
.flash-back { transform: rotateY(180deg); background: var(--brand-50); }
/* The back of a card is a dictionary entry — a definition, an example sentence, senses,
   synonyms — so in the dark theme it takes the same warm reading surface the Reading
   Book uses. --brand-50 resolves to rgba(209,29,43,.18) here, which put a maroon wash
   behind a block of prose meant to be read. The light theme keeps the pale brand tint:
   there --brand-50 is a near-white #fff1f2 that never fought the text. */
[data-theme="dark"] .flash-back { background: var(--paper); }
.flash-word { font-family: var(--font-display); font-size: 3rem; font-weight: 500; color: var(--ink); }
.flash-hint { font-size: .84rem; color: var(--faint); margin-top: 22px; }
.flash-camb { display: inline-flex; align-items: center; gap: 6px; margin-top: 16px; font-size: .82rem;
  font-weight: 600; color: var(--brand-600); text-decoration: none; }
.flash-camb:hover { text-decoration: underline; }
.flash-camb svg { width: 14px; height: 14px; }
/* On a CEFR back face (which reuses .sv-back) the link sits at the bottom rather than
   under the example. When a synonyms/antonyms block is present that block claims the
   auto margin first, so the link just tucks under it. */
.sv-back .flash-camb { margin-top: auto; align-self: center; padding-top: 16px; }
.sv-back .svb-sa ~ .flash-camb { margin-top: 12px; padding-top: 0; }
/* Cambridge link + the CC BY-SA credit the definitions are licensed under. */
.flash-foot { margin-top: auto; padding-top: 14px; display: flex; flex-direction: column;
  align-items: center; gap: 3px; }
.svb-sa ~ .flash-foot { margin-top: 12px; padding-top: 10px; }
.flash-foot .flash-camb { margin-top: 0; padding-top: 0; }
/* A CEFR back is a full dictionary entry — pronunciation, the main meaning, the
   word's other meanings, then synonyms/antonyms — so it gets a taller card and
   scrolls internally rather than spilling past its rounded corners. */
.flash-tall .flash-inner { min-height: 620px; }
.cefr-back { padding: 22px 26px 20px; overflow-y: auto; overscroll-behavior: contain; }
.cefr-back .svb-main { margin-top: 16px; }
.cefr-back .svb-def { font-size: 1.15rem; }
.cefr-back .svb-sa { padding-top: 16px; }

/* Position in the run ("12 / 1,448" + a thin bar) across the top of the card. On .flash,
   outside the rotating .flash-inner, so it stays put while the card turns; the faces
   start below it. pointer-events:none so a tap on the line still flips the card. */
.flash { position: relative; }
.flash-pos { position: absolute; top: 14px; left: 22px; right: 22px; z-index: 2; pointer-events: none;
  display: flex; align-items: center; gap: 10px; }
.flash-pos .bar { flex: 1; height: 4px; }
.flash-pos .bar > span { background: var(--lc, var(--brand-600)); }
.flash-pos-n { font-size: .74rem; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.flash:has(.flash-pos) .flash-face { padding-top: 44px; }

/* Part of speech + IPA + the pronunciation button, on one centred row. */
.svb-top { display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; }
.svb-ipa { font-size: .84rem; color: var(--muted); letter-spacing: .01em; }
.svb-audio { display: inline-grid; place-items: center; width: 26px; height: 26px; padding: 0; cursor: pointer;
  border-radius: 999px; border: 1px solid var(--border); background: var(--surface); color: var(--brand-600); }
.svb-audio:hover { background: var(--brand-50); border-color: var(--brand-600); }
.svb-audio svg { width: 14px; height: 14px; }

/* Deck controls sit in a column BESIDE the card, not under it. The card is 620px tall,
   so nav underneath fell below the fold on a laptop and the student had to scroll to
   reach Next on every single word. Alongside, all three stay in reach while the card
   is read. The column holds only the two recall answers (Still learning / Know) and is
   centred on the card; plain navigation (Prev, Shuffle) sits in .flash-below, under the
   card, where it was before.

   🔴 align-items MUST stay `stretch`. The card has no intrinsic size of its own — its
   faces are position:absolute and the height comes from .flash-inner's min-height — so
   under `flex-start` it collapses to zero width the moment the stage turns into a column
   on mobile. The nav is positioned with align-self instead. */
.flash-stage { display: flex; align-items: stretch; gap: 18px; }
/* margin:0 overrides .flash's own `margin: 0 auto`. An auto margin on the cross axis
   beats align-items:stretch, so the card would size to its content — and its content is
   two position:absolute faces, i.e. zero. Belt and braces: width:100% in the column case. */
.flash-stage > .flash { flex: 1 1 auto; min-width: 0; margin: 0; }
.flash-nav { display: flex; flex-direction: column; justify-content: center;
             align-self: center; flex: 0 0 170px; gap: 12px; }
.flash-nav .btn { width: 100%; justify-content: center; }

/* Recall scoreboard above the card: how far through the deck, and how the answers split.
   Sized to the card (not the stage) with the same padding-right the below-row uses, so the
   three panels line up with the card edges rather than floating over the button column. */
.flash-stats { display: grid; grid-template-columns: 1.3fr 1fr 1fr 1fr; gap: 12px;
               margin-bottom: 16px; padding-right: 188px; }
/* Each panel is a <button> (it filters the run), so the browser's own button styling has
   to be reset back to a plain card: full width, left-aligned, inherited font. */
.fs-item { background: var(--surface); border: 1px solid var(--border);
           border-radius: var(--r-lg); box-shadow: var(--shadow-sm); padding: 12px 16px;
           width: 100%; text-align: left; font: inherit; color: inherit; cursor: pointer;
           display: block; transition: border-color var(--t-fast), background var(--t-fast); }
.fs-item:hover { border-color: var(--border-2); }
/* The active filter. Each panel lights in its own colour so the strip says both "this is
   the filter" and "this is the bucket" at once.
   The ring is an INSET SHADOW, not a thicker border: border-width would change the box and
   the grid columns would shift by a few pixels as the student clicks between filters
   (worse on mobile, where the padding compensation is overridden). A shadow costs no layout. */
.fs-total.on { border-color: var(--brand-600); box-shadow: inset 0 0 0 2px var(--brand-600);
               background: color-mix(in srgb, var(--brand-600) 7%, var(--surface)); }
.fs-know.on  { border-color: var(--ok);   box-shadow: inset 0 0 0 2px var(--ok);
               background: color-mix(in srgb, var(--ok) 9%, var(--surface)); }
.fs-learn.on { border-color: var(--warn); box-shadow: inset 0 0 0 2px var(--warn);
               background: color-mix(in srgb, var(--warn) 9%, var(--surface)); }
/* Same blue as an exam card nobody has opened yet (--exam-line): both mean "untouched". */
.fs-fresh.on { border-color: var(--exam-line); box-shadow: inset 0 0 0 2px var(--exam-line);
               background: color-mix(in srgb, var(--exam-line) 9%, var(--surface)); }
.fs-lab { font-size: .72rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
          color: var(--muted); }
.fs-num { font-family: var(--font-display); font-size: 1.5rem; font-weight: 600; color: var(--ink);
          line-height: 1.2; margin-top: 2px; }
.fs-of { font-size: .95rem; font-weight: 500; color: var(--muted); }
.fs-total .bar { margin-top: 8px; }
.fs-know .fs-num { color: var(--ok, #1e8e5a); }
.fs-learn .fs-num { color: var(--warn, #d98324); }
.fs-fresh .fs-num { color: var(--exam-line); }

/* Deck navigation, under the card and centred: this is where Shuffle lived originally
   and it is not a recall answer, so it stays out of the pair on the right.
   The padding-right is the nav column (170px) plus the stage gap (18px): without it the
   row centres on the whole stage and sits ~94px right of the card it belongs to. */
.flash-below { display: flex; justify-content: center; gap: 12px; margin-top: 18px;
               flex-wrap: wrap; padding-right: 188px; }

/* The two answers are colour-coded and the code carries through to the card itself, so a
   student paging back reads the state at a glance instead of hunting for a lit button.
   Same two colours and the same 7% tint the exam catalogue uses for resume/completed —
   see .test-card-resume / .test-card-done — so the whole site means one thing by amber
   and one thing by green. */
.flash-know  { --btn-bg: var(--ok);   --btn-fg: #fff; border-color: transparent; box-shadow: var(--shadow-sm); }
.flash-know:hover  { --btn-bg: color-mix(in srgb, var(--ok) 86%, #000); }
.flash-learn { --btn-bg: var(--warn); --btn-fg: #fff; border-color: transparent; box-shadow: var(--shadow-sm); }
.flash-learn:hover { --btn-bg: color-mix(in srgb, var(--warn) 86%, #000); }
/* The card that has already been answered: edge in the answer's colour, and the FRONT
   face tinted to match. The back face keeps its own reading background untouched. */
.flash-known .flash-face:not(.flash-back) {
  border-color: var(--ok); background: color-mix(in srgb, var(--ok) 7%, var(--surface)); }
.flash-learning .flash-face:not(.flash-back) {
  border-color: var(--warn); background: color-mix(in srgb, var(--warn) 7%, var(--surface)); }
/* The answer already given is the one ringed, so the pair does not read as two live options. */
.flash-know.on { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 32%, transparent); }
.flash-learn.on { box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 32%, transparent); }

/* Below ~760px there is no room for a side column: drop back to a row under the card
   (the original layout), still one group, still wrapping rather than clipping Next. */
@media (max-width: 760px) {
  .flash-stage { flex-direction: column; }
  .flash-stage > .flash { width: 100%; }
  .flash-nav { align-self: stretch; }
  .flash-below { margin-top: 12px; padding-right: 0; }   /* no side column to offset against */
  .flash-stats { padding-right: 0; gap: 8px; }
  .fs-item { padding: 10px 12px; }
  .fs-num { font-size: 1.2rem; }
  /* Still learning / Know are the primary pair, so on a phone they split the full width
     side by side rather than hugging the right edge. */
  .flash-nav { flex: 0 0 auto; flex-direction: row; justify-content: center;
               margin-top: 20px; }
  .flash-nav .btn { flex: 1 1 0; width: auto; }
}

/* SAT deck extras: the inflected forms the exam used, and how often the word has
   been keyed — the frequency is the reason the decks are ordered the way they are. */
.svb-forms { margin-top: 14px; font-size: .8rem; color: var(--muted-2); display: flex;
  flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px; }
.svb-freq { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border);
  font-size: .82rem; color: var(--muted); }
.svb-freq b { color: var(--ink-2); font-weight: 700; }
/* "+128" chip closing a preview list on a deck card. */

/* The word's other meanings, listed under the main one. */
.svb-more { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border); text-align: left; }
.svb-more .svb-lab { color: var(--muted-2); text-align: left; }
.svb-mrow { font-size: .86rem; line-height: 1.5; color: var(--ink-2); }
.svb-mrow + .svb-mrow { margin-top: 9px; }
.svb-mpos { display: inline-block; margin-right: 7px; padding: 1px 7px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface); color: var(--muted-2);
  font-size: .66rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; vertical-align: 1px; }
.svb-mex { display: block; margin-top: 3px; font-size: .8rem; font-style: italic; color: var(--muted); }
/* A full entry that fits a 780px card wraps to roughly twice the height on a phone,
   so tighten the type and padding there; whatever still runs over scrolls. */
@media (max-width: 560px) {
  .cefr-back { padding: 18px 18px 16px; }
  .cefr-back .svb-def { font-size: 1.05rem; }
  .cefr-back .svb-main { margin-top: 12px; }
  .svb-more { margin-top: 14px; padding-top: 11px; }
  .svb-mrow { font-size: .82rem; }
  .svb-mex { font-size: .77rem; }
  .svb-sa { gap: 12px; }
}

/* SAT Vocab flashcard back — part of speech at the top, meaning up high,
   synonyms + antonyms split across the bottom. */
.sv-back { justify-content: flex-start; align-items: stretch; text-align: center; padding: 26px 30px 24px; }
.svb-pos { align-self: center; display: inline-block; font-size: .74rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; padding: 4px 13px; border-radius: 999px; }
/* Which deck the word came from ("Very common", "CEFR B2"). It used to sit above the
   card as a page badge; moved inside, left of the part-of-speech pill, so the header
   row could go entirely. Lighter weight than .svb-pos so the two never compete. */
.svb-deck { align-self: center; display: inline-block; font-size: .72rem; font-weight: 600;
  letter-spacing: .02em; padding: 4px 12px; border-radius: 999px; white-space: nowrap; }
/* The same deck/part-of-speech/IPA row repeated on the FRONT face, under the word, so a
   student can check the sense and pronunciation without flipping. It reuses .svb-top for
   the layout and only adds the spacing that separates it from the word and the hint. */
.flash-meta { margin-top: 18px; }
/* Transcription + speaker as one unit: on a phone the row wraps, and the speaker on a
   line of its own under the IPA reads as a stray icon. */
.svb-say { display: inline-flex; align-items: center; gap: 10px; }
/* Transitions: the Groups row. Rarity moved up beside the tabs, and with one row left the
   "GROUPS" label went too — the option names say what it selects. */
.tr-filter { display: flex; align-items: center; margin-bottom: 16px; min-width: 0; }
.tr-filter .pill-seg { min-width: 0; }
/* Year and month sit on one row and wrap together on a narrow screen rather than one
   bar dropping under the other and reading as unrelated. */
.sat-when { gap: 10px; flex-wrap: wrap; }
/* The line that says what the selected band actually means. The rule above it separates
   the controls from what they produced — without it the two filter rows and the result
   read as one undifferentiated block of small text. */
/* No rule above this note. The pill-segs over it are already enclosed objects with
   their own surface and border, so a hairline underneath drew the same boundary twice;
   and the note is small and muted enough to read as secondary on its own. The 14px of
   padding stays — space does the separating. */
.tr-note { font-size: .82rem; color: var(--muted); margin: 0 0 18px; padding-top: 14px; }
.tr-words { display: flex; flex-wrap: wrap; gap: 8px; }
/* The count rides inside the chip: a bare list hides which words actually matter.
   The chip is a <button> — it opens the meaning popup — so the browser's button styling
   has to be reset back to a chip. */
.tr-chip { display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
           font: inherit; color: inherit; border: 1px solid transparent;
           transition: border-color var(--t-fast), background var(--t-fast); }
.tr-chip:hover { border-color: var(--brand-300); }
/* The word the panel is currently explaining. Brand outline + tint, the same signal the
   reading view gives a tapped word, so the list always shows what the popup refers to. */
.tr-chip.picked { border-color: var(--brand-600); color: var(--brand-700);
  background: color-mix(in srgb, var(--brand-600) 10%, var(--surface)); }
.tr-chip.picked i { background: color-mix(in srgb, var(--brand-600) 16%, transparent);
  color: var(--brand-700); }
.tr-chip i { font-style: normal; font-size: .72rem; font-weight: 700; color: var(--muted);
  background: var(--surface-2); border-radius: 999px; padding: 1px 7px; }

/* CEFR level, right of the part of speech. Neutral outline rather than the deck colour:
   three coloured pills in a row would fight, and the level is reference information, not
   the headline. */
.svb-cefr { align-self: center; display: inline-block; font-size: .72rem; font-weight: 600;
  letter-spacing: .02em; padding: 4px 11px; border-radius: 999px; white-space: nowrap;
  color: var(--muted); background: var(--surface-2); border: 1px solid var(--border); }
.svb-cefr-none { cursor: help; opacity: .8; }
/* Estimated level: same colour so the scale still reads at a glance, but a dashed edge and
   a literal "est." so it is never taken for a published grade. */
.svb-cefr-est { border-style: dashed !important; cursor: help; }
.svb-est-tag { font-size: .85em; font-weight: 500; opacity: .75; font-style: italic; }
.flash-face > .flash-meta + .flash-hint { margin-top: 18px; }
.svb-main { margin-top: 30px; }
.svb-def { font-size: 1.4rem; line-height: 1.4; color: var(--ink); font-weight: 500; text-wrap: balance; }
.svb-ex { margin-top: 12px; font-size: .92rem; font-style: italic; color: var(--muted); line-height: 1.5; }
.svb-sa { margin-top: auto; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; padding-top: 20px; align-items: start; }
/* Words with no antonym (most nouns) show the one list across the full width. */
.svb-sa-one { grid-template-columns: 1fr; }
.svb-col { border-top: 3px solid; padding-top: 12px; }
.svb-syn { border-color: var(--ok); }
.svb-ant { border-color: var(--bad); }
.svb-lab { font-size: .72rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 9px; }
.svb-syn .svb-lab { color: var(--ok); }
.svb-ant .svb-lab { color: var(--bad); }
.svb-words { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.svb-chip { font-size: .82rem; font-weight: 600; color: var(--ink-2); background: var(--surface); border: 1px solid var(--border); border-radius: 999px; padding: 4px 11px; }

/* definition panel — example + source footer + Cambridge link */
.define-panel .dp-ex { font-size: .86rem; color: var(--muted); font-style: italic; margin-top: 8px; }
/* Link on its own line, credit underneath. It used to be one space-between row with the
   credit set to nowrap — fine while the credit was "Wiktionary · CC BY-SA", but the CEFR
   sources made it far too long for a 380px panel: it could not wrap, so it was clipped
   mid-word and squeezed "Cambridge Dictionary" onto two lines. */
.define-panel .dp-foot { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border);
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.dp-camb { display: inline-flex; align-items: center; gap: 6px; font-size: .8rem; font-weight: 600;
  color: var(--brand-600); text-decoration: none; white-space: nowrap; }
.dp-camb:hover { text-decoration: underline; }
.dp-camb svg { width: 14px; height: 14px; flex: none; }
/* Wraps freely now — an attribution that gets truncated is not an attribution. */
.dp-src { font-size: .66rem; color: var(--faint); line-height: 1.45; }

/* ===================== QUESTION BANK (Digital SAT blueprint) ===================== */
/* difficulty filter tabs */
.qb-diff { display: inline-flex; gap: 4px; padding: 4px; margin: 18px 0 22px; border-radius: 12px;
  background: var(--surface-2); border: 1px solid var(--border); }
/* difficulty filter is now a pill-seg; the sliding thumb takes on the active difficulty's colour */
.qb-diff[data-active="all"]    .pill-thumb { background-color: var(--brand-600); }
.qb-diff[data-active="easy"]   .pill-thumb { background-color: var(--ok); }
.qb-diff[data-active="medium"] .pill-thumb { background-color: var(--warn); }
.qb-diff[data-active="hard"]   .pill-thumb { background-color: var(--bad); }
.qb-diff[data-active="marked"] .pill-thumb { background-color: #7c5cff; }
/* Practised / Not practised filter, beside the difficulty one. Its thumb stays out of the
   difficulty colours so the two selections never read as one: ink-dark once narrowed. */
.qb-seen[data-active="done"] .pill-thumb, .qb-seen[data-active="new"] .pill-thumb { background-color: var(--ink); }
.qb-seen[data-active="done"] .pill-opt.active, .qb-seen[data-active="new"] .pill-opt.active { color: var(--surface); }
/* The rule that used to split the two sections is gone with the rest of them — the
   panels carry the structure now, so a gap is enough. */
.qb-grid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 28px; }
.qb-col { min-width: 0; }

/* Difficulty tints the panels in this column. --lvl-bd / --lvl-bg are only set when a
   level is chosen, so "All" falls through to the plain surface via the var() fallback.
   Colours are the ones the rest of the app already means by easy/medium/hard. */
.qb-col[data-diff="easy"]   { --lvl: var(--ok);   }
.qb-col[data-diff="medium"] { --lvl: var(--warn); }
.qb-col[data-diff="hard"]   { --lvl: var(--bad);  }
.qb-col[data-diff="marked"] { --lvl: #7c5cff;     }
.qb-col[data-diff="easy"], .qb-col[data-diff="medium"],
.qb-col[data-diff="hard"], .qb-col[data-diff="marked"] {
  --lvl-bd: color-mix(in srgb, var(--lvl) 55%, var(--border));
  --lvl-bg: color-mix(in srgb, var(--lvl) 6%, var(--surface));
}
/* Hard alone needs more in the dark theme. --bad is a rose whose blue channel (72) is
   higher than the dark surface's (35), so a 6% mix lifts blue as much as red and lands
   on rgb(32,26,37) — a violet-grey that reads as no tint at all, and sits too close to
   Marked. The other three keep the 6%: their hues already separate from the surface. */
[data-theme="dark"] .qb-col[data-diff="hard"] {
  --lvl-bg: color-mix(in srgb, var(--lvl) 15%, var(--surface));
  --lvl-bd: color-mix(in srgb, var(--lvl) 68%, var(--border));
}

/* Section header + its meter, boxed like the domains below it. */
.qb-headwin {
  background: var(--lvl-bg, var(--surface)); border: 1px solid var(--lvl-bd, var(--border));
  border-radius: var(--r-lg); box-shadow: var(--shadow-xs);
  padding: 14px 16px 10px; margin-bottom: 16px;
  transition: background var(--t), border-color var(--t);
}
.qb-head { display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding-bottom: 10px; margin-bottom: 2px; }
.qb-title { font-size: 1.32rem; margin: 0; line-height: 1.1; }
.qb-total { color: var(--muted); font-size: .8rem; }
.qb-start { flex-shrink: 0; }
/* Each domain is its own panel rather than a run of rows split by hairlines. Four
   equal-weight groups separated by dividers read as one long list you have to parse;
   boxed, each is a thing you can look at on its own. */
.qb-domain {
  background: var(--lvl-bg, var(--surface)); border: 1px solid var(--lvl-bd, var(--border));
  border-radius: var(--r-lg); box-shadow: var(--shadow-xs);
  padding: 6px 12px 10px;
  transition: background var(--t), border-color var(--t);
}
.qb-domain-head { width: 100%; display: flex; align-items: center; gap: 10px; padding: 12px 6px;
  background: none; border: none; cursor: pointer; font: inherit; color: var(--ink); text-align: left;
  border-radius: 10px; transition: background var(--t); }
.qb-domain-head:hover { background: var(--surface-2); }
.qb-domain-name { font-weight: 700; font-size: .97rem; flex: 1; min-width: 0; }
.qb-skills-in { overflow: hidden; min-height: 0; margin-left: 14px; border-left: 1.5px solid var(--border); }
.qb-skill { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 6px 8px 18px; color: var(--ink-2); font-size: .89rem; border-radius: 8px;
  transition: background var(--t); }
/* The slide used to animate padding-left, which reflowed all 29 rows' contents on every
   pointer-over. Translating the label gives the identical movement without touching layout;
   the row's background stays put, which is what padding-left was chosen for originally. */
.qb-skill > * { transition: transform var(--t) var(--ease); }
.qb-skill:hover { background: var(--surface-2); }
.qb-skill:hover > * { transform: translateX(4px); }
/* clickable domain/skill rows → launch the runner */
.qb-skill { width: 100%; font: inherit; background: none; border: none; cursor: pointer; }
.qb-skill .qb-skill-name { flex: 1; min-width: 0; text-align: left; }
.qb-go { display: inline-flex; flex-shrink: 0; color: var(--muted-2); opacity: 0; transform: translateX(-5px); transition: opacity var(--t), transform var(--t); }
.qb-go svg { width: 16px; height: 16px; }
.qb-domain-head:hover .qb-go, .qb-skill:hover .qb-go { opacity: 1; transform: translateX(0); }
/* a row whose domain file is still downloading (the text is fetched when a set starts) */
.qb-page [data-run][aria-busy="true"] { cursor: progress; opacity: .6; }
/* There is no hover on a touchscreen, so this arrow — the only thing marking these rows
   as tappable — never appeared at all. Show it outright where the pointer is coarse. */
@media (pointer: coarse) { .qb-go { opacity: 1; transform: none; } }
/* filter moved into a compact top-left row so it no longer eats a whole row of vertical space */
.qb-headwrap { margin-bottom: 24px; }
.qb-topbar { display: flex; align-items: center; justify-content: space-between; gap: 14px 24px; flex-wrap: wrap; }
/* CEFR filter + section switch, kept side by side at the right-hand end of the vocab
   header. They wrap together rather than the filter falling under the title alone. */
.qb-topbar-segs { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }

/* Digital SAT section switch — Full exam / Verbal / Math. It is a plain .pill-seg and adds
   nothing to it: same sliding crimson thumb, same white-on-crimson active label, same
   narrow-screen behaviour as the dashboard's filters.
   It carried a `display: grid` override here for a while, to lay the three options out as
   equal thirds on a phone. That silently broke the animation — .pill-thumb is positioned
   from the active button's offsetLeft/offsetWidth, which a grid changes underneath it —
   and .pill-seg already scrolls horizontally when it runs out of room, which is what every
   other one of these does. */
.dex-mode { flex: none; }
.dex-mode-note { margin-top: 10px; }
.dex-mode-note:empty { display: none; }   /* Full exam says nothing, and says it silently */
/* Year + section share the title row inside .qb-topbar-segs. `min-width: 0` lets the year
   bar — the wider of the two — scroll inside itself on a phone instead of pushing the page
   sideways; .pill-seg already handles the overflow once it is allowed to. */
.qb-topbar-segs .pill-seg { min-width: 0; }
/* Every page title inside the app is this one class: UI face at 800, icon, crimson.
   font-family is stated rather than inherited — Reading and Vocabulary used to reach
   for .h1 instead, and the moment .h1 took the display face those two pages drifted
   into a serif while the other five stayed sans. */
.qb-h1 { margin: 0; font-family: var(--font-ui); font-size: 2rem; font-weight: 800; letter-spacing: -.01em; color: var(--ink); display: inline-flex; align-items: center; gap: 11px; }
/* The page title reads in ink; only its icon stays crimson. A 2rem crimson heading on
   every screen spent the accent on the largest thing in view and left the sidebar's
   active pill — the one place the colour is actually saying something — competing with
   it. Concentrated into the icon, the same crimson marks the page without shouting. */
.qb-h1 svg { color: var(--brand-600); }
.qb-h1.qb-h1-brand { color: var(--ink); }

/* Dashboard "Accuracy by skill" cards (Question Bank data view) */
/* Weakest skills: one panel of rows, two columns on a wide screen (compact, 2026-09-14).
   Hairlines are the 1px gap showing the border colour, as in .stat-strip. */
/* Filled COLUMN-first — 1–5 down the left, 6–10 down the right — because a ranking is read
   down, not across. --qbf-rows (half the count, rounded up) is set inline by the view. */
.qbf-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: column;
  grid-template-rows: repeat(var(--qbf-rows, 5), auto); gap: 1px; background: var(--border); overflow: hidden; }
@media (max-width: 860px) { .qbf-list { grid-template-columns: minmax(0, 1fr); grid-auto-flow: row; grid-template-rows: none; } .qbf-fill { display: none; } }
.qbf-row { display: grid; grid-template-columns: 20px minmax(0, 1fr) minmax(56px, 22%) 42px; align-items: center; gap: 12px;
  padding: 10px 16px; background: var(--surface); min-width: 0; }
.qbf-rank { font-size: .76rem; font-weight: 700; color: var(--faint); font-variant-numeric: tabular-nums; text-align: right; }
.qbf-text { min-width: 0; }
.qbf-skill { font-weight: 650; color: var(--ink); font-size: .92rem; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The counts are the evidence behind the rank, so they wrap rather than lose their end to an ellipsis. */
.qbf-meta { font-size: .74rem; color: var(--muted-2); margin-top: 2px; font-variant-numeric: tabular-nums; line-height: 1.35; }
.qbf-bar { height: 7px; border-radius: 99px; background: rgba(127, 127, 127, .16); overflow: hidden; }
.qbf-bar i { display: block; height: 100%; border-radius: 99px; transition: width .5s ease; }
.qbf-pct { font-weight: 800; font-size: .92rem; font-variant-numeric: tabular-nums; color: var(--ink); text-align: right; }
@media (max-width: 480px) { .qbf-row { grid-template-columns: 16px minmax(0, 1fr) 42px; }
  .qbf-bar { grid-column: 2 / 4; grid-row: 2; } }
.qb-diff { margin: 0; }
/* Question Bank section, a touch enlarged now that the filter row is gone */
.qb-page .qb-title { font-size: 1.44rem; }
.qb-page .qb-domain-name { font-size: 1rem; }
.qb-page .qb-skill { font-size: .92rem; }
/* Gap between the panels. The old rule also set padding-bottom, which the panel
   itself now owns — leaving it here would flatten the box's lower edge. */
.qb-page .qb-domains { display: flex; flex-direction: column; gap: 14px; }
.qb-page .qb-domain { margin-bottom: 0; }
/* attempted-count meter (how many questions have been tried) */
.qb-prog { display: inline-flex; align-items: center; gap: 8px; flex-shrink: 0; }
.qb-bar { width: 62px; height: 6px; border-radius: 99px; background: var(--surface-2); border: 1px solid var(--border); overflow: hidden; }
.qb-bar i { display: block; height: 100%; width: 0; border-radius: 99px; background: var(--accent, #4a7dff); transition: width .45s var(--ease-spring); }
.qb-frac { color: var(--muted); font-size: .8rem; font-variant-numeric: tabular-nums; min-width: 46px; text-align: right; }
.qb-secmeter { display: flex; align-items: center; gap: 10px; padding: 8px 4px 12px; }
.qb-secmeter .qb-prog-lg .qb-bar { width: 150px; height: 9px; }
.qb-secmeter .qb-frac { font-size: .86rem; color: var(--ink-2); min-width: 0; }
.qb-secmeter-label { color: var(--muted-2); font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; }
/* legal / trademark disclaimer at the foot of the Question Bank */
.qb-legal { position: relative; margin: 46px 0 10px; padding-top: 20px; color: var(--muted-2); font-size: .74rem; line-height: 1.6; }
.qb-legal p { margin: 0 0 9px; }
.qb-legal p:last-child { margin-bottom: 0; }

/* Question Bank runner (Bluebook-style, count-up timer) */
/* Bluebook "barcode" ribbon between questions. The dash pattern itself lives with the
   other users of .rule, up in the Footer section; only its weight differs here. */
/* The one exception, and it is not a divider: this is the Bluebook barcode ribbon that
   belongs to the test chrome, so it keeps the heavier 2px bar and the brand ink (which
   the .engine scope remaps away from crimson — see "Red means one thing in here"). */
.qr-rule { flex-shrink: 0; height: 2px; border: none; --rule-c: var(--brand-600); --rule-tile: 40px; }
/* In the dark theme the .engine remap lands --brand-600 on --ink-2, which is near-white
   — a 2px near-white ribbon across a dark screen glares in a way the dark-on-white
   version never does. Light-on-dark carries more weight at equal contrast, so the dark
   theme takes a muted grey instead of the straight inversion. */
[data-theme="dark"] .qr-rule { --rule-c: var(--muted-2); }
.qr-rule-sm { margin: 12px 0 16px; }
.qr-result { margin-bottom: 16px; }
.qr-result .q-num { width: 28px; height: 28px; border-radius: 8px; }
.qr-result .choice { pointer-events: none; }
.qr-expl { margin-top: 14px; padding: 13px 16px; background: var(--surface-2); border-radius: var(--r);
  font-size: .92rem; line-height: 1.6; color: var(--ink-2); }
.qr-expl b { color: var(--ink); }
/* annotation tool */
.annotating .passage, .annotating .q-prompt { cursor: text; }
mark.annot { background: #fde68a; color: #422006; border-radius: 3px; padding: 0 1px; cursor: pointer; }
mark.annot.has-note { box-shadow: inset 0 -2px 0 #d97706; }
[data-theme="dark"] mark.annot { background: rgba(250, 204, 21, .32); color: #fde68a; }
/* Highlight colours (blue / light-red / green) — override the yellow default. */
mark.annot.hl-blue  { background: rgba(59, 130, 246, .30); color: inherit; }
mark.annot.hl-red   { background: rgba(248, 113, 113, .34); color: inherit; }
mark.annot.hl-green { background: rgba(34, 197, 94, .32); color: inherit; }
/* Underline styles — no fill, coloured underline in the text colour. */
mark.annot.ul-straight, mark.annot.ul-wavy { background: transparent; color: inherit; border-radius: 0; padding: 0; }
mark.annot.ul-straight { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }
mark.annot.ul-wavy { text-decoration: underline wavy; text-decoration-thickness: 1.5px; text-underline-offset: 3px; }
mark.annot.ul-straight.has-note, mark.annot.ul-wavy.has-note { box-shadow: none; background: rgba(217, 119, 6, .12); }

/* Selection toolbar: floats above the highlighted text. */
.annot-toolbar { position: fixed; z-index: 96;
  display: flex; align-items: center; gap: 7px; padding: 7px 9px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  box-shadow: var(--shadow-lg); animation: modalIn .16s var(--ease-spring); }
.annot-toolbar::after { content: ""; position: absolute; left: 50%; bottom: -6px; width: 12px; height: 12px;
  background: var(--surface); border-right: 1px solid var(--border); border-bottom: 1px solid var(--border);
  transform: translateX(-50%) rotate(45deg); }
.annot-toolbar.below::after { bottom: auto; top: -6px; border: none; border-left: 1px solid var(--border); border-top: 1px solid var(--border); }
.atb-color { width: 24px; height: 24px; border-radius: 50%; border: 2px solid rgba(0,0,0,.10);
  cursor: pointer; padding: 0; transition: transform var(--t-fast); }
.atb-color:hover { transform: scale(1.14); }
.atb-blue { background: #3b82f6; } .atb-red { background: #f87171; } .atb-green { background: #22c55e; }
.atb-div { width: 1px; height: 22px; background: var(--border); margin: 0 1px; }
.atb-ul { width: 32px; height: 28px; border: 1.5px solid var(--border-2); border-radius: 9px;
  background: var(--surface); color: var(--ink); display: grid; place-items: center; cursor: pointer;
  transition: all var(--t-fast); }
.atb-ul:hover { border-color: var(--brand-400); background: var(--surface-2); }
.atb-ul svg { width: 18px; height: 18px; }
.annot-pop { position: fixed; z-index: 95; width: 260px; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-lg); padding: 12px; animation: modalIn .2s var(--ease-spring); }
.annot-note { width: 100%; resize: vertical; border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px;
  font: inherit; font-size: .9rem; line-height: 1.5; background: var(--surface-2); color: var(--ink); }
.annot-note:focus { outline: none; border-color: var(--brand-600); }

/* ===== Question Bank runner — Bluebook-style chrome ===== */
.engine-top.qr-top { height: auto; min-height: 68px; display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 12px; padding: 8px 20px; border-bottom: none; }
.qr-top-left { justify-self: start; display: flex; flex-direction: row; align-items: center; gap: 12px; min-width: 0; }
.qr-top-titles { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* clickable EliteXSAT logo (no wobble) with a subtle red hover glow */
.qr-logo { position: relative; flex-shrink: 0; display: inline-grid; place-items: center; padding: 3px;
  border: none; background: none; cursor: pointer; border-radius: 12px; }
.qr-logo img { position: relative; z-index: 1; height: 38px; width: auto; display: block;
  transition: filter .25s var(--ease), transform .2s var(--ease-spring); }
.qr-logo::before { content: ""; position: absolute; inset: -5px; z-index: 0; border-radius: 15px; opacity: 0;
  background: radial-gradient(closest-side, rgba(209, 29, 43, .35), rgba(209, 29, 43, .08) 65%, transparent);
  transition: opacity .25s var(--ease); }
.qr-logo:hover::before, .qr-logo:focus-visible::before { opacity: 1; }
.qr-logo:hover img { filter: drop-shadow(0 5px 12px rgba(209, 29, 43, .45)); transform: translateY(-1px); }
.qr-logo:active img { transform: scale(.94); }
.qr-logo:focus-visible { outline: none; }
.qr-top .et-title { font-weight: 700; font-size: 1rem; line-height: 1.2; }
.qr-directions { align-self: flex-start; display: inline-flex; align-items: center; gap: 4px; padding: 1px 0;
  background: none; border: none; font: inherit; font-size: .82rem; font-weight: 600; color: var(--ink-2); cursor: pointer; }
.qr-directions svg { width: 14px; height: 14px; }
/* The middle grid cell. The clock does not move: .qr-center is only ever as wide as the
   clock itself, so the grid keeps centring exactly what it centred before. The pause
   button is taken out of flow and hung off the clock's left edge — that is the whole
   reason adding it shifts nothing. */
.qr-center { justify-self: center; position: relative; display: flex; align-items: center; min-width: 0; }
.qr-center .qr-pause { position: absolute; right: 100%; top: 50%; transform: translateY(-50%); margin-right: 14px; }
/* On a phone there is nothing to hang it on: the title block already overflows its own
   track, so a button to the left of the clock lands on top of "Directions". It goes
   underneath instead — still without moving the clock a pixel. */
@media (max-width: 720px) {
  /* align-self:start pins the clock to the top of the row, so hanging a button below it
     does not push the clock up as the row grows — same rule as above, stated vertically. */
  .qr-center { flex-direction: column-reverse; gap: 4px; align-self: start; }
  .qr-center .qr-pause { position: static; transform: none; margin-right: 0; }
}
.qr-clock-wrap { justify-self: center; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.qr-time { font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; min-height: 1.3em; }
.qr-time.hidden::after { content: "\2014 : \2014"; color: var(--muted-2); font-weight: 500; }
.qr-time.warn { color: var(--warn); }
.qr-hide { border: 1px solid var(--ink-2); background: var(--surface); border-radius: var(--r-chip); padding: 2px 18px;
  font: inherit; font-size: .78rem; font-weight: 600; color: var(--ink); cursor: pointer; }
.qr-hide:hover { background: var(--surface-2); }
.qr-tools { justify-self: end; display: flex; align-items: center; gap: 2px; }
.qr-tool2 { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 5px 12px; border-radius: 10px;
  background: none; border: none; font: inherit; font-size: .72rem; font-weight: 600; color: var(--ink-2); cursor: pointer; transition: background var(--t); }
.qr-tool2:hover { background: var(--surface-2); }
.qr-tool2.on { color: var(--brand-600); }
.qr-tool2 svg { width: 20px; height: 20px; }
/* Annotate ON: fill the pen red + a red pill, so the active state is unmistakable. */
.qr-tool2[data-annotate].on { color: var(--brand-600); background: rgba(209, 29, 43, .13); }
.qr-tool2[data-annotate].on svg { fill: var(--brand-600); stroke: var(--brand-600); }
@media (max-width: 720px) { .qr-tool2 span { display: none; } }
/* Full screen is the widest label and the least essential one, so it drops to a bare
   icon first — the toolbar keeps its labels down to 720px without wrapping. */
@media (max-width: 980px) { .qr-tool2[data-fs] span { display: none; } }

/* ===== Pause (Digital SAT library exams only) =====
   The button carries the remaining allowance in its label, so tabular figures — the
   countdown must not shuffle the toolbar sideways once a second. Spent, it stays in
   place and dims rather than disappearing: a control that vanishes mid-test reads as a
   bug, and the label ("Pause used") is the explanation. */
/* `display: flex` on the class outranks the browser's own [hidden] rule — the same trap
   .pill-seg hit — so the button has to be told explicitly to go during a break. */
.qr-tool2[hidden] { display: none; }
/* "Pause 2:53" is the only two-word label in the toolbar, so it is the only one that can
   wrap — and a wrapped label made this button a line taller than every other tool. */
.qr-pause .qp-label { white-space: nowrap; }
.qr-pause .qp-left { font-variant-numeric: tabular-nums; }
.qr-tool2.qr-pause.spent { opacity: .42; cursor: default; }
.qr-tool2.qr-pause.spent:hover { background: none; }
/* Every other tool drops to a bare icon at 720px. This one keeps its figure — how much
   pause time is left is the one thing a student cannot read off anything else — and
   sheds only the word, so it costs the toolbar four characters rather than a whole
   label. The label is one flex child (the icon-over-text column stays intact); the word
   and the clock are inline inside it. */
@media (max-width: 720px) {
  /* .qr-tool2 span hides EVERY span in here, the inner ones included — the wrapper has
     to be put back and the clock with it, or the label reserves a box with nothing in it.
     Smaller type and tighter padding keep the one labelled button close to the height of
     its icon-only neighbours instead of standing 20px taller than the row. */
  .qr-pause .qp-label, .qr-pause .qp-left { display: inline; }
  .qr-pause .qp-word { display: none; }
  .qr-pause { padding: 2px 8px; gap: 0; }
  .qr-pause .qp-label { font-size: .62rem; line-height: 1.15; }
}
/* The overlay covers the entire runner, toolbar included. Opaque, not a scrim: a
   translucent screen would leave the questions readable and turn a pause into free
   thinking time, which is the one thing this must not buy. */
.qr-paused { position: absolute; inset: 0; z-index: 95; display: grid; place-items: center;
  padding: 24px; background: var(--surface); text-align: center; }
.qr-paused-inner { max-width: 540px; }
.qr-paused-icon { background: var(--surface-2); color: var(--ink-2); }
.qr-paused-lead { margin: 10px 0 4px; }
.qr-paused-clock { font-size: 3.2rem; font-weight: 800; font-variant-numeric: tabular-nums;
  letter-spacing: -.02em; line-height: 1.1; margin: 16px 0 6px; }
/* Wide enough that each of the note's two authored lines stays on one line; it still
   wraps on a phone, where 440px does not exist. */
.qr-paused-note { max-width: 440px; margin: 0 auto 24px; line-height: 1.6; }
@media (max-width: 480px) { .qr-paused-clock { font-size: 2.6rem; } }

/* Math reference sheet — the printed sheet from the real test: two rows of figures with
   their formulas underneath, then the three constants. Drawn in currentColor so it
   follows the theme; the whole sheet scrolls sideways rather than squashing on a phone. */
.ref-sheet { font-family: var(--font-read); color: var(--ink); overflow-x: auto; }
.ref-row { display: grid; grid-template-columns: repeat(4, 1fr) 1.75fr; gap: 10px; align-items: start;
  min-width: 660px; margin-bottom: 24px; }
/* Fixed-height figure box → one shared baseline per row, so the formulas below line up
   even though the circle cell carries two of them. */
.ref-cell { text-align: center; }
.ref-figwrap { height: 104px; display: flex; align-items: flex-end; justify-content: center; margin-bottom: 10px; }
.ref-fig { width: 100%; max-width: 115px; height: auto; display: block; }
.ref-fig text { font-family: var(--font-read); font-style: italic; font-size: 12.5px; fill: currentColor; stroke: none; }
.ref-fig text.ang { font-style: normal; font-size: 8.5px; }
.ref-fig text.sm { font-size: 10px; }
.ref-eq { font-size: .95rem; line-height: 1.9; white-space: nowrap; }
.ref-eq i { font-style: italic; }
.ref-eq sup { font-size: .7em; vertical-align: super; }
.ref-eq .frac { font-size: .82em; }
.ref-eq-txt { font-style: normal; }
.ref-pair { display: flex; gap: 4px; justify-content: center; }
.ref-pair .ref-fig { max-width: 105px; }
.ref-facts { border-top: 1px solid var(--border); padding-top: 14px; font-size: .92rem; line-height: 1.9; }
.ref-facts p { margin: 0; }

/* pane expand tools */
.qr-pane-toolbar { display: flex; margin-bottom: 8px; }
.pane-left .qr-pane-toolbar { justify-content: flex-end; }
.pane-right .qr-pane-toolbar { justify-content: flex-start; }
.qr-pane-tool { display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--surface); color: var(--muted-2); cursor: pointer; box-shadow: var(--shadow-sm); }
.qr-pane-tool:hover { background: var(--surface-2); color: var(--ink); }
.qr-pane-tool svg { width: 15px; height: 15px; }
.qbank-runner .engine-body.two-pane { display: flex; }
.qbank-runner .engine-body.two-pane > .pane-left { flex: var(--lf, 50) 1 0; min-width: 0; border-right: none; }
.qbank-runner .engine-body.two-pane > .pane-right { flex: var(--rf, 50) 1 0; min-width: 0; }
.qr-splitter { flex: 0 0 1px; align-self: stretch; position: relative; background: var(--border); cursor: col-resize; touch-action: none; }
.qr-splitter::after { content: ""; position: absolute; top: 0; bottom: 0; left: -6px; right: -6px; cursor: col-resize; }

/* A 13px-wide target is a comfortable one for a mouse and a miss for a fingertip, which
   lands on ~9mm of screen. Widened only where the pointer is coarse, so the precise thin
   divider is untouched for anyone using a mouse — and the line itself stays 1px either
   way; this grows the invisible grab area, not the design. */
@media (pointer: coarse) {
  .qr-splitter::after { left: -14px; right: -14px; }
  .calc-split::after  { left: -12px; right: -12px; }
}
.qr-splitter:hover, .engine-body.splitting .qr-splitter { background: var(--brand-600); box-shadow: 0 0 0 1px var(--brand-600); }
.engine-body.splitting, .engine-body.splitting * { cursor: col-resize !important; user-select: none; }
@media (max-width: 720px) {
  .qbank-runner .engine-body.two-pane { flex-direction: column; }
  .qbank-runner .engine-body.two-pane > .pane-left, .qbank-runner .engine-body.two-pane > .pane-right { flex: 1 1 auto; }
  .qr-splitter { display: none; }
}

/* ===== Ownership watermark (alternating "EliteXSAT" / "Eljan Ahmadli" + faint center logo) — light enough to read through, proves the page is EliteXSAT in any screenshot.
   The two marks alternate one-for-one, brand then name, and they alternate DOWN the page as well as across it: the tile is 600x344 — four 300x172 cells laid out as a checkerboard — so no column ever repeats the same word. One long combined line was tried first and rejected; the marks-per-area is identical to the single-name original.
   Lives INSIDE .engine-stage (the middle area between the top/bottom ribbons) so it NEVER crosses the header/footer chrome.
   z-index 4 paints above the passage/question content but under every menu (navigator/more 90, annot-pop 95, modals 100+);
   pointer-events:none keeps text selection (annotation) and all buttons usable. Applies to BOTH the Question Bank runner and the full mock-test engine. */
.engine .qr-watermark {
  position: absolute; inset: 0; z-index: 4; pointer-events: none; overflow: hidden;
  background-repeat: repeat; background-position: center;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='344'%3E%3Cg fill='%23151519' fill-opacity='0.07' font-family='Segoe%20UI,system-ui,Arial,sans-serif' font-size='20' letter-spacing='0.5' text-anchor='middle'%3E%3Ctext x='150' y='95' transform='rotate(-28 150 90)'%3EEliteXSAT%3C/text%3E%3Ctext x='450' y='95' transform='rotate(-28 450 90)'%3EEljan%20Ahmadli%3C/text%3E%3Ctext x='150' y='267' transform='rotate(-28 150 262)'%3EEljan%20Ahmadli%3C/text%3E%3Ctext x='450' y='267' transform='rotate(-28 450 262)'%3EEliteXSAT%3C/text%3E%3C/g%3E%3C/svg%3E");
}
/* Dark mode needs FAINTER ink, not the same amount. A light mark on a near-black page
   reads far stronger than a dark mark on white: at equal alpha the tiled name and the
   centre logo jumped out and pulled the eye off the question. Roughly halved so the
   watermark still survives a screenshot without competing with the text. */
[data-theme="dark"] .engine .qr-watermark {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='344'%3E%3Cg fill='%23ffffff' fill-opacity='0.04' font-family='Segoe%20UI,system-ui,Arial,sans-serif' font-size='20' letter-spacing='0.5' text-anchor='middle'%3E%3Ctext x='150' y='95' transform='rotate(-28 150 90)'%3EEliteXSAT%3C/text%3E%3Ctext x='450' y='95' transform='rotate(-28 450 90)'%3EEljan%20Ahmadli%3C/text%3E%3Ctext x='150' y='267' transform='rotate(-28 150 262)'%3EEljan%20Ahmadli%3C/text%3E%3Ctext x='450' y='267' transform='rotate(-28 450 262)'%3EEliteXSAT%3C/text%3E%3C/g%3E%3C/svg%3E");
}
.engine .qr-wm-logo {
  position: absolute; top: 50%; left: 50%; width: min(42vw, 460px); height: auto;
  /* The logo art already has a ~5deg clockwise tilt baked in, so 0deg extra rotation makes the
     watermark match the user's 000-removebg-preview.png reference exactly (don't re-add rotation here). */
  transform: translate(-50%, -50%) rotate(0deg); opacity: 0.10; user-select: none; -webkit-user-drag: none;
}
/* Same reasoning as the tiled name above — and the logo's red also glows against a dark
   page, so drop most of the colour along with the opacity. Taken one step further to .04
   on 2026-08-14 at Eljan's request: a small nudge, not a rewrite — the mark still has to
   survive a screenshot, which is the only reason it is there. */
[data-theme="dark"] .engine .qr-wm-logo { opacity: 0.04; filter: grayscale(0.55); }

/* question header: number + Mark for Review + ABC */
.qr-qhead { display: flex; align-items: center; gap: 12px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; padding: 7px 10px 7px 8px; }
.qr-mark { display: inline-flex; align-items: center; gap: 7px; padding: 4px 6px; border-radius: 8px; background: none;
  border: none; font: inherit; font-size: .9rem; font-weight: 500; color: var(--ink-2); cursor: pointer; }
.qr-mark:hover { background: var(--surface-2); }
.qr-mark.on { color: var(--brand-600); }
.qr-mark.on svg { fill: var(--brand-600); }
.qr-mark svg { width: 17px; height: 17px; }
.qr-tally { font-size: .76rem; font-weight: 700; color: var(--muted); background: var(--surface); border: 1px solid var(--border); border-radius: 99px; padding: 4px 11px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.qr-abc { margin-left: auto; min-width: 42px; padding: 4px 11px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); font: inherit; font-size: .84rem; font-weight: 700; color: var(--ink-2); cursor: pointer; text-decoration: line-through; }
.qr-abc:hover { background: var(--surface-2); }
.qr-abc.on { background: var(--brand-50); color: var(--brand-700); border-color: var(--brand-200); }

/* Un-push ABC ONLY when something else in the header owns the trailing edge (Question
   Bank: the verdict, or the legacy Check button). In the timed exam the header has
   neither, so ABC keeps margin-left:auto and sits at the far right of the window.
   🔴 .qr-verdict has to be in this list: it replaced .qr-check on the trailing edge, and
   without it ABC and the verdict both claim `auto` and the header splits in two. */
.qr-qhead:has(.qr-check) .qr-abc,
.qr-qhead:has(.qr-verdict) .qr-abc { margin-left: 0; }
/* The verdict, where the Check button used to sit. Same geometry on purpose: the corner
   was already where the eye went for "how did that go", and keeping the box identical
   means the answer lands in place instead of the header reflowing around a new word. */
.qr-verdict { margin-left: auto; display: inline-flex; align-items: center; justify-content: center;
  min-width: 42px; padding: 4px 13px; border: 1px solid transparent; border-radius: 8px;
  font-size: .84rem; font-weight: 700; }
.qr-verdict.correct { background: var(--ok-bg); color: var(--ok); border-color: var(--ok); }
.qr-verdict.wrong { background: var(--bad-bg); color: var(--bad); border-color: var(--bad); }
[data-theme="dark"] .qr-verdict.correct { color: #6ee7b7; border-color: var(--ok-bd); }
[data-theme="dark"] .qr-verdict.wrong { color: #fda4af; border-color: var(--bad-bd); }

/* Check, on the chosen row. It is the primary action of the whole Question Bank, so it
   is a filled button rather than the outlined chip the header used — at the far right of
   the row it has to read as "press me" without a heading above it to explain itself.
   margin-left:auto pins it to the trailing edge; align-self keeps it centred against a
   choice that wraps to three lines instead of stretching down the whole row. */
/* 🔴 28px, because that is exactly what the A/B/C/D badge measures. The row is a flex
   box with align-items:flex-start, so its height is set by the TALLEST item — the badge,
   not the text (25px). At 34px the chip became the tallest thing in the row, and the
   whole option grew 6px the instant it was selected: press an answer and it lifts. It is
   pinned to the badge rather than picked by eye, so the row cannot change height no
   matter which item happens to be tallest. */
.choice .ch-check { margin-left: auto; align-self: center; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  /* height + line-height:1, not min-height: min-height is a floor, and the chip's own
     text (.84rem at the inherited 1.5 line-height, plus padding) sailed 2px over it. The
     box is centred by flex, so fixing the height costs nothing and makes 28 mean 28. */
  min-width: 74px; height: 28px; padding: 0 16px; line-height: 1; border-radius: 8px;
  background: var(--pick); color: #fff; font-size: .84rem; font-weight: 700; letter-spacing: .01em;
  box-shadow: 0 1px 2px rgba(0,0,0,.18); cursor: pointer;
  transition: filter var(--t-fast), transform var(--t-fast); }
.choice .ch-check:hover { filter: brightness(1.08); }
.choice .ch-check:active { transform: scale(.96); }
.choice .ch-check.is-busy { opacity: .65; pointer-events: none; }
/* A fingertip is ~9mm, so touch still needs 44px — but growing the box would grow the row
   again, which is the bug this whole block exists to avoid. The negative block margin is
   what separates the two: the chip PAINTS and is PRESSED at 44px while contributing
   44 − 16 = 28px to layout, the badge's height, so the row stays put. */
@media (pointer: coarse) {
  .choice .ch-check { height: 44px; min-width: 88px; margin-block: -8px; }
}
/* Narrow columns: the passage pane can squeeze a choice to almost nothing, and a chip
   that never shrinks would push the option text into a one-word-per-line column. */
@media (max-width: 560px) { .choice .ch-check { min-width: 62px; padding: 5px 11px; } }

.qr-check { margin-left: auto; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-width: 42px; padding: 4px 13px; border: 1px solid var(--pick); border-radius: 8px;
  background: var(--pick-bg); color: var(--pick); font: inherit; font-size: .84rem; font-weight: 700;
  cursor: pointer; transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast); }
.qr-check:hover { filter: brightness(.97); }
.qr-check.correct { background: var(--ok-bg); color: var(--ok); border-color: var(--ok); }
.qr-check.wrong { background: var(--bad-bg); color: var(--bad); border-color: var(--bad); }
[data-theme="dark"] .qr-check { background: rgba(59,91,219,.20); color: #b3c0f7; border-color: rgba(59,91,219,.55); }
[data-theme="dark"] .qr-check.correct { background: var(--ok-bg); color: #6ee7b7; border-color: var(--ok-bd); }
[data-theme="dark"] .qr-check.wrong { background: var(--bad-bg); color: #fda4af; border-color: var(--bad-bd); }

/* "More" dropdown */
.qr-more-menu { position: fixed; z-index: 90; min-width: 162px; background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: var(--shadow-lg); padding: 6px; animation: modalIn .18s var(--ease-spring); }
.qr-more-item { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 11px; border-radius: 8px; background: none;
  border: none; font: inherit; font-size: .88rem; color: var(--ink); cursor: pointer; text-align: left; }
.qr-more-item:hover { background: var(--surface-2); }
.qr-more-item svg { width: 16px; height: 16px; color: var(--muted-2); }

/* Appearance (White / Dark) menu — opened from the "Appearance" tool.
   These just drive the app's real light/dark theme, so no per-exam token overrides. */
.qr-bg-menu { position: fixed; z-index: 90; background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; box-shadow: var(--shadow-lg); padding: 12px 13px 13px; animation: modalIn .18s var(--ease-spring); }
.qr-bg-title { font-size: .7rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted-2); margin-bottom: 10px; }
.qr-bg-grid { display: flex; gap: 8px; }
.qr-bg-swatch { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 64px; padding: 8px 4px 7px;
  border-radius: 11px; background: none; border: 1.5px solid transparent; cursor: pointer; font: inherit; }
.qr-bg-swatch:hover { background: var(--surface-2); }
.qr-bg-swatch.on { border-color: var(--brand-600); background: var(--brand-50); }
.qr-bg-dot { width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--border-2);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .04); background: var(--surface); }
.qr-bg-lbl { font-size: .73rem; color: var(--ink-2); font-weight: 500; }
.qr-bg-swatch.on .qr-bg-lbl { color: var(--brand-600); }

@media (max-width: 820px) {
  .qb-grid { grid-template-columns: 1fr; }
  .qb-rw { padding-right: 0; }
  .qb-math { padding-left: 0; border-left: none; border-top: 1px solid var(--border); padding-top: 22px; margin-top: 12px; }
}

/* ======================================================================
   PRICING
   ====================================================================== */
/* The heading stands alone — the sentence that used to explain the three plans said what
   the three cards say two inches lower, so it was the reader's time spent on nothing. */
.pc-intro { margin-bottom: 16px; }

/* auto-fit rather than a hard 3, and no companion breakpoint: a fixed three-column grid
   held its shape from 561px all the way up, so the whole tablet range showed three ~200px
   cards with the price and the 3-month line wrapping inside them. minmax lets the row fall
   to 2 and then to 1 on its own, and it is the same rule the free block already uses — one
   responsive strategy on the page instead of two. Three cards do go 2+1 in the middle
   band; that is the lesser evil against three cramped columns. */
.price-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; align-items: stretch; }
/* Compact (Eljan, 2026-09-15): padding, gaps, the key and the price all one step down. */
.price-card { padding: 18px 20px; display: flex; flex-direction: column; gap: 14px; position: relative;
  border-top-width: 4px; border-top-style: solid; border-top-color: var(--plan); }

/* A / B / C as the exam's own answer key. .bubble already carries the geometry (46px, 11px
   radius, 2px border, the .ch-key letter-to-box ratio); only the colour is local. */
.pc-key { --bub: var(--plan); width: 38px; height: 38px; font-size: 1.05rem; border-radius: 9px; }
/* flex-start, not center. The head's height follows .pc-name, and only Elite carried a
   third line there, so centring sat the C key ~14px lower than A and B — the one row on
   the page whose whole job is to be compared across the three cards. Top-aligned, the
   three keys and the three plan names line up whatever the card below them says. */
.pc-head { display: flex; align-items: flex-start; gap: 12px; }
.pc-name h3 { color: var(--plan); margin: 0; }
/* The number a pricing page exists to be scanned for, so it is the largest type after the
   h1 rather than the h2-and-a-bit it used to be. clamp keeps it from crowding a 250px card. */
.price-amt { font-family: var(--font-display); font-size: clamp(2rem, 3vw, 2.4rem); font-weight: 500; line-height: 1; margin: 2px 0 0; }
.price-amt small { font-size: .95rem; color: var(--muted-2); font-family: var(--font-ui); }
/* The second way to pay for the same plan. It used to sit inside .pc-head under the price,
   where it made Elite's header taller than the other two and knocked the keys and the
   feature lists out of line. It now sits below the list, next to the state chip — quieter,
   and closer to the point where the plan is actually being chosen. */
.pc-alt { font-size: .82rem; line-height: 1.35; color: var(--muted); }
.pc-alt b { color: var(--ink-2); font-weight: 700; }

/* The quarterly-billing alternative to the main Subscribe button. Not a second button —
   .btn-ghost read as plain unstyled text on a white card (no border, no fill, nothing to
   signal it was clickable), so this is a real underlined text link instead of a button
   dressed down to look like one. */
.pc-alt-row { display: flex; align-items: baseline; justify-content: space-between; gap: 2px 8px; flex-wrap: wrap; }
.pc-alt-row .pc-alt { font-size: .8rem; }
.pc-quarterly-link { background: none; border: none; padding: 2px 0; margin: 0;
  display: inline-block; flex: none; font-size: .82rem; font-weight: 600; color: var(--brand-link);
  text-decoration: underline; cursor: pointer; }
.pc-quarterly-link:hover { color: var(--brand-600); }

/* Only the four rows that DIFFER between plans. No tick column: every line here is a
   difference, so a tick would be the same mark against four different facts. */
.pc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.pc-list li { position: relative; padding-left: 16px; color: var(--ink-2); font-size: .9rem; line-height: 1.45; }
/* .8, not .55. At 6px a square this small is already quiet, and on the Free card — slate
   on white — .55 made the lowest-contrast bullets on a page whose heading is "Most of
   EliteXSAT is free". The tier the page is arguing for must not be the faintest thing on it. */
.pc-list li::before { content: ""; position: absolute; left: 0; top: .62em;
  width: 6px; height: 6px; border-radius: 2px; background: var(--plan); opacity: .8; }
.pc-list b { color: var(--ink); font-weight: 700; }

/* A state, not a button. Nothing here can be bought yet, and a live-looking "Get started"
   that lands on a page saying "coming soon" is the dead end this replaces. */
.pc-state { margin-top: auto; text-align: center; font-size: .84rem; font-weight: 600;
  color: var(--muted-2); border: 1px dashed var(--border-2); border-radius: var(--r);
  padding: 8px 12px; }
/* Same bottom-alignment .pc-state has — without it, a card with fewer feature rows (Pro,
   4 lines) puts its Subscribe button higher up the card than one with more rows (Elite, 5
   lines + the quarterly alt line), so the buttons across a row of cards don't line up. */
/* .price-card > .btn covers pricing.html, whose buttons are plain "Get started" links
   without the .pc-subscribe hook — there the Pro button floated 31px above Elite's. */
.pc-subscribe, .price-card > .btn { margin-top: auto; }
.price-card .btn, .price-card .pc-state { padding-block: 9px; }
/* The compact padding is for a mouse. On a touchscreen every control keeps a 44px target. */
@media (pointer: coarse) { .price-card .btn, .pc-more-act .btn, .pc-redeem-row .btn { min-height: 44px; } .pc-quarterly-link { min-height: 44px; display: inline-flex; align-items: center; } }
.pc-state-mine { display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  border-style: solid; border-color: var(--plan); color: var(--plan); }
.pc-state-mine svg { width: 15px; height: 15px; }
.price-card.is-mine { box-shadow: 0 0 0 1px var(--plan) inset, var(--shadow-sm); }

/* One card carries more weight than the other two, because a row of three equals asks the
   reader to do the ranking the page should have done. Same inset-ring mechanism as
   .is-mine, one pixel heavier — no scale transform and no gradient: the restraint is the
   best thing about this page and a lifted, tinted card would undo it. Never both at once;
   views.js drops the flag when the account is already on that plan, so the ring always
   means exactly one thing. */
.price-card.is-featured { box-shadow: 0 0 0 2px var(--plan) inset, var(--shadow-sm); }
.pc-badge { position: absolute; top: 12px; right: 12px; padding: 3px 9px;
  border: 1px solid var(--plan); border-radius: var(--r-chip); background: var(--surface);
  font-size: .68rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--plan); }

/* Per-plan colour identity — grey / teal / brand red, the palette the old comparison table
   used per column. Declared once as --plan so the accent, the key, the heading, the bullets
   and the state chip cannot drift apart. No background tint any more: three tinted blocks
   side by side under a white "free on every plan" card made the page look like four
   unrelated surfaces. The top rule and the letter carry the identity instead. */
.price-card-free  { --plan: #64748b; }
.price-card-pro   { --plan: #0b7d70; }
.price-card-elite { --plan: var(--brand-600); }
[data-theme="dark"] .price-card-free  { --plan: #94a3b8; }
[data-theme="dark"] .price-card-pro   { --plan: #2dd4bf; }
[data-theme="dark"] .price-card-elite { --plan: #ff6b78; }

/* What everyone gets. Deliberately the quietest block on the page and deliberately the
   widest — it is the honest counterweight to three price tags, and it is most of what the
   product actually is. */
/* The free block leads now, so the gap belongs under it and the plan grid follows.

   It takes the same 4px top rule the plan cards wear, so it reads as a peer of theirs
   rather than a footnote under them — it is, after all, the largest thing on the page.
   In --ok green, which is the one colour the three plans do not use and the colour its
   own icons already carry: a fourth identity for the tier that costs nothing. */
.pc-always { margin-bottom: 16px; padding: 14px 20px 16px;
  border-top-width: 4px; border-top-style: solid; border-top-color: var(--ok); }
.price-grid + .pc-foot, .pc-always + .price-grid { margin-top: 0; }
.pc-always h2 { margin: 0 0 10px; font-size: 1.05rem; }
/* auto-fit, not a fixed count: the list grows as more of the product becomes free, and a
   hardcoded number would strand the odd one out on a row of its own — which is exactly
   what a 210px minimum did here, fitting four across and leaving the fifth alone. 185px
   is the widest minimum that still lets all five share one row at the card's 1038px. */
.pc-always-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(185px, 1fr)); gap: 10px 16px; }
.pc-always-item { display: flex; align-items: center; gap: 10px; font-size: .9rem; line-height: 1.3; }
.pc-always-item .feature-icon { width: 32px; height: 32px; border-radius: 9px; margin: 0; flex: none; }
.pc-always-item .feature-icon svg { width: 16px; height: 16px; }
.pc-always-item .small { font-size: .78rem; }
/* Who is buying (2026-10-11). The page used to stack three audiences — a student, a
   teacher buying seats, a group sharing one account — and a reader had to scroll past two
   of them to find their own. One switch under the free block, two panes; each pane holds
   only the plans that reader can use. The free block stays ABOVE the switch: it is true
   for everyone. */
.pc-aud { display: flex; justify-content: center; margin: 2px 0 16px; }
.pc-aud .pill-opt { min-width: 132px; justify-content: center; }
@media (pointer: coarse) { .pc-aud .pill-opt { min-height: 44px; } }
.pc-pane[hidden] { display: none; }
.pc-pane > .pc-more:first-child, .pc-pane > .pc-teach:first-child { margin-top: 0; }
/* The quiet buy button. Thirteen identical crimson buttons on one page ranked nothing;
   now the recommended choice in each group keeps .btn-primary and the rest take this —
   an outline in the brand colour, so it still reads as something to press (a plain
   .btn-outline is grey and looked disabled beside a red one). Filled on hover, which is
   the same crimson the primary rests in. */
.pc-btn-quiet { --btn-bg: transparent; --btn-fg: var(--brand-link);
  border-color: color-mix(in srgb, var(--brand-600) 45%, transparent); box-shadow: none; }
.pc-btn-quiet:hover { --btn-bg: var(--brand-600); --btn-fg: #fff; border-color: transparent; }
/* pricing.html only: the shared static-page header spends 40% of the first screen on the
   logo, which every other static page can afford and a page of prices cannot — the plan
   cards started below the fold. Same lockup, smaller. */
.brand-header-compact { gap: 8px; padding: clamp(22px, 4.5vh, 40px) 20px clamp(14px, 2.5vh, 24px); }
.brand-header-compact .brand-hero .brand-logo { height: clamp(44px, 6vw, 54px); }
.brand-header-compact .brand-hero .brand-name { font-size: clamp(1.7rem, 3.6vw, 2.1rem); line-height: 1.3; }

/* Teacher plan. Two real price cards (they ARE a choice between Pro and Elite, like the
   row above) under one seat switch. The head holds the title on the left and the switch
   on the right, and stacks on a phone. */
.pc-teach { margin-top: 24px; }
.pc-teach-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 10px 24px;
  flex-wrap: wrap; margin-bottom: 12px; }
.pc-teach-head h2 { margin: 0 0 2px; }
.pc-teach-head p { margin: 0; max-width: 56ch; }
.pc-teach-pick { display: flex; align-items: center; gap: 10px; }
.pc-teach-pick .pill-opt { min-width: 52px; justify-content: center; font-variant-numeric: tabular-nums; }
.pc-teach-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; align-items: stretch; }
/* The answer-key square, holding the seat count. Fixed at the width "100" needs so the
   plan name beside it does not shift when the count changes. */
.pc-key-seats { width: 48px; font-size: .98rem; font-variant-numeric: tabular-nums; }
/* The figure a teacher actually compares: what one student costs. Sits directly under the
   pack price, tinted with the plan colour so it reads as part of the price, not the list. */
.pc-teach-per { font-size: .88rem; line-height: 1.4; color: var(--ink-2);
  padding: 8px 10px; border-left: 3px solid var(--plan); border-radius: 0 var(--r) var(--r) 0;
  background: color-mix(in srgb, var(--plan) 7%, transparent); }
.pc-teach-per b { color: var(--ink); font-weight: 800; font-variant-numeric: tabular-nums; }
/* "My class" — the teacher's panel, above the two cards. One card: the code is the
   loudest thing in it (it is what gets read out to a room), the list below is quiet. */
.pc-class { padding: 16px 20px; margin-bottom: 16px; border-top: 4px solid var(--ok); }
.pc-class-top { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px 24px; flex-wrap: wrap; }
.pc-class-label { font-size: .78rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--muted-2); }
.pc-class-code { font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: clamp(1.5rem, 4vw, 2rem);
  font-weight: 700; letter-spacing: .08em; color: var(--ink); line-height: 1.2; user-select: all; }
.pc-class-btns { display: flex; align-items: center; gap: 8px 16px; flex-wrap: wrap; }
.pc-class-how { margin: 8px 0 0; max-width: 62ch; }
.pc-class-warn { margin: 10px 0 0; padding: 8px 10px; border-left: 3px solid var(--bad); border-radius: 0 var(--r) var(--r) 0;
  background: var(--bad-bg); color: var(--bad); font-weight: 600; }
.pc-class-count { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); font-size: .95rem; color: var(--ink-2); }
.pc-class-count b { color: var(--ink); font-weight: 800; font-variant-numeric: tabular-nums; }
.pc-class-list { list-style: none; margin: 8px 0 0; padding: 0; display: grid; }
.pc-class-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 4px 16px;
  padding: 8px 0; border-top: 1px solid var(--border); }
.pc-class-who { min-width: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; }
.pc-class-who b { color: var(--ink); font-weight: 700; font-size: .92rem; }
.pc-class-who span { overflow-wrap: anywhere; }
.pc-class-noseat { color: var(--bad); font-weight: 700; }
.pc-class-remove { background: none; border: 1px solid var(--border-2); border-radius: var(--r); padding: 5px 10px;
  font: inherit; font-size: .82rem; font-weight: 600; color: var(--ink-2); cursor: pointer; }
.pc-class-remove:hover { border-color: var(--bad); color: var(--bad); }
.pc-class-remove:disabled { opacity: .5; cursor: default; }
.pc-class-empty { margin: 8px 0 0; }
@media (max-width: 560px) {
  .pc-class-row { grid-template-columns: minmax(0, 1fr) auto; }
  .pc-class-when { grid-column: 1; grid-row: 2; }
  .pc-class-remove { grid-column: 2; grid-row: 1 / span 2; }
}
@media (pointer: coarse) { .pc-class-remove, .pc-class-btns .btn { min-height: 44px; } }
/* The student's side, inside the Class code box when they already sit in a class. */
.pc-seat { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 16px; }
.pc-seat-line { margin: 0; font-size: .92rem; line-height: 1.45; color: var(--ink-2); }
.pc-seat-line b { color: var(--ink); }

/* Foot of a Teacher card: pinned to the bottom like .pc-subscribe, and whatever is in it
   (button or "Not open yet" chip) spans the card. */
.pc-teach-act { margin-top: auto; display: grid; }
@media (pointer: coarse) { .pc-teach-pick .pill-opt { min-height: 44px; } }

/* Capacity tiers. Deliberately NOT price cards: no letter key, no top rule, no feature
   list — they are the same product at a different allowance, and dressing them like the
   three above would put six equal-looking options on one page and make the real choice
   harder to see. Each is two numbers and a price. */
.pc-more { margin-top: 24px; }
.pc-more-head { margin-bottom: 10px; }
.pc-more-head h2 { margin: 0 0 2px; }
/* One panel, a row per tier (compact, 2026-09-15). Hairlines are the 1px gap showing the
   border colour, as in .stat-strip and the dashboard lists. */
.pc-more-list { display: grid; gap: 1px; background: var(--border); overflow: hidden; padding: 0; }
.pc-more-row { display: grid; grid-template-columns: minmax(120px, .7fr) minmax(0, 2fr) minmax(90px, auto) 170px;
  align-items: center; gap: 8px 18px; padding: 10px 16px; background: var(--surface); }
.pc-more-name { font-weight: 700; font-size: .98rem; color: var(--ink); }
/* The two numbers are the whole point, so they are the largest thing on the card and the
   words between them are not. */
.pc-more-nums { font-size: .92rem; color: var(--ink-2); }
.pc-more-nums b { font-size: 1.05rem; font-weight: 800; color: var(--ink); font-variant-numeric: tabular-nums; }
.pc-more-nums span { color: var(--muted-2); }
/* How many people can share the Educator account at once — the thing the tier sells, so
   it leads the row as a chip in the brand tint rather than as one more grey number. */
/* The row's two facts wrap as two units — the chip, then the daily allowance — never
   in the middle of "25 exams · 10 mocks a day". */
.pc-more-nums { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; }
.pc-more-nums .pc-more-day { white-space: nowrap; color: var(--ink-2); }
.pc-more-nums .pc-more-people { display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: var(--r-chip); white-space: nowrap;
  background: color-mix(in srgb, var(--brand-600) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--brand-600) 35%, transparent);
  color: var(--ink); font-weight: 600; }
.pc-more-people svg { width: 14px; height: 14px; color: var(--brand-link, var(--brand-600)); }
.pc-more-people b { font-size: 1.05rem; font-weight: 800; }
.pc-more-price { font-family: var(--font-display); font-size: 1.35rem; font-weight: 500; line-height: 1;
  color: var(--ink); text-align: right; }
.pc-more-act .btn, .pc-more-act .pc-state { display: flex; width: 100%; justify-content: center; }
.pc-more-act .btn { padding-block: 8px; }
.pc-more-price small { font-size: .82rem; color: var(--muted-2); font-family: var(--font-ui); }
.pc-more-row.is-mine { box-shadow: 0 0 0 1px var(--ok) inset; }
/* Both states now, not just the owned one — the plain "Not open yet" chip needs the same
   breathing room under the price. The fixed 8px overrides the base rule's `margin-top: auto`
   on purpose: these three cards hold identical content, so an even gap reads better here
   than a bottom-pinned chip. */
.pc-more-act .pc-state { margin-top: 0; }
.pc-more-act .pc-state-mine { border-color: var(--ok); color: var(--ok); }
/* A phone: name and price on top, the numbers under the name, the button across the foot. */
@media (max-width: 640px) {
  .pc-more-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name price" "nums nums" "act act"; gap: 4px 12px; padding: 12px 14px; }
  .pc-more-name { grid-area: name; } .pc-more-nums { grid-area: nums; } .pc-more-price { grid-area: price; } .pc-more-act { grid-area: act; margin-top: 6px; }
}
.pc-foot { margin-top: 18px; }

/* Promo box. Sits ABOVE the grid, and is the one element on this page allowed to use the
   brand tint as a fill — a limited-time discount is the only thing here that is genuinely
   an announcement. It is a single row on desktop (say + field on one line) because it must
   not push the six price cards below the fold on the laptops most students use. */
.pc-promo {
  margin: 0 0 18px; padding: 12px 18px;
  background: var(--brand-50); border: 1px solid var(--brand-200); border-radius: var(--r-lg);
  display: flex; align-items: baseline; gap: 6px 14px; flex-wrap: wrap;
}
/* --brand-link, not --brand-700. The badge/chip pattern this strip borrows pairs -50 with
   -700, but those two are only ever a few words of chrome; here the crimson carries the
   offer itself, and -700 measures 2.44:1 on the tinted panel in dark mode. --brand-link
   is the ramp's one theme-aware entry, lifted to #da555e there for exactly this reason. */
.pc-promo b { color: var(--brand-link); font-size: 1rem; }
/* Pushed to the end of the row rather than sat next to the countdown: it is the one thing
   in the strip you can act on, and it should not read as a third piece of the sentence. */
.pc-promo-jump {
  margin-left: auto; background: none; border: none; padding: 4px;
  font-size: .84rem; font-weight: 600; color: var(--brand-link);
  text-decoration: underline; cursor: pointer;
}
.pc-promo-jump:hover { color: var(--brand-600); }
/* Applied. The strip stays — removing it would make the struck-through prices above look
   like they had simply always been that way — but it stops advertising and confirms. */
.pc-promo.is-on { border-color: var(--ok); }
.pc-promo.is-on .pc-promo-jump { display: none; }
@media (max-width: 620px) { .pc-promo-jump { margin-left: 0; } }

/* The pre-discount price, struck through beside the new one. Deliberately small and quiet:
   at anything near the .price-amt size two numbers compete and the card stops answering
   "what does this cost" at a glance. .55em keeps it legible next to a 2.4–3rem figure
   without becoming the thing you read first. */
/* Its own line, not inline before the new price. Inline, "$10.99 $8.79" fits on the Pro
   card and wraps on Elite, so the same discount rendered two different ways in one row —
   and the wrap pushed Elite's feature list 48px below the other two. A block sits the old
   number above the new one on every card, at every width, which is also the order the
   comparison is read in. */
.pc-was {
  display: block; font-size: .55em; font-weight: 400; color: var(--muted-2);
  text-decoration-thickness: 1.5px; line-height: 1.2;
}
/* The reserved blank on cards this discount does not touch — Free, and anything priced
   outside Paddle. Present so the row keeps one baseline; hidden so it says nothing. */
.pc-was.is-blank { visibility: hidden; }
/* Inside .pc-alt the surrounding type is already .82rem, so the same .55em would drop
   under the 12px floor the rest of the page keeps to — and that line is Elite's alone,
   so it has no row to stay level with and can stay inline. */
.pc-alt .pc-was { display: inline; font-size: .9em; margin-right: 2px; }
.pc-more-price .pc-was { font-size: .8em; }
/* The dark theme remaps --brand-50 to a translucent crimson but leaves --brand-200 at its
   light value, so the pale pink measured 11.8:1 against the panel it frames — a hairline
   that shouted louder than the offer inside it. Toned to the same translucent crimson at a
   weight that reads as an edge, not a highlight. */
[data-theme="dark"] .pc-promo { border-color: rgba(209, 29, 43, .45); }

/* Redeem box. Framed like a panel rather than a plan card on purpose — it is not a
   fourth thing to choose between, it is the door for someone who already has a code. */
/* Compact (2026-09-15): the words on the left, the field on the right, one row on a wide
   screen; the message spans both. */
.pc-redeem {
  margin-top: 24px; padding: 16px 20px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-lg);
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 380px); align-items: center; gap: 10px 24px;
}
.pc-redeem h2 { font-size: 1.05rem; }
/* Two code boxes side by side (2026-10-05): class code | discount code. Inside the pair
   each box is one column — words, then the field — because half the page is too narrow
   for the words-left / field-right row the single box used. */
.pc-codes { margin-top: 24px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
/* The two boxes are one row and must read as a pair (2026-10-11): the words take the
   spare height and the field sits on the floor of its box, so both fields share a line
   even when one description wraps and the other does not; and the two buttons are one
   width, whatever their labels. The message, when there is one, adds a row below. */
.pc-codes .pc-redeem { margin-top: 0; grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr auto; align-content: stretch; align-items: start; }
.pc-codes .pc-redeem-row .btn { min-width: 104px; justify-content: center; }
/* A code is 12 characters at most; 140px holds one, and the lower basis keeps the button
   beside the field in the narrow two-column range instead of dropping under it. */
.pc-codes .pc-redeem-row .input { flex-basis: 140px; }
@media (max-width: 760px) { .pc-codes { grid-template-columns: minmax(0, 1fr); } }
.pc-redeem-msg { grid-column: 1 / -1; }
@media (max-width: 760px) { .pc-redeem { grid-template-columns: minmax(0, 1fr); } }
.pc-redeem-row { display: flex; gap: 10px; flex-wrap: wrap; }
.pc-redeem-row .input {
  flex: 1 1 180px; min-width: 0;
  font-variant-numeric: tabular-nums; letter-spacing: .06em; text-transform: uppercase;
}
.pc-redeem-row .btn { flex: none; }
.pc-redeem-msg { margin: 0; color: var(--muted); }
.pc-redeem-msg:empty { display: none; }
.pc-redeem-msg.is-ok  { color: var(--ok); font-weight: 600; }
.pc-redeem-msg.is-bad { color: var(--bad); font-weight: 600; }
@media (max-width: 520px) { .pc-redeem-row .btn { width: 100%; } }
/* No breakpoints for this grid: auto-fit already drops a column whenever one no longer
   fits 210px, which is the same thing the two media queries here were doing by hand — and
   they would have fought it the moment a sixth item was added. */

/* ======================================================================
   DARK THEME (applied via [data-theme="dark"] — set from system or toggle)
   ====================================================================== */
[data-theme="dark"] {
  /* --faint measured 3.04:1 on the dark surface — same failure as the light theme, so
     it is lifted to the same 4.5:1 floor. --muted-2 already clears it at 4.99:1. */
  --ink: #eef2f7; --ink-2: #dbe2ec; --muted: #9aa7b8; --muted-2: #7d8a9d; --faint: #768395;
  --bg: #0a0e15; --surface: #141a23; --surface-2: #1c2330; --surface-3: #28313f;
  --border: #232c3b; --border-2: #313c4e;
  /* Lifted well clear of --surface and swung warm. The first pass (#171b21) sat a
     hair off the card colour and read as no change at all on a dark screen. */
  --paper: #24211a;
  --paper-edge: #2b271f;
  /* #3b5bdb measures 2.78:1 against the dark surface — under the 3:1 floor for a
     non-text element, so the outline would fade out. Lifted to ~3.6:1 here. */
  --exam-line: #5c7ce6;
  /* Lifted to the same crimson the slogan's italic already uses in this theme, which was
     picked for exactly this reason: 5.01:1 on --bg, against 3.28:1 for the light value. */
  --brand-link: #da555e;
  --brand-50: rgba(209, 29, 43, .18);
  --rw-bg: rgba(139, 92, 246, .16); --math-bg: rgba(34, 211, 238, .14);
  --ok-bg: rgba(16, 185, 129, .15); --ok-bd: rgba(16, 185, 129, .42);
  --bad-bg: rgba(244, 63, 94, .15); --bad-bd: rgba(244, 63, 94, .42);
  --warn-bg: rgba(245, 158, 11, .15); --warn-bd: rgba(245, 158, 11, .42);
  --shadow-xs: 0 1px 2px rgba(0,0,0,.5);
  --shadow-sm: 0 1px 3px rgba(0,0,0,.5), 0 1px 2px rgba(0,0,0,.4);
  --shadow-md: 0 6px 18px -4px rgba(0,0,0,.6), 0 2px 8px -2px rgba(0,0,0,.5);
  --shadow-lg: 0 22px 48px -14px rgba(0,0,0,.7), 0 6px 16px -8px rgba(0,0,0,.5);
  --shadow-brand: 0 12px 30px -10px rgba(209, 29, 43, .6);
  /* The light ring is brand crimson, which on a near-black surface tops out around 3:1
     even at full opacity — no headroom. Dark gets a lifted crimson instead, the same way
     --exam-line is lifted above, so the focus indicator stays clearly above the 3:1 floor. */
  --ring: 0 0 0 4px rgba(240, 138, 146, .8);
  color-scheme: dark;
}
[data-theme="dark"] body { background: var(--bg); }
[data-theme="dark"] .side-win { background: rgba(22, 28, 38, .72); border-color: rgba(255, 255, 255, .07); }
[data-theme="dark"] .toast { background: var(--surface-3); color: var(--ink); border: 1px solid var(--border-2); }
[data-theme="dark"] .q-num { background: var(--brand-600); color: #fff; }
[data-theme="dark"] .nav-pill { background: var(--brand-600); }
/* Inside a test the .engine remap lands that --brand-600 on --ink-2, which is near-white
   in this theme — so the base rule's #fff label was white on near-white. Same inversion
   as .q-num and .btn-primary. Hover needs restating too: the base rule darkens to #000,
   which under an inverted label would be black on black. */
[data-theme="dark"] .engine .nav-pill { color: var(--bg); }
[data-theme="dark"] .engine .nav-pill:hover { background: var(--ink); color: var(--bg); }
[data-theme="dark"] .btn-dark { background: var(--surface-3); color: var(--ink); }
[data-theme="dark"] .btn-outline { background: transparent; }
[data-theme="dark"] .input { background: var(--surface-2); border-color: var(--border-2); color: var(--ink); }
[data-theme="dark"] .choice { background: var(--surface-2); }
[data-theme="dark"] .choice:hover { border-color: var(--warn); background: rgba(217,119,6,.14); }
[data-theme="dark"] .choice.selected { background: rgba(59,91,219,.18); }
/* the pale --pick-bg tint would be a near-white tile on a dark navigator */
[data-theme="dark"] .nav-cell.answered { background: rgba(59,91,219,.22); color: #b3c0f7; border-color: rgba(59,91,219,.6); }
[data-theme="dark"] .skel { background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%); background-size: 400% 100%; }
[data-theme="dark"] img.brand-logo { filter: drop-shadow(0 6px 10px rgba(0,0,0,.5)); }

/* ======================================================================
   RESPONSIVE — sidebar → off-canvas drawer + mobile topbar
   ====================================================================== */
@media (max-width: 900px) {
  body.has-sidebar .app-main { padding-left: 0; padding-top: calc(78px + env(safe-area-inset-top, 0px)); }
  .sidebar {
    top: 0; left: 0; bottom: 0; width: 280px; padding: 14px;
    background: var(--bg); border-right: 1px solid var(--border);
    transform: translateX(-108%); transition: transform .42s var(--ease-spring); animation: none;
    box-shadow: 0 0 60px rgba(0,0,0,.3);
  }
  .sidebar.open { transform: none; }
  /* The drawer's own logo window comes off on phones. At this width the brand is already
     fixed across the top in .app-topbar, so the drawer opened with a second, larger copy
     of it directly underneath — the same mark twice, ~100px of a short screen spent
     saying nothing, and the first real destination pushed down for it. On desktop the
     window stays: there is no topbar there, so it is the only brand on screen and the
     way back to #/tests. */
  .side-win-logo { display: none; }
  .app-topbar {
    display: flex; align-items: center; gap: 12px; position: fixed; top: 0; left: 0; right: 0; z-index: 55;
    height: calc(62px + env(safe-area-inset-top, 0px)); padding-top: env(safe-area-inset-top, 0px);
    padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px));
    background: rgba(255,255,255,.8); backdrop-filter: blur(16px) saturate(180%);
    -webkit-backdrop-filter: blur(16px) saturate(180%); border-bottom: 1px solid var(--border);
  }
  [data-theme="dark"] .app-topbar { background: rgba(12,16,22,.8); }
  .app-topbar .brand-logo { height: 36px; }
  .app-topbar .brand-name { font-size: 1.1rem; font-weight: 800; letter-spacing: -.02em; color: var(--ink); }
  .scrim { position: fixed; inset: 0; background: rgba(2,6,12,.5); z-index: 58; opacity: 0; pointer-events: none; transition: opacity var(--t); }
  .scrim.show { opacity: 1; pointer-events: auto; }
}
/* The .price-grid single-column override that used to live here is gone: auto-fit with a
   250px minimum reaches one column on its own well before this width, and keeping the
   breakpoint would only have forced 1 where 2 still fits. */

/* ======================================================================
   READING LIBRARY
   ====================================================================== */
/* Text-led card: band + length on one line, then title and author, then the progress bar
   pinned to the bottom. No decorative glyph — see the note in views.js bookCard(). */
/* The reading level is the card's outline rather than a chip inside it. Fifteen books
   on screen meant fifteen little labelled boxes repeating a word the border can say on
   its own. Colours are the ones the app already means by easy / middle / hard.
   The level is still in the text layer for anyone who cannot use the colour — see
   .a11y-only in the card markup; colour is never the only carrier. */
.read-card { padding: 18px; display: flex; flex-direction: column; gap: 6px; height: 100%; cursor: pointer;
  border-color: var(--lvl-bd, var(--border)); background: var(--lvl-bg, var(--surface));
  transition: background var(--t), border-color var(--t); }
.read-card.card-hover:hover { border-color: var(--lvl-bd, var(--brand-200)); }
.rc-easy   { --lvl: var(--ok);   }
.rc-middle { --lvl: var(--warn); }
.rc-hard   { --lvl: var(--bad);  }
/* Both mixes are the Question Bank's, to the percentage, so the two shelves are one
   system in either theme. The outline used to be full-strength --lvl here, which next
   to the Question Bank's softened border read as a different, louder component. */
.rc-easy, .rc-middle, .rc-hard {
  --lvl-bd: color-mix(in srgb, var(--lvl) 55%, var(--border));
  --lvl-bg: color-mix(in srgb, var(--lvl) 6%, var(--surface));
}
/* --bad is a rose whose blue channel outruns the dark surface's, so 6% of it reads as a
   violet-grey rather than red. Same lift the Question Bank's Hard needed — and the
   border goes up with it, or the two shelves drift apart again in this one cell. */
[data-theme="dark"] .rc-hard {
  --lvl-bd: color-mix(in srgb, var(--lvl) 68%, var(--border));
  --lvl-bg: color-mix(in srgb, var(--lvl) 15%, var(--surface));
}
.rc-top { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
.rc-len { font-size: .76rem; color: var(--muted-2); white-space: nowrap; }
.read-card h3 { font-size: 1.08rem; line-height: 1.3; margin-top: 2px; }
.rc-by { font-size: .84rem; color: var(--muted-2); }
/* margin-top:auto pins the footer down however short the title is, so every bar in a row
   sits on the same line. */
.rc-foot { margin-top: auto; padding-top: 14px; }
.rc-prog-lab { display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
               font-size: .75rem; color: var(--muted-2); margin-bottom: 6px; }
.rc-pct { font-weight: 600; color: var(--ink); }   /* a measurement, not an accent */
/* Full books: contents list, and how far through the reader has got. */
.bk-progress { height: 4px; border-radius: 999px; background: var(--border); overflow: hidden; }
.bk-progress span { display: block; height: 100%; background: var(--brand-600); border-radius: 999px; }
.bk-toc { display: grid; gap: 2px; max-height: 60vh; max-height: 60svh; overflow-y: auto; }
.bk-toc-item { display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px; cursor: pointer;
  background: none; border: 0; border-radius: var(--r-md); text-align: left; color: var(--ink-2); font: inherit; }
.bk-toc-item:hover { background: var(--brand-50); color: var(--ink); }
.bk-toc-item.current { background: var(--brand-50); color: var(--brand-700); font-weight: 600; }
.bk-toc-n { flex: none; width: 30px; height: 30px; display: grid; place-items: center; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border); font-size: .78rem; font-weight: 700; color: var(--muted-2); }
.bk-toc-item.current .bk-toc-n { background: var(--brand-600); border-color: var(--brand-600); color: #fff; }
.bk-toc-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bk-toc-w { flex: none; font-size: .76rem; color: var(--faint); }

.band-easy   { background: var(--ok-bg);    color: var(--ok);       border-color: var(--ok-bd); }
/* Amber, not brand red: the Question Bank already uses green / amber / red for Easy /
   Medium / Hard, and a "middle" badge in brand red sat one shade away from "hard"
   (#d11d2b vs #e11d48) — indistinguishable side by side, and on a filter thumb. One
   difficulty ramp across the site. */
.band-middle { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-bd); }
.band-hard   { background: var(--bad-bg);   color: var(--bad);      border-color: var(--bad-bd); }

/* One UI sliding-pill segmented control (reusable, single-select) */
.cefr-toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.cefr-label { margin-right: 2px; }
/* `display: inline-flex` on the class outranks the browser's own [hidden] rule, so the
   dashboard's section bar stayed on screen when it was told to hide. Stated explicitly. */
.pill-seg[hidden] { display: none; }
/* --surface-2 on --border read as almost the same grey as the page underneath it in
   light mode (near 1:1), so the rail vanished and the two options looked like a lone
   red button floating next to plain text rather than one control. One step darker on
   both keeps it subtle but gives the bar an edge that actually shows. */
.pill-seg { position: relative; display: inline-flex; align-items: center; background: var(--surface-3);
  border: 1px solid var(--border-2); border-radius: var(--r-chip); padding: 4px; max-width: 100%;
  box-shadow: var(--shadow-xs); overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
.pill-seg::-webkit-scrollbar { display: none; }
.pill-thumb { position: absolute; top: 4px; bottom: 4px; left: 0; width: 0; border-radius: var(--r-chip);
  background-color: var(--brand-600); box-shadow: var(--shadow-sm); z-index: 0;
  transition: transform var(--t-slow) var(--ease-spring), width var(--t-slow) var(--ease-spring); }
.pill-opt { position: relative; z-index: 1; flex-shrink: 0; display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 15px; border-radius: var(--r-chip); font-weight: 600; font-size: .86rem; color: var(--muted);
  background: transparent; border: none; cursor: pointer; white-space: nowrap; transition: color var(--t); }
.pill-opt svg { width: 15px; height: 15px; }
@media (max-width: 480px) { .pill-opt { padding: 7px 11px; font-size: .8rem; } }
.pill-opt:hover { color: var(--ink); }
.pill-opt.active { color: #fff; }
/* A filter bar on a locked tab: shown so the student can see what they would be buying,
   but faded and unusable. The whole bar dims together — dimming only the buttons would
   leave the sliding thumb at full strength and the control would look half-live.
   No :hover lift and no cursor change: nothing here responds, so nothing should invite
   a press. The buttons carry `disabled` as well, which is what actually stops input. */
.seg-preview { opacity: .45; filter: saturate(.7); cursor: default; user-select: none; }
.seg-preview .pill-opt { cursor: default; }
.seg-preview .pill-opt:hover { color: var(--muted); }
.seg-preview .pill-opt.active:hover { color: #fff; }

/* A tab this plan cannot open. Still clickable — the tap is what explains the padlock —
   so it is dimmed rather than disabled, and the padlock itself is quieter than the label
   so the tab still reads as a place to go rather than as an error. */
.pill-locked { opacity: .62; }
.pill-locked:hover { opacity: 1; }
.pill-locked svg { width: 13px; height: 13px; opacity: .75; }
.pill-locked.active { opacity: 1; }

/* reader */
.reader { width: min(94%, 1200px); margin-inline: auto; }
.reader-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 28px; flex-wrap: wrap; }
.reader-title { min-width: 0; }
.reader-controls { display: flex; flex-direction: column; align-items: flex-end; gap: 12px; flex-shrink: 0; }
@media (max-width: 760px) { .reader-controls { align-items: stretch; width: 100%; flex-shrink: 1; min-width: 0; } }
.reader-body { font-family: var(--font-read); font-size: 1.14rem; line-height: 1.95; color: var(--ink-2);
  background: var(--paper);   /* a book page, not another app card */
  overflow-wrap: break-word; word-break: break-word; hyphens: auto; }
.reader-body p { margin-bottom: 1.05em; }
/* per-word spans — every word is tappable; CEFR highlight is opt-in per level */
.reader-body .w { border-radius: 5px; cursor: pointer; transition: background .12s ease; }
.reader-body .w:hover { background: var(--surface-2); }
/* multi-select highlight: one container class per active level lights its words */
.reader-body.lv-A1 .w[data-lv="A1"] { background: rgba(16, 185, 129, .20); box-shadow: inset 0 -2px 0 #10b981; }
.reader-body.lv-A2 .w[data-lv="A2"] { background: rgba(6, 182, 212, .20);  box-shadow: inset 0 -2px 0 #06b6d4; }
.reader-body.lv-B1 .w[data-lv="B1"] { background: rgba(59, 130, 246, .20); box-shadow: inset 0 -2px 0 #3b82f6; }
.reader-body.lv-B2 .w[data-lv="B2"] { background: rgba(139, 92, 246, .20); box-shadow: inset 0 -2px 0 #8b5cf6; }
.reader-body.lv-C1 .w[data-lv="C1"] { background: rgba(245, 158, 11, .24); box-shadow: inset 0 -2px 0 #f59e0b; }
.reader-body.lv-C2 .w[data-lv="C2"] { background: rgba(239, 68, 68, .24);  box-shadow: inset 0 -2px 0 #ef4444; }
.reader-body .w.picked { background: rgba(209, 29, 43, .18) !important; box-shadow: inset 0 -2px 0 var(--brand-600) !important; }

/* definition panel */
.define-panel { position: fixed; z-index: 120; bottom: max(18px, env(safe-area-inset-bottom, 0px)); right: max(18px, env(safe-area-inset-right, 0px)); width: min(380px, calc(100vw - 36px));
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow-lg);
  padding: 18px 20px; animation: modalIn .32s var(--ease-spring); }
/* word · level · close. The level sits next to the word; margin-left:auto on the close
   button keeps the × pinned right however long the word or the level label is. */
.define-panel .dp-top { display: flex; align-items: baseline; gap: 10px; }
.define-panel .dp-lvl { flex: none; align-self: center; }
.define-panel .dp-x { margin-left: auto; flex: none; align-self: flex-start; }
.define-panel .dp-word { font-family: var(--font-display); font-size: 1.5rem; font-weight: 500; color: var(--ink); }
/* "noun", "adj." — a grammatical label beside the word. The italic already marks it. */
.define-panel .dp-pos { font-size: .85rem; color: var(--muted); font-style: italic; font-weight: 600; }
.define-panel .dp-def { color: var(--ink-2); line-height: 1.55; margin-top: 8px; }
.define-panel .dp-syn { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 6px; }
.define-panel .dp-syn .badge { cursor: pointer; }
/* Word outside the graded list — deliberately quiet, so it never reads as a CEFR grade. */
.dp-nolevel { background: var(--surface-2); color: var(--muted); border-color: var(--border); cursor: help; }
@media (max-width: 560px) { .define-panel { left: 12px; right: 12px; bottom: 12px; width: auto; } }

/* compact definition panel */
.define-panel { width: min(322px, calc(100vw - 32px)); padding: 14px 16px; }
.define-panel .dp-word { font-size: 1.22rem; }
.define-panel .dp-def { font-size: .92rem; line-height: 1.5; margin-top: 6px; }
.define-panel .dp-syn { margin-top: 10px; }
.define-panel .dp-syn .badge { font-size: .72rem; padding: 3px 9px; }

/* ======================================================================
   COLLAPSIBLE ICON-ONLY SIDEBAR (desktop) — One UI focus mode
   ====================================================================== */
/* NOTE: no width/padding transition here — transitioning these layout props on
   the fixed, animated sidebar left the collapse stuck at its old width. The
   collapse snaps; the icons/labels still fade via their own transitions. */
.si-label { white-space: nowrap; }
@media (min-width: 901px) {
  body.sidebar-collapsed .sidebar { width: 78px !important; }
  body.has-sidebar.sidebar-collapsed .app-main { padding-left: 104px !important; }
  body.sidebar-collapsed .side-win { padding: 8px 6px; }
  body.sidebar-collapsed .side-win-logo { padding: 12px 0; }
  body.sidebar-collapsed .side-logo { justify-content: center; gap: 0; }
  body.sidebar-collapsed .side-logo .brand-name { display: none; }
  /* logo stays the SAME size/position when collapsing — only the wordmark hides */
  body.sidebar-collapsed .side-item { justify-content: center; padding: 12px 0; gap: 0; }
  body.sidebar-collapsed .side-item .si-label { display: none; }
  /* The pair exists to spare the rail two CAPTIONED rows. Collapsed there are no
     captions to spare, and side by side each half is only ~29px wide — so it unstacks
     back into two ordinary icon rows and matches the column it sits in. */
  body.sidebar-collapsed .side-duo { flex-direction: column; gap: 3px; }
  body.sidebar-collapsed .side-duo .side-item::after,
  body.sidebar-collapsed .side-duo .side-item + .side-item::before { content: none; }
  body.sidebar-collapsed .side-account { justify-content: center; padding: 10px 0; }
  body.sidebar-collapsed .side-account .sa-text { display: none; }
}

/* ======================================================================
   INPUT CAPABILITY — touch vs pointer
   The sheet had 71 :hover rules and not one (hover:*) or (pointer:*) query, so a
   phone got desktop assumptions: hover states latch on after a tap and stay until
   the next tap elsewhere, and several controls were built at mouse precision.
   These blocks are additive and sit last on purpose — same specificity, later wins.
   ====================================================================== */

/* No hover hardware: kill the states that latch. The worst was .choice — tapping an
   answer left it bordered in --warn ORANGE beside the student's blue selection for the
   rest of the question, which reads as a warning about a choice they just made. */
@media (hover: none) {
  .choice:hover { border-color: var(--border-2); background: var(--surface); }
  .choice.selected:hover { border-color: var(--pick); background: var(--pick-bg); }
  .card-hover:hover { transform: none; box-shadow: var(--shadow-sm); border-color: var(--border); }
  .brand:hover .brand-logo { transform: none; }
}

/* Coarse pointer: raise the near-miss targets to the 44px floor. These were 22-40px —
   usable with a mouse, fiddly with a thumb. Sizes are left alone on fine pointers so
   desktop density does not change. */
@media (pointer: coarse) {
  .pill-opt { min-height: 44px; }
  .qb-skill { min-height: 44px; }
  .icon-btn { min-width: 44px; min-height: 44px; }
  .btn-sm { min-height: 44px; }
  /* 🔴 display:flex, NOT inline-flex. This rule and `.footer-grid a { display: block }`
     have the same specificity, so on a touch device this one won — and it turned the two
     link columns into one run-on line: "Digital SATQuestion BankGenerate Mock". Block-level
     flex keeps each link on its own row and still gives the 44px target. fit-content stops
     the hit area stretching across the whole column, which looked wrong on the brand link;
     inside .social-row the parent is the flex container, so the icons stay side by side. */
  .site-footer a { display: flex; width: fit-content; align-items: center; min-height: 44px; }
  /* Hiding the clock for the rest of a module is a consequential, irreversible-feeling
     action and it sat in a 22px-tall pill directly under the timer. */
  .qr-hide { min-height: 44px; padding-inline: 20px; }
  .pass-eye { min-width: 44px; min-height: 44px; }
}

/* ===== Figure background: --fig-bg (2026-09-26) =====================================
   Numbers inside figures carry a background-coloured OUTLINE (stroke + paint-order, the
   Desmos/Bluebook look) so a graphed line breaks only behind the digit itself; hollow
   markers are filled with the same colour. It must match what is really BEHIND the
   figure — and that is NOT --bg (#f6f7fb, the page): the engine, panels and choices are
   --surface or a state colour, so var(--bg) showed as grey boxes on white (Eljan's
   screenshot, 2024-03 Int-B M1 Q1). Every container a figure can sit in sets --fig-bg,
   mirroring its own background rule; dark-mode states are translucent over --surface,
   so they are composited here by hand. Verified per state with getComputedStyle. */
:root { --fig-bg: var(--surface); }
.passage .figure, .rv-passage, .rv-why-row, .qr-expl { --fig-bg: var(--surface-2); }
.choice:hover { --fig-bg: var(--warn-bg); }
.choice.selected { --fig-bg: var(--pick-bg); }
.choice.correct, .choice.correct:hover, .rv-opt.ok, .rv-why-row.ok { --fig-bg: var(--ok-bg); }
.choice.wrong, .choice.wrong:hover, .rv-opt.bad { --fig-bg: var(--bad-bg); }
[data-theme="dark"] .choice { --fig-bg: var(--surface-2); }
[data-theme="dark"] .choice:hover { --fig-bg: color-mix(in srgb, rgb(217 119 6) 14%, var(--surface)); }
[data-theme="dark"] .choice.selected { --fig-bg: color-mix(in srgb, rgb(59 91 219) 18%, var(--surface)); }
/* (dark .choice.correct/.wrong render --surface-2 — the dark .choice rule wins on source order) */
[data-theme="dark"] .rv-opt.ok, [data-theme="dark"] .rv-why-row.ok { --fig-bg: color-mix(in srgb, rgb(16 185 129) 15%, var(--surface)); }
[data-theme="dark"] .rv-opt.bad { --fig-bg: color-mix(in srgb, rgb(244 63 94) 15%, var(--surface)); }
@media (hover: none) {
  .choice:hover { --fig-bg: var(--surface); }
  .choice.selected:hover { --fig-bg: var(--pick-bg); }
}

/* ---------- Document pages (about.html) ----------
   The same type scale privacy / terms / refund set in their own inline .legal block:
   a 2rem title, 1.2rem section heads, body-colour paragraphs. "Who we are" is a page
   of prose like those, and used to open with the 46px marketing title and a large grey
   lead instead, so it read as a different site. */
/* The policy pages start 40px under the header; .section alone starts lower. */
.section.doc-main { padding-top: 40px; padding-bottom: 80px; }
.doc-page { line-height: 1.7; }
.doc-page h1 { font-size: 2rem; margin-bottom: 14px; }
.doc-page h2 { font-family: var(--font-ui); font-size: 1.2rem; font-weight: 600; margin: 34px 0 10px; }
.doc-page p { margin: 0; color: var(--ink-2); }
.doc-page .backlink { display: inline-block; margin-bottom: 24px; color: var(--brand-link); }

/* Sidebar logo window: pinned at the top and SOLID (Eljan, 2026-10-20, both sites). Every
   .side-win is frosted glass (72% + blur), which is right at rest — but once the logo sticks,
   the windows scrolling under it showed through and it looked washed out. The fill is the
   glass colour already mixed with the page background, so at rest it looks exactly as before;
   it just stops being see-through. */
.sidebar > .side-win-logo { position: sticky; top: 0; z-index: 3; flex-shrink: 0;
  background: color-mix(in srgb, #ffffff 72%, var(--bg));
  backdrop-filter: none; -webkit-backdrop-filter: none; }
[data-theme="dark"] .sidebar > .side-win-logo { background: color-mix(in srgb, rgb(22 28 38) 72%, var(--bg)); }
