/* ===========================================================================
 * subjects/vocab/styles.css — the Vocabulary Builder's own surfaces
 * ===========================================================================
 * SCOPED SHEET. Every rule here is prefixed with :where(.vocab-app), the class
 * VocabApp puts on its root (subjects/vocab/app.jsx) in BOTH embeddings — the
 * Learning Lab workspace view and the standalone Vocabulary Builder product
 * mode. Keep it that way: this sheet loads after every component sheet, so an
 * unscoped rule here silently wins app-wide ties (finding C6).
 *
 * :where() contributes NO specificity, so the prefix restricts matching
 * without shifting any cascade result — e.g. app-surface.css's
 * `html[data-surface] .progress-bar-track` still outranks
 * `.flashcards-screen .progress-bar-track` exactly as before.
 *
 * The app-wide rules that used to live here (:root tokens, the element rules,
 * .topbar, .icon-btn, the .mode-screen / .mode-header / .progress-bar family)
 * moved to shared-chrome.css at the repo root, which loads early.
 * ========================================================================= */
/* ---------------------------------------------------------------------------
 * FOCUS (V-12). The app-wide default computed to `outline-width: 0.909091px`,
 * `outline-offset: 0` — an orange hairline flush against a near-black rounded
 * button, which is WCAG 2.4.11 (Focus Appearance) failed on the one surface
 * where the keyboard is the primary input. 2px wide with 2px of offset, on
 * EVERY focusable control in this subject.
 *
 * :focus-visible only, so a mouse click never paints a ring; the offset means
 * the ring reads against the control's own fill rather than merging with it,
 * and `--focus-ring` falls back to the accent where a tint has not defined one.
 * ------------------------------------------------------------------------ */
:where(.vocab-app) :is(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus-ring, var(--accent, #C96F4A));
  outline-offset: 2px;
}

/* No `min-height: 100vh`: this is the vocab subject root, inside main.stage's
   `zoom` — measured 1045px painted against a 950px window. `.shell` (and
   `.subject-root`'s 806px phone box) carry the same cream from outside it. */
:where(.vocab-app).app { background: var(--bg); }

:where(.vocab-app) .app-main { padding: 0 0 60px; }

:where(.vocab-app) .topbar-right { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-soft); }

/* ---------- BUTTONS ---------- */
:where(.vocab-app) .btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer;
  font-family: var(--font-ui); font-weight: 600; border: 1px solid transparent;
  border-radius: 14px; padding: 12px 18px; font-size: 15px;
  background: var(--surface); color: var(--ink);
  transition: transform var(--dur-quick, 120ms) var(--ease-settle, ease), background var(--dur-quick, 120ms) var(--ease-settle, ease), box-shadow var(--dur-quick, 120ms) var(--ease-settle, ease);
}

:where(.vocab-app) .btn-sm { padding: 9px 14px; font-size: 13px; border-radius: 10px; }

:where(.vocab-app) .btn-lg { padding: 16px 22px; font-size: 16px; border-radius: 16px; }

:where(.vocab-app) .btn-primary { background: var(--ink); color: var(--cream); border-color: var(--ink); }

:where(.vocab-app) .btn-primary:hover { transform: translateY(-1px); box-shadow: var(--shadow); }

:where(.vocab-app) .btn-ghost { background: transparent; color: var(--ink); border-color: var(--ink-10); }

:where(.vocab-app) .btn-ghost:hover { background: var(--surface); border-color: var(--ink-30); }

/* RAISED secondary — a real second action, not a way out. `ghost` is the exit
   treatment (transparent, hairline) and `primary` is the one recommendation, so
   a screen that genuinely offers TWO things to do had no shape for the second
   one (VI-09). The app's surface language already names it: raised = tappable
   = lighter-than-page fill + border + shadow. */
:where(.vocab-app) .btn-raised {
  background: var(--surface); color: var(--ink); border-color: var(--ink-30);
  box-shadow: var(--shadow-sm);
}
:where(.vocab-app) .btn-raised:hover {
  transform: translateY(-1px); box-shadow: var(--shadow); filter: var(--hover-filter, none);
}

:where(.vocab-app) .btn:disabled { opacity: .5; cursor: not-allowed; }

/* ---------- BADGES ---------- */
:where(.vocab-app) .pos-badge {
  font-family: var(--font-display); font-style: italic; font-size: 13px;
  color: var(--ink-50);
}

/* ================= HOME SCREEN ================= */
:where(.vocab-app) .home-screen { padding: 0 20px 40px; }

:where(.vocab-app) .ring-center { display: flex; flex-direction: column; align-items: center; justify-content: center; }

:where(.vocab-app) .ring-num {
  font-family: var(--font-display); font-size: 28px; font-weight: 700;
  line-height: 1; color: var(--ink);
}

:where(.vocab-app) .ring-sub {
  font-size: 11px; color: var(--ink-soft); letter-spacing: 0.4px;
  text-transform: uppercase; margin-top: 2px; font-weight: 600;
}

/* Modes list */
:where(.vocab-app) .modes-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }

:where(.vocab-app) .mode-row {
  display: flex; align-items: center; gap: 14px;
  background: var(--surface); border: 1px solid var(--ink-10);
  border-radius: var(--radius); padding: 14px 16px;
  cursor: pointer; font-family: inherit; color: inherit;
  text-align: left; transition: transform var(--dur-quick, 120ms) var(--ease-settle, ease), box-shadow var(--dur-quick, 120ms) var(--ease-settle, ease);
  width: 100%; box-shadow: var(--shadow-sm);
}

:where(.vocab-app) .mode-row:hover { transform: translateY(-1px); box-shadow: var(--shadow); }

:where(.vocab-app) .mode-row-icon {
  width: 42px; height: 42px; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; color: var(--cream);
}

/* Practice mode icons — distinct hues at matched oklch L/C so the icons read
   as a colorful set (not 3 browns + 1 green) while still sitting in the warm
   cream/ink world.
   SEMANTIC-AXIS RULE (study R2-28/D11, mirrored in shared-icons.jsx
   __CHIP_HUES): a chip labels a CATEGORY, success/danger label an ANSWER, so a
   categorical row must sit >= 30 deg in OKLCH hue from success #5B7A4A
   (H 135.1) and danger #A0412E (H 33.1). `coral` (H 35.1, 2 deg off danger)
   and `sage` (H 154.7, 20 deg off success) FAIL that bar and are no longer
   assigned to any vocab mode row — they stay defined only so nothing that
   still references the class breaks. Do not re-assign them. */
:where(.vocab-app) .mode-row.mode-lavender .mode-row-icon { background: oklch(0.55 0.13 295); }

 /* violet — Definition Quiz */
:where(.vocab-app) .mode-row.mode-teal     .mode-row-icon { background: oklch(0.55 0.10 200); }

 /* teal   — Words in Context */
:where(.vocab-app) .mode-row.mode-indigo   .mode-row-icon { background: oklch(0.52 0.12 250); }

 /* indigo — Fill in the Blank */
:where(.vocab-app) .mode-row.mode-plum     .mode-row-icon { background: oklch(0.50 0.13 340); }

 /* plum   — Synonyms & Antonyms */
:where(.vocab-app) .mode-row.mode-marigold .mode-row-icon { background: oklch(0.62 0.13 75);  }

 /* amber  — Flashcards */
:where(.vocab-app) .mode-row.mode-ink .mode-row-icon { background: var(--ink); }

 /* green      — RETIRED, see rule above */
:where(.vocab-app) .mode-row-body { flex: 1; min-width: 0; }

:where(.vocab-app) .mode-row-title {
  font-family: var(--font-display); font-size: 16px; font-weight: 700;
  letter-spacing: -0.01em; line-height: 1.15;
  display: flex; align-items: center; gap: 8px; min-width: 0;
}

:where(.vocab-app) .mode-row-resume {
  font-family: var(--font-ui, inherit);
  font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 7px 2.5px; border-radius: 99px;
  background: var(--ink); color: var(--cream, #fff);
  flex-shrink: 0;
}

:where(.vocab-app) .mode-row-sub { font-size: 13px; color: var(--ink-soft); margin-top: 2px; line-height: 1.35; }

:where(.vocab-app) .mode-row-arrow { color: var(--ink-30); flex-shrink: 0; }

:where(.vocab-app) .mode-row:hover .mode-row-arrow { color: var(--ink); }

/* Sections */
:where(.vocab-app) .section-h {
  display: flex; justify-content: space-between; align-items: baseline;
  margin: 24px 0 12px;
}

:where(.vocab-app) .section-h h2 {
  font-family: var(--font-display); font-size: 18px;
  font-weight: 600; letter-spacing: -0.01em; margin: 0;
}

/* Word of the day */
/* Read-only → inset surface, no border/shadow (see surface language in
   shared-app-shell.jsx). The word chips below it ARE tappable, so they keep
   the raised --surface treatment. */
:where(.vocab-app) .wotd-card {
  background: var(--bg-inset, var(--cream-2)); border: none;
  border-radius: var(--radius-inset); padding: 20px;
  box-shadow: none; position: relative; overflow: hidden;
  margin-bottom: 8px;
}

:where(.vocab-app) .wotd-card::before {
  content: ""; position: absolute; top: -30px; right: -30px;
  width: 100px; height: 100px; background: var(--accent-marigold-tint);
  border-radius: 50%; z-index: 0;
}

:where(.vocab-app) .wotd-head { display: flex; gap: 6px; align-items: center; margin-bottom: 12px; position: relative; z-index: 1; }

:where(.vocab-app) .wotd-word {
  font-family: var(--font-display); font-size: 32px; font-weight: 700;
  letter-spacing: -0.02em; line-height: 1.05; margin-bottom: 4px;
  position: relative; z-index: 1;
}

:where(.vocab-app) .wotd-phon {
  font-family: var(--font-mono); font-size: 13px; color: var(--ink-soft);
  margin-bottom: 12px; position: relative; z-index: 1;
}

:where(.vocab-app) .wotd-def { font-size: 14px; line-height: 1.5; margin-bottom: 8px; position: relative; z-index: 1; }

:where(.vocab-app) .wotd-ex {
  font-family: var(--font-display); font-style: italic;
  font-size: 14px; color: var(--ink-70); line-height: 1.45;
  position: relative; z-index: 1;
}

/* Recent sessions */
:where(.vocab-app) .session-list { display: flex; flex-direction: column; gap: 8px; }

:where(.vocab-app) .session-row {
  display: grid; grid-template-columns: 1fr auto auto; gap: 12px; align-items: center;
  padding: 12px 14px; background: var(--surface); border: 1px solid var(--ink-10);
  border-radius: var(--radius);
  font-family: inherit; color: inherit; text-align: left;
  width: 100%; box-sizing: border-box;
}

:where(.vocab-app) .session-row-clickable {
  cursor: pointer;
  grid-template-columns: 1fr auto auto auto;
  transition: border-color var(--dur-quick, 120ms) var(--ease-settle, ease), background var(--dur-quick, 120ms) var(--ease-settle, ease);
}

:where(.vocab-app) .session-row-clickable:hover { border-color: var(--accent, #c6623c); }

:where(.vocab-app) .session-row-chev { color: var(--ink-soft); flex-shrink: 0; }

:where(.vocab-app) .session-mode {
  font-size: 13px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

:where(.vocab-app) .session-score { font-variant-numeric: tabular-nums; font-size: 13px; }

:where(.vocab-app) .score-big { font-family: var(--font-display); font-size: 18px; font-weight: 700; }

:where(.vocab-app) .score-small { color: var(--ink-soft); font-size: 13px; }

:where(.vocab-app) .session-date { font-size: 12px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

:where(.vocab-app) .session-bar { display: none; }

 /* hide bar in narrow layout */

/* Word chips grid — auto-fills the column it is given (the deck is a wide mode
   screen now), with a floor of 2: min() drops the track min to half the row
   minus half the gap once the container is narrower than 2×165, so a small
   phone still gets two chips per row instead of one column of stripes. */
:where(.vocab-app) .word-chip-grid {
  display: grid; gap: 6px;
  grid-template-columns: repeat(auto-fill, minmax(min(165px, calc(50% - 3px)), 1fr));
}

:where(.vocab-app) .word-chip {
  background: var(--surface); border: 1px solid var(--ink-10); border-radius: 10px;
  padding: 9px 12px; cursor: pointer; font-family: inherit;
  /* V-05: the deck grid's chips measured ~35px. */
  min-height: 44px;
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  color: var(--ink); text-align: left; font-size: 13px;
  transition: border-color var(--dur-quick, 120ms) var(--ease-settle, ease);
}

:where(.vocab-app) .word-chip:hover { border-color: var(--ink-30); }

:where(.vocab-app) .wc-word {
  font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  hyphens: auto; -webkit-hyphens: auto; overflow-wrap: break-word;
}

/* Long words (>12 chars, study C15 — "compassionat/e" mid-word breaks):
   drop the single-line ellipsis truncation in favor of a wrapped, hyphenated
   break plus a smaller type size, so the whole word stays legible instead of
   being cut. The grid row auto-sizes, so a chip growing to two lines here
   doesn't disturb its neighbors. */
:where(.vocab-app) .word-chip--long .wc-word {
  white-space: normal; font-size: 13px; line-height: 1.3;
}

/* Status fills. The FILL is the status signal; the LABEL is always the deck's
   near-black ink (study R2-27/D10). --accent-sage-ink is rebound at runtime to
   theme.success (#5B7A4A), which on the sage tint measured 3.77:1 — the words
   the student had actually earned were the faintest thing on the screen and
   read as disabled next to the 14.19:1 un-met chips. Inheriting .word-chip's
   var(--ink) puts mastered at 11.67:1 and learning at 8.87–11.61:1 across
   every tint, so all three states share one label ink and only the paper
   changes. Do NOT re-introduce a per-status .wc-word color. */
:where(.vocab-app) .word-chip-mastered { background: var(--accent-sage-tint); border-color: transparent; }

:where(.vocab-app) .word-chip-learning { background: var(--accent-marigold-tint); border-color: transparent; }

/* Chip legend (R2-27) — one read-only line above the grid so green/gold are
   decoded rather than guessed. The swatches wear the SAME classes the chips
   do (.word-chip + status), so the legend cannot drift from the grid: a hue
   edit above moves both. --swatch only strips the button geometry. */
:where(.vocab-app) .word-chip--swatch {
  width: 13px; height: 13px; min-width: 0; flex: none;
  /* min-height too, or .word-chip's 44px tap floor wins over `height` and the
     13px square renders as a 13×44 BAR — which is what shipped, and is why the
     filter pills read as decoration rather than as the key they are (VI-14). */
  min-height: 0;
  padding: 0; border-radius: 4px; cursor: default; pointer-events: none;
}

:where(.vocab-app) .ses-stat { font-size: 12px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

:where(.vocab-app) .ses-known { color: var(--accent-sage-ink); font-weight: 700; }

:where(.vocab-app) .ses-studying { color: var(--accent-coral-ink); font-weight: 600; }

/* ================= FLASHCARDS ================= */
:where(.vocab-app) .flashcards-screen {
  display: flex; flex-direction: column; gap: 14px;
}

:where(.vocab-app) .flashcards-screen .progress-bar-track,
:where(.vocab-app) .flashcards-screen .flashcard-stage { margin-left: 20px; margin-right: 20px; }

/* V-64 — the two grade buttons pinned to the foot of the scroller, the same
   idiom as the quiz's advance CTA (.quiz-advance above). On a 956pt phone the
   card fills the viewport, so a flipped card used to cost a scroll before it
   could be graded — and grading IS the mode's loop. The bar keeps its
   normal-flow slot, so nothing jumps when it pins or unpins; the card itself
   stays in the page flow and scrolls under it. */
:where(.vocab-app) .flashcards-screen .rate-row {
  position: sticky;
  bottom: 0;
  z-index: 3;
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px));
  background: var(--bg);
  border-top: 1px solid var(--line);
}

/* A sticky element can't leave its containing block: the screen's own 40px
   bottom padding would strand the bar above the end of the scroll. The bar IS
   the bottom here. */
:where(.vocab-app) .flashcards-screen { padding-bottom: 0; }

:where(.vocab-app) .flashcard-stage {
  perspective: 1200px; min-height: 360px;
  display: flex; align-items: center; justify-content: center;
}

/* Grid stack, not absolutely-positioned faces: the card GROWS with whichever
   face is taller, so the page is the only vertical scroller. Absolute faces
   forced the back to scroll inside itself, and on a phone a pan started on the
   card moved that inner box instead of the page. min-height keeps a short
   card's proportions. */
:where(.vocab-app) .flashcard {
  width: 100%; min-height: 360px; display: grid;
  transform-style: preserve-3d;
  transition: transform var(--dur-settle, 220ms) var(--ease-settle, ease); cursor: pointer;
}

/* The card is role="button"; a div gets no focus ring for free. The width and
   offset come from the subject-wide rule at the top of this sheet (V-12); this
   only rounds the ring to the card's own corner. */
:where(.vocab-app) .flashcard:focus-visible {
  border-radius: var(--radius-lg);
}

:where(.vocab-app) .flashcard.flipped { transform: rotateY(180deg); }

:where(.vocab-app) .flashcard-face {
  grid-area: 1 / 1; position: relative; inset: auto; backface-visibility: hidden;
  background: var(--surface); border: 1px solid var(--ink-10);
  border-radius: var(--radius-lg); padding: 22px;
  display: flex; flex-direction: column; box-shadow: var(--shadow);
}

:where(.vocab-app) .flashcard-front { background: linear-gradient(135deg, var(--surface) 0%, var(--cream-2) 100%); }

/* No overflow of its own — see .flashcard above. */
:where(.vocab-app) .flashcard-back { transform: rotateY(180deg); }

/* PHONE: the card and its rate row share one viewport. 360px + the header + the
   progress track + 22px of face padding pushed the rate row under the fold on a
   667pt phone (measured: rate-row bottom ~40px past it). Cap the card against
   the viewport there; tablet/desktop keep the settled 360. vh first, dvh for
   browsers that know it. */
:where(html[data-surface="phone"]) :where(.vocab-app) .flashcard-stage,
:where(html[data-surface="phone"]) :where(.vocab-app) .flashcard {
  min-height: min(360px, 44vh);
  min-height: min(360px, 44dvh);
}

/* …and the card is usually taller than that floor anyway: both faces share one
   grid cell, so a rich BACK (example + hint + parts + synonyms) sets the height
   of the front too. The rest of the deficit is chrome — a tighter stack and a
   tighter face on the phone, worth ~32px, which is what put the rate row back
   on screen for a typical word. Tablet/desktop are untouched. */
:where(html[data-surface="phone"]) :where(.vocab-app) .flashcards-screen { gap: 10px; }
:where(html[data-surface="phone"]) :where(.vocab-app) .flashcard-face { padding: 16px; }


:where(.vocab-app) .card-corner { display: flex; gap: 6px; align-items: center; }

:where(.vocab-app) .card-center { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; }

:where(.vocab-app) .card-word {
  font-family: var(--font-display);
  /* V-61: a fixed 44px headword forced a mid-word break on a narrow shell, and
     a hyphenless split reads as two words — exactly wrong on the one element
     the card exists to teach. The clamp shrinks the type instead; `anywhere`
     is the last resort for a headword too long even at 28px, where the old
     `break-word` broke while there was still room to shrink. */
  font-size: clamp(28px, 11vw, 44px);
  font-weight: 700; line-height: 1; text-align: center; letter-spacing: -0.02em;
  overflow-wrap: anywhere;
}

:where(.vocab-app) .card-phon { font-family: var(--font-mono); font-size: 13px; color: var(--ink-soft); }

:where(.vocab-app) .speak-btn {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--cream); border: 1px solid var(--ink-10); border-radius: 999px;
  padding: 7px 14px; cursor: pointer; font-family: inherit; font-size: 13px;
  color: var(--ink); margin-top: 10px;
  /* V-05: measured ~30px tall. Real height, not an overlay — it sits in a
     column with room, and a 44px pill reads as the control it is. */
  min-height: 44px; justify-content: center;
}

:where(.vocab-app) .card-hint {
  text-align: center; font-size: 11px; color: var(--ink-30);
  letter-spacing: 0.08em; text-transform: uppercase;
}

:where(.vocab-app) .card-back-body { display: flex; flex-direction: column; gap: 12px; padding-top: 12px; }

:where(.vocab-app) .back-word { font-family: var(--font-display); font-weight: 700; font-size: 26px; line-height: 1; letter-spacing: -0.02em; }

:where(.vocab-app) .back-def { font-size: 15px; line-height: 1.45; }

:where(.vocab-app) .back-section { border-top: 1px solid var(--ink-10); padding-top: 10px; }

:where(.vocab-app) .back-label { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 4px; font-weight: 700; }

:where(.vocab-app) .back-example { font-family: var(--font-display); font-style: italic; font-size: 14px; color: var(--ink-70); line-height: 1.4; }

:where(.vocab-app) .back-mnemonic { font-size: 13px; color: var(--ink); }

:where(.vocab-app) .back-etym { font-size: 13px; color: var(--ink-70); line-height: 1.4; }

:where(.vocab-app) .back-pills { display: flex; flex-wrap: wrap; gap: 5px; }

:where(.vocab-app) .pill { font-size: 12px; padding: 3px 10px; border-radius: 999px; }

/* Relation pills (synonyms / antonyms) — CATEGORICAL hues, never the semantic
   pair. `--accent-sage-*` is bound to theme.success/successBg (foundation.js
   :101-109) and `--accent-coral-*` to the accent, so these two lists used to
   paint "same meaning" in the right-answer green and "opposite meaning" in the
   brand accent, on three surfaces (deck peek, flashcard back, review card).
   CLAUDE.md: semantic tokens label an ANSWER; the named carve-outs are
   destructive controls and the exam clock, not content labels.
   The pair below is the one this subject already vetted for exactly this
   distinction — ui.jsx REL_TONE, teal H200 for SAME and plum H340 for
   OPPOSITE, both >= 30deg OKLCH from success and danger and >= 5:1 on their
   own tint. Same meaning, same colour, wherever it is said. */
:where(.vocab-app) .pill-sage { background: oklch(0.95 0.035 200); color: oklch(0.42 0.10 200); }

:where(.vocab-app) .pill-coral { background: oklch(0.95 0.040 340); color: oklch(0.42 0.13 340); }

:where(.vocab-app) .rate-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

:where(.vocab-app) .rate-btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; padding: 14px 14px; min-height: 62px;
  border: 2px solid var(--ink-10); border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
  font-family: inherit; cursor: pointer; transition: all var(--dur-quick, 120ms) var(--ease-settle, ease);
}

/* VI-21 — BOTH answers are raised, and neither is primary. "I know this word"
   used to be a near-black filled button beside a light card: the emphasised
   option was the one that REMOVES the word from study, and the two are equally
   honest answers to "did you know it?". Same fill, same border, same size; the
   resting shadow is what makes them read as the pair of controls they are
   (surface language: raised = tappable). */
:where(.vocab-app) .rate-btn { box-shadow: var(--shadow-sm); }

:where(.vocab-app) .rate-btn:hover { transform: translateY(-1px); box-shadow: var(--shadow); }

:where(.vocab-app) .rate-btn-label { font-size: 14px; font-weight: 700; letter-spacing: -0.01em; }

:where(.vocab-app) .rate-btn-sub { font-size: 13px; color: var(--ink-soft); }

:where(.vocab-app) .rate-skip { border-color: var(--ink-10); }

/* VI-21: no fill, no ink inversion — see .rate-btn above. The class survives
   for the disabled state below and as the hook the JS already targets; if a
   future change wants to tell the two apart again, do it in the LABEL, not by
   promoting one of them to a primary. */

:where(.vocab-app) .rate-reviewed.is-disabled,
:where(.vocab-app) .rate-reviewed:disabled {
  background: var(--ink-10); border-color: var(--ink-10); color: var(--ink-30);
  cursor: not-allowed; transform: none; box-shadow: none;
}

:where(.vocab-app) .rate-reviewed.is-disabled .rate-btn-label,
:where(.vocab-app) .rate-reviewed:disabled .rate-btn-label { color: var(--ink-30); }

:where(.vocab-app) .rate-reviewed.is-disabled .rate-btn-sub,
:where(.vocab-app) .rate-reviewed:disabled .rate-btn-sub { color: var(--ink-30); }

/* ================= QUIZ ================= */
:where(.vocab-app) .quiz-screen { display: flex; flex-direction: column; }

:where(.vocab-app) .quiz-screen .quiz-body { padding: 0 20px; display: flex; flex-direction: column; gap: 14px; }

/* THE QUESTION'S MEASURE (VB-03). This used to be a vocab-local
   `max-width: 600px; margin-inline: auto`, at `data-surface="desktop"` only.
   Two defects came out of those three declarations:
     · `margin-inline: auto` CENTRED the body inside the column, so the
       question started at a different x than the `.mode-header`, the eyebrow
       and the `.progress-bar-track` above it — three left edges on the app's
       most-used screen, against the Surfaces rule that all four share one
       column. (A narrower measure is fine; a different LEFT EDGE is not — the
       app-wide rule below caps without centring for exactly this reason.)
     · desktop-only + a local number INVERTED the surface ladder: an iPad Air
       11 portrait (tablet, no rule) got a 748px question column while an iPad
       Pro 13 portrait (desktop) got 660. The bigger iPad showed the smaller
       question.
   Both go away by deferring to the app-wide decision instead of restating it:
   `--content-measure` (app-surface.css) is the ONE COLUMN FOR ONE QUESTION
   token that already caps `.mc-stem`, `.mc-choices` and the why block at both
   tablet and desktop — so the vocab prompt card now agrees with the choice
   list it sits above, at every surface, on the same left edge. */
html[data-surface="tablet"] :where(.vocab-app) .quiz-body,
html[data-surface="desktop"] :where(.vocab-app) .quiz-body { width: 100%; max-width: var(--content-measure); }

/* Read-only stimulus → inset, per the app's surface language (study R2-25):
   this card wore the same var(--surface) fill + shadow as the tappable
   choices below it, which is a dead-tap invitation on the most repeated
   screen in the app. */
:where(.vocab-app) .quiz-prompt-card {
  background: var(--bg-inset); border: none;
  border-radius: var(--radius-inset); padding: 20px; box-shadow: none;
}

:where(.vocab-app) .quiz-instr {
  font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-soft); margin-bottom: 12px; font-weight: 700;
}

:where(.vocab-app) .quiz-word-row { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }

:where(.vocab-app) .quiz-word {
  font-family: var(--font-display); font-size: 36px; font-weight: 700;
  letter-spacing: -0.02em; line-height: 1; word-break: break-word;
}

:where(.vocab-app) .quiz-sentence {
  font-size: 17px; line-height: 1.5; font-family: var(--font-serif-body);
}

:where(.vocab-app) .quiz-sentence strong {
  font-weight: 700;
}

:where(.vocab-app) .quiz-subprompt { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-soft); flex-wrap: wrap; }

:where(.vocab-app) .quiz-options { display: grid; gap: 8px; }

:where(.vocab-app) .opt-def { font-size: 13px; color: var(--ink-70); line-height: 1.4; }

:where(.vocab-app) .opt-src { color: var(--ink-soft); }

:where(.vocab-app) .opt-src em { font-family: var(--font-display); font-style: italic; color: var(--ink-70); }


:where(.vocab-app) .opt-elim {
  align-self: center;
  width: 28px; height: 28px; border-radius: 999px;
  border: 1px solid var(--ink-30);
  background: transparent; color: var(--ink-soft);
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; cursor: pointer; transition: all var(--dur-quick, 120ms) var(--ease-settle, ease);
}

:where(.vocab-app) .opt-elim:hover { border-color: var(--ink-soft); color: var(--ink); }

:where(.vocab-app) .opt-elim.is-on {
  background: var(--accent-coral-tint);
  border-color: var(--accent-coral);
  color: var(--accent-coral-ink);
}

:where(.vocab-app) .opt-mark { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }

:where(.vocab-app) .opt-mark-correct { background: var(--accent-sage); color: #fff; }

:where(.vocab-app) .opt-mark-wrong { background: var(--accent-coral); color: #fff; }

/* McItem .mc-* rules retired here in favor of the canonical unscoped
 * rules in components/items/mc-item.css (Sub-phase D). Vocab inherits the
 * canonical visual; no surface-specific tweaks needed. */

/* AnalogyItem .an-* rules retired here in favor of the canonical
 * unscoped rules in components/items/analogy-item.css (Sub-phase D part 2).
 * Vocab quiz inherits the canonical visual.
 *
 * The .exam-screen tree that used to override .an-* here went with exam.jsx;
 * only the REVIEW surface still renders analogy stems, so only its two rules
 * survive. */

:where(.vocab-app) .exam-review .an-eyebrow { display: none; }

:where(.vocab-app) .exam-review .an-stem {
  font-family: var(--font-display);
  font-size: 22px; font-weight: 600; letter-spacing: -0.005em;
  line-height: 1.3; margin: 0 0 12px;
  text-wrap: pretty;
}


:where(.vocab-app) .quiz-reveal {
  background: var(--accent-marigold-tint); border-radius: var(--radius);
  padding: 16px 18px; display: flex; flex-direction: column; gap: 6px;
  animation: slideUp var(--dur-settle) var(--ease-settle);
}

@keyframes slideUp { from { opacity: 0; transform: translateY(10px); } }

:where(.vocab-app) .reveal-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }

:where(.vocab-app) .reveal-word { font-family: var(--font-display); font-size: 20px; font-style: italic; font-weight: 600; }

:where(.vocab-app) .reveal-phon { font-family: var(--font-mono); font-size: 13px; color: var(--ink-70); }

:where(.vocab-app) .reveal-def { font-size: 14px; line-height: 1.4; }

:where(.vocab-app) .reveal-ex { font-family: var(--font-display); font-style: italic; font-size: 13px; color: var(--ink-70); line-height: 1.4; }

:where(.vocab-app) .reveal-hint { font-size: 13px; padding: 8px 12px; background: rgba(255,255,255,0.55); border-radius: 10px; }

/* P0.6 — advance CTA pinned to the foot of the scroller.
   Before: the button was the last line of the reveal card, so every wrong
   answer cost a scroll at the exact moment a discouraged student decides
   whether to continue. Sticky INSIDE the app's scroller (.shell), full-bleed
   against the page background with a hairline above — the same idiom as the
   onboarding footer (.onboard-foot, index.html). The bar keeps its normal-flow
   slot, so nothing shifts when it pins or unpins. */
:where(.vocab-app) .quiz-screen .quiz-advance {
  position: sticky;
  bottom: 0;
  z-index: 3;
  /* Full-bleed against .quiz-body's 20px gutters. */
  margin: 0 -20px;
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px));
  background: var(--bg);
  border-top: 1px solid var(--line);
  /* VI-20 — ONE HEIGHT ACROSS EVERY BEAT. The bar's content changes between
     beats (a one-line "Next", a two-line "On to the next round" on a narrow
     phone, an icon that a locale drops), and every change moved the button the
     student is about to tap. It reserves the tallest of them instead, so the
     target is in the same place on every question of a sitting. */
  box-sizing: border-box;
  min-height: var(--action-bar-min-h, 76px);
  display: flex; align-items: center;
}

:where(.vocab-app) .quiz-screen .quiz-advance .btn { width: 100%; }

/* A sticky element can't leave its containing block, so the screen's own 40px
   bottom padding would strand the bar 40px above the end of the scroll. While
   the bar is up, the bar IS the bottom. */
:where(.vocab-app) .quiz-screen.has-advance { padding-bottom: 0; }

/* ================= SESSION COMPLETE ================= */
:where(.vocab-app) .session-complete {
  text-align: center; padding: 40px 20px 60px;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
}

:where(.vocab-app) .sc-burst { animation: pop var(--dur-settle) var(--ease-settle); }
/* V-22: both now run on the duration tokens, which collapse to 0ms under
   reduced motion. The explicit guard stays as the belt to that braces — an
   animation at 0ms still fires its from-state for a frame in some engines. */
@media (prefers-reduced-motion: reduce) {
  :where(.vocab-app) .sc-burst { animation: none; }
  :where(.vocab-app) .quiz-reveal { animation: none; }
}

@keyframes pop { from { transform: scale(0.3); opacity: 0; } }

:where(.vocab-app) .sc-title {
  font-family: var(--font-display); font-size: 30px; font-style: italic;
  font-weight: 500; margin: 0;
}

:where(.vocab-app) .sc-stats { display: flex; gap: 26px; margin: 16px 0; }

:where(.vocab-app) .sc-stat { text-align: center; }

:where(.vocab-app) .sc-num {
  font-family: var(--font-display); font-size: 36px; font-style: italic;
  line-height: 1; color: var(--accent-coral-ink);
}

:where(.vocab-app) .sc-lbl { font-size: 11px; color: var(--ink-soft); margin-top: 4px; letter-spacing: 0.06em; }

:where(.vocab-app) .sc-actions { display: flex; gap: 10px; margin-top: 16px; width: 100%; }

:where(.vocab-app) .sc-actions > * { flex: 1; }

:where(.vocab-app) .empty-state { text-align: center; padding: 60px 20px; }

:where(.vocab-app) .empty-state h2 { font-family: var(--font-display); font-size: 22px; font-weight: 600; }

/* ================= STATS ================= */
:where(.vocab-app) .stats-section {
  background: var(--bg-inset, var(--cream-2)); border: none; border-radius: var(--radius-inset);
  padding: 18px; margin: 0 20px 12px;
}

:where(.vocab-app) .stats-section h3 {
  font-family: var(--font-display); font-size: 16px;
  font-weight: 600; margin: 0 0 14px;
}

:where(.vocab-app) .empty-inline { color: var(--ink-soft); font-size: 13px; padding: 14px 0; text-align: center; }


:where(.vocab-app) .tab {
  background: none; border: none; padding: 9px 10px; font-family: inherit; font-size: 13px;
  color: var(--ink-soft); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px;
}

:where(.vocab-app) .tab.active { color: var(--ink); border-bottom-color: var(--accent-coral); font-weight: 600; }

:where(.vocab-app) .field {}

:where(.vocab-app) .field-label { font-size: 13px; font-weight: 600; margin-bottom: 4px; }

:where(.vocab-app) .field-hint { font-size: 13px; color: var(--ink-soft); margin-bottom: 8px; }

:where(.vocab-app) .chip {
  padding: 6px 12px; border-radius: 999px; border: 1px solid var(--ink-10);
  background: var(--surface); cursor: pointer; font-family: inherit; font-size: 12px; color: var(--ink);
  text-transform: capitalize;
}

:where(.vocab-app) .chip.active { background: var(--ink); color: var(--cream); border-color: var(--ink); }

:where(.vocab-app) .toggle {
  width: 40px; height: 22px; border-radius: 999px; background: var(--ink-10); border: none;
  position: relative; cursor: pointer; padding: 0; transition: background var(--dur-quick, 120ms) var(--ease-settle, ease);
}

:where(.vocab-app) .toggle.on { background: var(--accent-sage); }

:where(.vocab-app) .toggle-dot {
  position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; background: #fff;
  border-radius: 50%; transition: left var(--dur-quick, 120ms) var(--ease-settle, ease);
}

:where(.vocab-app) .toggle.on .toggle-dot { left: 20px; }

:where(.vocab-app) .msg { font-size: 12px; color: var(--accent-sage-ink); padding: 7px 10px; background: var(--accent-sage-tint); border-radius: 8px; }

/* ================= SESSION REVIEW ================= */
/* What is left of the old exam sheet after exam.jsx was deleted: the replay
   surface (review.jsx) reuses the exam question/option/score visuals, so the
   `.exam-*` names stay, but nothing here is scoped to a live sitting any more. */


:where(.vocab-app) .exam-qtype {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-soft); margin-bottom: 14px; font-weight: 700;
}

:where(.vocab-app) .exam-prompt-syn {
  font-family: var(--font-display); font-size: 36px; font-weight: 700;
  letter-spacing: 0.02em; color: var(--ink); text-align: center;
  padding: 26px 16px; background: var(--surface);
  border-radius: var(--radius-lg); margin-bottom: 20px;
  border: 1px solid var(--ink-10);
}

:where(.vocab-app) .exam-prompt-sent {
  font-size: 17px; line-height: 1.55; color: var(--ink);
  padding: 20px; background: var(--surface);
  border-radius: var(--radius-lg); margin-bottom: 20px;
  border: 1px solid var(--ink-10);
  font-family: var(--font-display); font-weight: 500;
}

:where(.vocab-app) .exam-options { display: flex; flex-direction: column; gap: 8px; }

:where(.vocab-app) .exam-opt {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border: 2px solid var(--ink-10);
  border-radius: 12px; padding: 12px 14px;
  cursor: pointer; font-family: inherit; color: var(--ink);
  text-align: left; transition: all var(--dur-quick, 120ms) var(--ease-settle, ease); font-size: 14px;
  position: relative;
}

:where(.vocab-app) .exam-opt.picked { border-color: var(--ink); background: var(--bg); }

:where(.vocab-app) .exam-opt.eliminated { opacity: 0.5; cursor: default; }

:where(.vocab-app) .exam-opt.eliminated .exam-opt-text {
  text-decoration: line-through;
  text-decoration-thickness: 1.5px;
  text-decoration-color: var(--ink-30);
}

:where(.vocab-app) .exam-opt-letter {
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--bg); border: 1.5px solid var(--ink-10);
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 12px; flex-shrink: 0;
}

:where(.vocab-app) .exam-opt.picked .exam-opt-letter { background: var(--ink); color: var(--surface); border-color: var(--ink); }

:where(.vocab-app) .exam-opt-text { flex: 1; line-height: 1.4; }

:where(.vocab-app) .exam-opt-tag {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  padding: 3px 7px; border-radius: 999px; font-weight: 700; flex-shrink: 0;
}

:where(.vocab-app) .tag-correct { background: var(--accent-sage); color: white; }

:where(.vocab-app) .tag-wrong { background: var(--accent-coral); color: white; }

:where(.vocab-app) .tag-correct-picked { background: var(--accent-sage); color: white; }


/* Review */
:where(.vocab-app) .exam-review { padding: 0 0 40px; }

:where(.vocab-app) .exam-score-card {
  background: var(--bg-inset, var(--cream-2)); border: none;
  border-radius: var(--radius-inset); padding: 20px; margin: 0 20px 12px;
  display: flex; flex-direction: column; gap: 14px; align-items: stretch;
}

:where(.vocab-app) .exam-score-main { display: flex; align-items: baseline; gap: 12px; }

:where(.vocab-app) .exam-score-pct {
  font-family: var(--font-display); font-size: 48px; font-weight: 700;
  line-height: 1; font-variant-numeric: tabular-nums;
}

:where(.vocab-app) .ex-pct-sign { font-size: 24px; color: var(--ink-soft); margin-left: 2px; }

:where(.vocab-app) .exam-score-sub { font-size: 13px; color: var(--ink-70); }

:where(.vocab-app) .exam-score-actions { display: flex; gap: 8px; }

:where(.vocab-app) .exam-score-actions > * { flex: 1; }

:where(.vocab-app) .exam-review-grid { display: flex; flex-direction: column; gap: 10px; padding: 0 20px; }

:where(.vocab-app) .exam-review-sidebar {
  background: var(--surface); border: 1px solid var(--ink-10);
  border-radius: var(--radius); padding: 8px;
  display: flex; flex-direction: row; gap: 4px;
  overflow-x: auto;
}

:where(.vocab-app) .ex-sidebar-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-soft); padding: 6px 4px; font-weight: 700;
  flex-shrink: 0; align-self: center;
}

:where(.vocab-app) .ex-sidebar-item {
  display: flex; align-items: center; gap: 4px;
  background: var(--bg); border: 1px solid var(--ink-10);
  cursor: pointer; padding: 6px 9px; border-radius: 8px;
  font-family: inherit; font-size: 12px; color: var(--ink);
  flex-shrink: 0;
}

:where(.vocab-app) .ex-sidebar-item.active { background: var(--ink); color: var(--cream); border-color: var(--ink); }

:where(.vocab-app) .ex-si-num { font-variant-numeric: tabular-nums; font-weight: 700; }

:where(.vocab-app) .ex-si-preview { display: none; }

:where(.vocab-app) .ex-si-mark { font-weight: 700; font-size: 11px; }

:where(.vocab-app) .ex-sidebar-item.correct { border-color: var(--accent-sage); color: var(--accent-sage-ink); }

:where(.vocab-app) .ex-sidebar-item.wrong { border-color: var(--accent-coral); color: var(--accent-coral-ink); }

:where(.vocab-app) .ex-sidebar-item.skipped { color: var(--ink-50); }

:where(.vocab-app) .ex-sidebar-item.active.correct,
:where(.vocab-app) .ex-sidebar-item.active.wrong,
:where(.vocab-app) .ex-sidebar-item.active.skipped { color: var(--cream); }

:where(.vocab-app) .exam-review-main {
  background: var(--surface); border: 1px solid var(--ink-10);
  border-radius: var(--radius); padding: 18px;
}

/* V-48 — TWO PANES ON THE BIG SURFACE. The question list is NAVIGATION, not
   stretched content, so it is the one vocab surface that earns a second column
   (the readable-column rule still holds for the question itself: the second
   track is capped at `--content-max`, NOT `1fr`, because a stem and four choice
   rows stretched to the full wide screen are harder to scan, not easier. The
   sidebar is what earns the extra width; the rest stays margin, so the pair is
   centred.)
   review.jsx applies the same values inline at `isSplit` (>=1024 AND landscape)
   and inline wins, so this block is the mirror for the stylesheet's own
   breakpoint — the two MUST stay in step: edit `splitStyles()` there and this
   block together. */
html[data-surface="desktop"] :where(.vocab-app) .exam-review-grid {
  display: grid;
  grid-template-columns: minmax(190px, 270px) minmax(0, var(--content-max));
  justify-content: center;
  align-items: start;
  gap: 20px;
}

html[data-surface="desktop"] :where(.vocab-app) .exam-review-sidebar {
  flex-direction: column;
  overflow-x: visible; overflow-y: auto;
  max-height: 70vh;
}

html[data-surface="desktop"] :where(.vocab-app) .exam-review-main { min-width: 0; }

/* A column that wide showing only a number is the strip's phone shape wearing
   the desktop's clothes. review.jsx already computes (and truncates) a preview
   for every row; this is where it becomes visible. */
html[data-surface="desktop"] :where(.vocab-app) .ex-sidebar-item {
  width: 100%; justify-content: flex-start; text-align: left;
}

html[data-surface="desktop"] :where(.vocab-app) .ex-si-preview {
  display: block; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

html[data-surface="desktop"] :where(.vocab-app) .ex-sidebar-item .ex-si-mark { flex: none; }

:where(.vocab-app) .exam-options-review .exam-opt { cursor: default; }

:where(.vocab-app) .exam-options-review .exam-opt.is-correct { border-color: var(--accent-sage); background: var(--accent-sage-tint); }

:where(.vocab-app) .exam-options-review .exam-opt.is-wrong { border-color: var(--accent-coral); background: var(--accent-coral-tint); }

/* Revealed exam options stay disabled (no further interaction) but should
   stay fully visible — without this, the browser dims the green/red feedback
   to ~50% which makes it hard to read. */
:where(.vocab-app) .exam-options-review .exam-opt:disabled,
:where(.vocab-app) .exam-options-review .exam-opt[disabled] {
  opacity: 1; cursor: default;
}

/* Inline-feedback explanation card sits between the options and the action
   bar (instant mode). Same look as the end-of-test review, just placed
   in-flow so the answer/feedback/next sit together. */


:where(.vocab-app) .exam-explain {
  margin-top: 16px; padding: 14px; background: var(--bg);
  border-radius: 12px; border: 1px dashed var(--ink-10);
}

:where(.vocab-app) .ex-explain-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-soft); font-weight: 700; margin-bottom: 6px;
}

:where(.vocab-app) .exam-explain p { margin: 0 0 12px; font-size: 13px; line-height: 1.55; }

:where(.vocab-app) .ex-word-card { background: var(--bg-inset, var(--cream-2)); border: none; border-radius: var(--radius-inset); padding: 12px; }

:where(.vocab-app) .ex-word-head { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; flex-wrap: wrap; }

:where(.vocab-app) .ex-word-term { font-family: var(--font-display); font-size: 17px; font-weight: 700; }

:where(.vocab-app) .ex-word-pos { font-family: var(--font-display); font-style: italic; font-size: 12px; color: var(--ink-soft); }

:where(.vocab-app) .ex-word-def { font-size: 13px; line-height: 1.5; color: var(--ink-70); margin-bottom: 4px; }

/* ===========================================================================
   APPENDED — the "known" deck bucket (placement self-report).
   =========================================================================== */
/* The fourth chip state, alongside .word-chip-mastered (sage) and
   .word-chip-learning (marigold). Two channels, deliberately:
     fill    — lavender-tint, the remaining quiet warm tint, so the four states
               separate at a glance without any of them shouting.
     border  — DASHED, and the only status that wears one. A known word is what
               the student TOLD us during placement; nothing has been measured.
               An outline rather than a solid edge is that distinction, and it
               survives a colour-blind read of the fill, which sage/marigold/
               lavender alone would not.
   Label ink is inherited from .word-chip (var(--ink)) like the other two — do
   NOT add a per-status .wc-word colour (see the note above .word-chip-mastered). */
:where(.vocab-app) .word-chip-known {
  background: var(--accent-lavender-tint);
  border-color: var(--accent-lavender);
  border-style: dashed;
}

/* .word-chip:hover carries a pseudo-class and so outranks the rule above;
   restate the hover edge in the known family rather than letting it fall back
   to the generic ink. */
:where(.vocab-app) .word-chip-known:hover { border-color: var(--accent-lavender-ink); }

:where(.vocab-app) .ex-word-ex {
  font-family: var(--font-display); font-style: italic; font-size: 12px; color: var(--ink-70);
  border-left: 2px solid var(--ink-10); padding-left: 8px; line-height: 1.4;
}

:where(.vocab-app) .exam-review-nav {
  display: flex; justify-content: space-between; align-items: center;
  margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--ink-10);
}

:where(.vocab-app) .ex-review-count { font-size: 12px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

/* ===========================================================================
   APPENDED — word peek (deck chip → the word, not a 869-card run).
   =========================================================================== */
/* Sheet geometry is the practice feedback modal's (bottom-anchored, full-bleed,
   capped column), so it is already verified phone→desktop and needs no surface
   branch. z-index 200 is the shell's documented modal band (index.html: overlays
   55+, reading popover 120, modals 200). The scrim IS the outside-click target —
   deck.jsx guards on e.target === e.currentTarget so a tap inside never closes. */
:where(.vocab-app) .deck-peek-scrim {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(28, 22, 18, 0.42);
  display: flex; align-items: flex-end; justify-content: center;
  /* The native shell renders under the camera island (viewport-fit=cover), and
     this sheet is fixed to the VIEWPORT, not to the padded .shell — so the
     inset has to be held here too. */
  box-sizing: border-box; padding-top: env(safe-area-inset-top, 0px);
}

:where(.vocab-app) .deck-peek {
  width: 100%; max-width: 560px; box-sizing: border-box;
  background: var(--bg); color: var(--ink);
  border-top-left-radius: 20px; border-top-right-radius: 20px;
  padding: 18px 18px calc(18px + env(safe-area-inset-bottom, 0px));
  box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.18);
  /* dvh so a mobile URL bar can't push the actions under the fold; vh first for
     browsers that don't know it. The second term keeps the sheet's TOP edge
     clear of the camera island where the inset is non-zero (the native shell);
     88dvh stays the cap where it is 0, which is every browser. */
  max-height: min(88vh, calc(100vh - env(safe-area-inset-top, 0px) - 12px));
  max-height: min(88dvh, calc(100dvh - env(safe-area-inset-top, 0px) - 12px));
  overflow-y: auto;
}

:where(.vocab-app) .deck-peek-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
}

:where(.vocab-app) .deck-peek-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 6px;
}

/* Raised, because it is tappable and it is what the sheet opens focused on.
   40px keeps it near the tap floor without crowding a long headword. */
:where(.vocab-app) .deck-peek-close {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0;
  background: var(--surface); border: 1px solid var(--ink-10); border-radius: 999px;
  color: var(--ink); cursor: pointer;
}

:where(.vocab-app) .deck-peek-close:hover { border-color: var(--ink-30); }
/* V-05: the 40px circle stays (a 44px one crowds a long headword, per the note
   above); the hit area grows to 44 through an overlay that shifts no layout. */
:where(.vocab-app) .deck-peek-close { position: relative; }
:where(.vocab-app) .deck-peek-close::after { content: ''; position: absolute; inset: -2px; }

/* Read-only inset (no border, no shadow, no chevron): the status is a fact
   about the word, not something to tap. */
:where(.vocab-app) .deck-peek-status {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 12px; padding: 6px 10px;
  background: var(--bg-inset, var(--cream-2)); border-radius: var(--radius-inset);
  color: var(--ink-70); font-size: 13px; line-height: 1.4;
}

/* .speak-btn's own margin-top is tuned for the flashcard's centred stack. */
:where(.vocab-app) .deck-peek-speak { margin-top: 12px; }

/* .card-back-body is reused verbatim (flow column, gap 12); the sheet only adds
   the rule that separates the header block from the card content. */
:where(.vocab-app) .deck-peek .card-back-body {
  margin-top: 14px; border-top: 1px solid var(--ink-10);
}

:where(.vocab-app) .deck-peek-actions {
  display: flex; flex-direction: column; align-items: stretch; gap: 6px;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--ink-10);
}

:where(.vocab-app) .deck-peek-actions .btn { width: 100%; justify-content: center; }

/* A text link, not a second raised button — two equal-weight commitments on a
   lookup surface would rebuild the problem the peek exists to fix. */
:where(.vocab-app) .deck-peek-secondary {
  background: none; border: none; padding: 8px 0; cursor: pointer;
  font-family: inherit; font-size: 13px; font-weight: 700; color: var(--accent);
  /* V-05: measured ~32px. Real height (the actions column has the room) and
     centred like the .btn rows it follows. */
  display: flex; align-items: center; justify-content: center; min-height: 44px;
}

/* =====================================================================
   HOME: deck bar + practice guidance (2026-08-30)  — appended section,
   nothing above is modified.
   ===================================================================== */

/* The thin deck bar. A TAPPABLE row (it is the way in to Progress) that
   contains a READ-ONLY fill — so the row carries the raised affordance and the
   bar itself stays flat. The bar is ~6px: it is a fact at a glance, not a
   dashboard. Segment fills are passed inline from home.jsx, and they are the
   deck chips' own status tints (.word-chip-mastered / -learning / -known), so
   the bar cannot drift from the deck screen's colours. "To study" is not a
   segment — it is the empty remainder of the track, which is what a progress
   bar means. */
/* Fill/border/shadow come from window.__raised(t) inline in home.jsx — the one
   place the raised triple is defined; this rule owns only layout. */
:where(.vocab-app) .vhome-deckbar {
  width: 100%;
  margin-top: 12px;
  display: block;
  text-align: left;
  padding: 11px 14px 12px;
  font-family: inherit;
  cursor: pointer;
}
:where(.vocab-app) .vhome-deckbar:hover { filter: var(--hover-filter, none); }

:where(.vocab-app) .vhome-deckbar-line {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

:where(.vocab-app) .vhome-deckbar-track {
  display: flex;
  height: 6px;
  margin-top: 8px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--bg-elevated, #fff);
  box-shadow: inset 0 0 0 1px var(--ink-10, rgba(0,0,0,.08));
}

/* Every nonzero segment keeps a visible minimum width, and none of them may
   stretch: widths are the true percentage of the deck (flex-grow would refill
   the row and make a 3%-studied deck read as finished). */
:where(.vocab-app) .vhome-deckbar-seg { flex: 0 0 auto; min-width: 3px; }

:where(.vocab-app) .vhome-deckbar-streak {
  display: block;
  margin-top: 7px;
  font-size: 13px;
  color: var(--ink-soft);
}

/* One guidance line under a practice card — what it is, why, how long. Sits
   OUTSIDE the card (the card is a button; a caption is not part of the
   commitment) and reads as a caption, not a second subtitle. */
:where(.vocab-app) .vhome-guide {
  margin: 6px 2px 0;
  font-size: 13px;
  color: var(--ink-soft);
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   Vocabulary in context — passage surface (subjects/vocab/passages.jsx)
   Appended 2026-08-30. Self-contained: nothing above this line is touched.
   ========================================================================== */

/* A ghost control: the quietest thing on a card that is otherwise tappable.
   No fill, no border — hiding a passage is housekeeping, never a second offer
   competing with "open this one". */
:where(.vocab-app) .vp-ghost {
  background: none; border: none; cursor: pointer;
  font-family: inherit; font-size: 12px; font-weight: 600;
  padding: 12px 12px; align-self: center; white-space: nowrap;
}
:where(.vocab-app) .vp-ghost:hover { text-decoration: underline; }

/* The prose. A reading surface first — the marks below are the only deviation
   from plain body text.
   MEASURE CAP (VB-05). This is the one surface where the readable-column token
   and the readable MEASURE part company: at `--content-max` the passage ran
   84-88 characters a line on both iPad classes, against a comfortable 45-75,
   for an audience of 10-15. Measured after the cap: 59-71, averaging ~67.
   The cap is stated in `ch` so it tracks the type rather than a pixel guess
   (the font-size is pinned here for the same reason — `ch` resolves against
   THIS element's font, so leaving it to the paragraphs would make the unit
   mean something different from what it looks like it means), and it is
   centred inside the column so the passage keeps the page's gutters and simply
   stops growing. Around 545px it becomes a no-op, so the phone column is
   untouched.
   The gloss popover is an absolute child of `.vp-prose` and clamps to its box
   (passages.jsx VpGloss), so narrowing the prose narrows the clamp with it —
   which is the behaviour we want, not a regression. */
:where(.vocab-app) .vp-prose {
  font-size: 16px; max-width: 56ch; margin-inline: auto;
}
:where(.vocab-app) .vp-prose .vp-para {
  margin: 0 0 14px; font-size: 16px; line-height: 1.72; text-wrap: pretty;
}
:where(.vocab-app) .vp-prose .vp-para:last-child { margin-bottom: 0; }

/* Target-word mark: a dotted accent underline and NOTHING else. No fill (that
   channel is the student's own highlighter), no bold (bold turns reading into
   scanning, which is what reading-in-context exists to defeat). */
:where(.vocab-app) .vp-mark {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: inherit; line-height: inherit;
  text-decoration: underline dotted var(--accent, currentColor);
  text-underline-offset: 3px; text-decoration-thickness: 1.5px;
}
:where(.vocab-app) .vp-mark:hover { text-decoration-style: solid; }
/* On a question screen the quoted word is evidence, not a control. */
:where(.vocab-app) .vp-mark-static { cursor: default; }
:where(.vocab-app) .vp-mark-static:hover { text-decoration-style: dotted; }

/* The paragraph the current question came from, while the passage is expanded.
   Ink weight only — colour on this surface already means progress state.
   Scoped to the expanded panel only — on the reading screen there is no
   "current question", so nothing there may be dimmed. */
:where(.vocab-app) .vp-passage-scroll .vp-para-active { color: var(--ink); }
:where(.vocab-app) .vp-passage-scroll .vp-para:not(.vp-para-active) { color: var(--ink-70, inherit); }

/* The expandable passage on a question screen: capped at ~40vh with its own
   scroll, so opening it never pushes the question off the screen. `relative`
   is load-bearing — the auto-scroll measures paragraph offsets against it. */
:where(.vocab-app) .vp-passage-scroll {
  position: relative; max-height: 40vh; overflow-y: auto;
  margin-top: 10px; padding: 12px 14px;
  -webkit-overflow-scrolling: touch;
}
:where(.vocab-app) .vp-passage-scroll .vp-para { font-size: 14.5px; line-height: 1.65; }

/* Every target word on the intro screen, as chips. Read-only reference, so no
   raise and no chevron — they are what the sitting is about, not links. */
:where(.vocab-app) .vp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
:where(.vocab-app) .vp-chip {
  display: inline-block; padding: 4px 9px; border-radius: 999px;
  background: var(--bg, var(--cream-1, #fff)); color: var(--ink);
  font-size: 13px; line-height: 1.35;
}

/* Passage summary — the confidence pane's chips. Same pill as .vp-chip, but
   these ARE controls (tap to claim the word), so they get a cursor, a pressed
   state driven by inline theme colors, and a hover lift-by-filter. */
:where(.vocab-app) .vp-chip-pick {
  cursor: pointer;
  font-family: inherit;
  font-weight: 600;
  /* V-05: .vp-chip's 4px padding left these at ~26px; these ones are tapped. */
  display: inline-flex; align-items: center; min-height: 44px;
  transition: filter var(--dur-quick, 120ms) var(--ease-settle, ease), background var(--dur-quick, 120ms) var(--ease-settle, ease);
}
:where(.vocab-app) .vp-chip-pick:hover { filter: var(--hover-filter, brightness(0.97)); }

/* STACKED variant — the workout's closing "Which of these do you own now?"
   grid, whose chip is three block lines (headword / definition / miss count)
   rather than a one-line pill.
   The 44pt fix above made every .vp-chip-pick an `inline-flex` ROW, so inside
   a `repeat(2, minmax(0,1fr))` grid those three spans became three columns —
   about a third of a half-width cell for the headword, which is enough to fire
   `hyphens: auto` and print *acri-mo-nious* in a vocabulary app (VI-04). The
   cell is a COLUMN; `min-width: 0` keeps it shrinkable inside the grid track,
   `align-items: stretch` keeps the lines full width, and the headword's own
   hyphenation is turned off inline (workout.jsx) so a word can only ever break
   at a real syllable-free last resort. */
:where(.vocab-app) .vp-chip-pick--stack {
  display: flex; flex-direction: column; align-items: stretch;
  justify-content: flex-start; min-width: 0;
}

/* Tap-gloss popover. Anchored at the tapped word inside `.vp-prose` (which is
   `position: relative` for exactly this), so it stays with the text while the
   expanded passage box scrolls. It floats, so it earns a shadow — the fill is
   still inset, and opaque so prose never reads through it. */
:where(.vocab-app) .vp-prose { position: relative; }
/* V-70 — the gloss is a transient card floating OVER the prose, so it wears
   the raised treatment at full strength: bgCard fill (set inline), an
   accent-family hairline, a shadow clearly deeper than a list row's, and
   radiusCard. The old inset fill + 0.13 shadow read as part of the paper and
   had to be hunted for. Width is set inline per surface, so it grows with the
   column instead of shrinking to a footnote on an iPad. */
:where(.vocab-app) .vp-gloss {
  position: absolute; z-index: 6; max-width: 360px;
  padding: 12px 14px; box-sizing: border-box;
  border: 1px solid var(--accent-300, rgba(42, 38, 32, 0.24));
  box-shadow: 0 12px 30px rgba(42, 38, 32, 0.22), 0 2px 6px rgba(42, 38, 32, 0.12);
  animation: vp-gloss-in var(--dur-quick, 120ms) var(--ease-settle, ease);
}
/* Opacity only — the placement above a word is a `transform` set inline, and
   an animated transform would fight it. Reduced motion collapses --dur-quick
   to 0ms, which makes this an instant swap with no code branch. */
@keyframes vp-gloss-in { from { opacity: 0; } to { opacity: 1; } }
:where(.vocab-app) .vp-gloss-head { font-weight: 600; letter-spacing: -0.01em; }
/* Caret: a rotated corner of the same fill, clipped by the box's own edge. Its
   two outward edges carry the same hairline so the border reads unbroken. */
:where(.vocab-app) .vp-gloss-caret {
  position: absolute; width: 10px; height: 10px; transform: rotate(45deg);
  bottom: -6px; margin-left: -5px;
  border-right: 1px solid var(--accent-300, rgba(42, 38, 32, 0.24));
  border-bottom: 1px solid var(--accent-300, rgba(42, 38, 32, 0.24));
}
:where(.vocab-app) .vp-gloss-below .vp-gloss-caret {
  bottom: auto; top: -6px;
  border-right: none; border-bottom: none;
  border-left: 1px solid var(--accent-300, rgba(42, 38, 32, 0.24));
  border-top: 1px solid var(--accent-300, rgba(42, 38, 32, 0.24));
}

/* Word Builder (roots.jsx `build` beat) — the wrong-answer nudge. The answer
   row shakes rather than flashing a colour, because the row is read-only
   ground: it has no fill of its own to change without reading as tappable. */
@keyframes wp-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
}
.wp-answer { transition: background var(--dur-quick, 120ms) var(--ease-settle, ease); }
.wp-shake { animation: wp-shake var(--dur-settle) var(--ease-settle); }
@media (prefers-reduced-motion: reduce) {
  .wp-shake { animation: none; }
}

/* ===========================================================================
   APPENDED — the Word parts deck drawer (home.jsx §6b).
   =========================================================================== */
/* A dense wrapping row rather than the deck's auto-fill grid: a part is 2-8
   characters ("bi-", "-ology"), so fixed 165px tracks would be mostly air. */
:where(.vocab-app) .part-chip-row {
  display: flex; flex-wrap: wrap; gap: 6px;
}
/* Reuses .word-chip (and its status fills) for the colour language; this only
   strips the grid-cell geometry and shrinks the box to the part it holds,
   while keeping the row tall enough to hit (min-height 34px). */
:where(.vocab-app) .part-chip {
  padding: 7px 11px; min-height: 44px; justify-content: center;
  font-size: 13px; font-weight: 600;
}
:where(.vocab-app) .part-chip .wc-word { white-space: nowrap; }
/* The open chip keeps its status fill and gains an edge — it is the one thing
   in the row the detail card below belongs to. */
:where(.vocab-app) .part-chip-open { border-color: var(--ink-30); }

/* Read-only detail card: inset fill, no border, no shadow (fill + radius come
   from the theme tokens inline). */
:where(.vocab-app) .part-detail { margin-top: 8px; padding: 12px 13px 13px; }
:where(.vocab-app) .part-detail-members {
  display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px;
}
/* Member words are TEXT, not controls — they sit on the page's own paper
   inside the inset card so they read as a list, and carry no affordance. */
:where(.vocab-app) .part-member {
  background: var(--bg); color: var(--ink); border-radius: 999px;
  padding: 3px 9px; font-size: 12px; font-weight: 600;
}

/* Passage intro — the "I know most of these words" confirm pane. Read-only
   surround (it holds the claim, it is not itself tappable); the chips inside
   carry the tappability. */
:where(.vocab-app) .vp-know-pane {
  background: var(--bg-inset);
  border-radius: var(--radius-inset, 4px);
  padding: 14px;
}

/* Placement finale — the pane holding the top slice's chips. Read-only
   surround (it holds the claims, it is not itself tappable); the chips carry
   the tappability. Capped and internally scrolled because the slice above the
   staircase's top edge can run to ~160 words on a full deck, and the Finish
   button lives OUTSIDE this box so it is always reachable. */
:where(.vocab-app) .wk-finale-pane {
  background: var(--bg-inset);
  border-radius: var(--radius-inset, 4px);
  padding: 12px;
  max-height: 55vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* =====================================================================
   DECK PEEK: withdraw the known claim (2026-08-30) — appended, nothing
   above is modified.
   ===================================================================== */

/* Quieter than .deck-peek-secondary on purpose: this is a correction, not a
   destination. Muted ink, normal weight, no accent — it must be findable
   without competing with the two study actions above it. */
:where(.vocab-app) .deck-peek-unknow {
  background: none; border: none; padding: 6px 0; cursor: pointer;
  font-family: inherit; font-size: 13px; font-weight: 600;
  color: var(--ink-soft); text-align: left;
}
:where(.vocab-app) .deck-peek-unknow:hover { color: var(--ink); }

/* ---------------------------------------------------------------------------
   Passage narration (read phase)
   --------------------------------------------------------------------------- */
/* Karaoke highlight. A SOFT FILL AND NOTHING ELSE — no colour on the ink, no
   weight change, no box. It has to coexist with the dotted target-word mark
   sitting on the very same word, so the two use different channels: the mark
   owns the underline, the highlight owns the ground. The tint is the accent at
   low alpha (accent-100 where the ramp exists), which stays clear of the
   semantic hues that mean right/wrong everywhere else.
   The span is inline and unpadded in the block direction, so lighting a word
   can never reflow the line. */
:where(.vocab-app) .vp-w { border-radius: 3px; white-space: nowrap; }
:where(.vocab-app) .vp-w-on {
  background: var(--accent-soft, rgba(0, 0, 0, 0.07));
  box-shadow: 0 0 0 2px var(--accent-soft, rgba(0, 0, 0, 0.07));
  transition: background var(--dur-quick, 120ms) var(--ease-settle, ease);
}
/* Paragraph-level fallback: the pack and the prose disagreed, so we claim only
   what we know — this paragraph is the one being read. */
:where(.vocab-app) .vp-para-narr {
  background: var(--accent-soft, rgba(0, 0, 0, 0.05));
  border-radius: var(--radius-inset, 4px);
  box-shadow: 0 0 0 4px var(--accent-soft, rgba(0, 0, 0, 0.05));
}

/* The bar. Sticky at the foot of the readable column, full-bleed to the
   screen's padding like the questions phase's Next. Read-only ground with
   RAISED controls on it: the bar is chrome, the buttons are the tappable
   things. */
:where(.vocab-app) .vp-narr {
  position: sticky; bottom: 0; z-index: 4;
  margin: 16px -20px 0;
  padding: 8px 20px calc(8px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; justify-content: center; gap: 8px;
}
:where(.vocab-app) .vp-narr-btn {
  display: inline-flex; align-items: center; justify-content: center;
  /* V-05: 44px tap floor. */
  width: 44px; height: 44px; padding: 0; cursor: pointer;
  border-radius: var(--radius-btn, 14px);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  transition: filter var(--dur-quick, 120ms) var(--ease-settle, ease);
}
:where(.vocab-app) .vp-narr-btn-main { width: 48px; height: 48px; }
:where(.vocab-app) .vp-narr-btn:hover,
:where(.vocab-app) .vp-narr-rate:hover { filter: var(--hover-filter, brightness(0.97)); }
/* Speed is a direct SELECTION (owner, 2026-09): the button's label is the
   current rate, and a tap opens the four values above it. Cycling meant three
   taps to walk back from 1.5x to 0.8x. */
:where(.vocab-app) .vp-narr-rate {
  height: 44px; min-width: 46px; padding: 0 10px; cursor: pointer;
  border-radius: var(--radius-btn, 14px);
  font-family: inherit; font-size: 12.5px; font-weight: 700;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  transition: filter var(--dur-quick, 120ms) var(--ease-settle, ease);
}

/* Speed menu. Anchored inside the sticky bar's own positioning context, so it
   rides with the bar instead of drifting; it floats above the prose, so it
   earns a shadow and an opaque fill. Items are raised ground's own children —
   plain rows, no fill of their own, the current one marked in accent ink plus
   a check so the mark survives a glance. 44px tall for the thumb. */
:where(.vocab-app) .vp-narr-ratewrap { position: relative; display: inline-flex; }
:where(.vocab-app) .vp-narr-ratemenu {
  position: absolute; z-index: 7; bottom: calc(100% + 8px); right: 0;
  min-width: 92px; padding: 4px; box-sizing: border-box;
  display: flex; flex-direction: column;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.13);
}
:where(.vocab-app) .vp-narr-rateitem {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  min-height: 44px; padding: 0 8px 0 10px; cursor: pointer;
  background: none; border: none; border-radius: var(--radius-btn, 14px);
  font-family: inherit; font-size: 13px; font-variant-numeric: tabular-nums;
  transition: filter var(--dur-quick, 120ms) var(--ease-settle, ease);
}
:where(.vocab-app) .vp-narr-rateitem:hover { filter: var(--hover-filter, brightness(0.97)); }

/* Narration auto-scroll: the lit word (and the gloss popover the narration's
   stop opens under it) must clear the sticky .vp-narr bar. The JS band handles
   the moving highlight; this covers any native scrollIntoView path — a tapped
   mark, focus — so a popover is never parked under the bar. */
:where(.vocab-app) .vp-prose .vp-w,
:where(.vocab-app) .vp-prose .vp-mark {
  scroll-margin-top: 96px;
  scroll-margin-bottom: 148px;
}

/* NATIVE SHELL (V-63). In the Capacitor WebView a long-press on the quiz word
   or a choice row starts an iOS text selection and pops the Copy / Look Up /
   Translate callout over the answers — a browser affordance that reads as a
   bug inside a native app, and one that can hide the very choices it covers.
   Answer chrome is a control, not copy, so it opts out of selection. Passage
   prose is deliberately NOT covered: looking a word up there is the point. */
:where(.vocab-app) .quiz-prompt-card,
:where(.vocab-app) .quiz-options {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* ===========================================================================
   V-65 — reader back bar (see VpStickyBack, passages.jsx).
   The SLOT is sticky and zero-height, so the bar it holds costs no flow height
   and nothing shifts when it appears mid-scroll. It sits under the reading
   popover (120) and the modal band (200) and over the narration bar (4).
   =========================================================================== */
/* FIXED, not sticky — sticky top:0 never pins in the iOS WebView (see
   VpStickyBack, passages.jsx). Out of the flow, so the bar costs no layout.
   The strip spans the screen; the bar inside keeps the readable column. */
:where(.vocab-app) .vp-backslot {
  position: fixed; top: 0; left: 0; right: 0; z-index: 6;
  pointer-events: none;
}

/* At desktop the persistent left rail (.sidenav) owns the leading edge of the
   shell, and `.shell-body` — the column this bar belongs to — starts after it.
   A viewport-wide fixed strip centres the bar half a rail too far left, over
   the sidebar wordmark. Start the strip where the content plane starts so the
   bar lands on the same column as the prose. Tablet/phone have no rail, so
   the base rule is already the content plane there. */
html[data-surface="desktop"] :where(.vocab-app) .vp-backslot {
  left: var(--sidenav-w);
}


:where(.vocab-app) .vp-back {
  display: none;
  align-items: center; gap: 10px;
  box-sizing: content-box;
  max-width: var(--content-max, 560px); margin: 0 auto;
  min-height: 44px; padding: env(safe-area-inset-top, 0px) 12px 0 6px;
  pointer-events: auto;
}

:where(.vocab-app) .vp-back.is-on { display: flex; }

/* 44px hit box on a text link (V-05); the padding is invisible on it. */
:where(.vocab-app) .vp-back-btn {
  flex: none; background: none; border: none; cursor: pointer;
  padding: 12px 10px; min-height: 44px;
  font-family: inherit; font-size: 14px; font-weight: 700; letter-spacing: -0.01em;
}

:where(.vocab-app) .vp-back-title {
  min-width: 0; flex: 1 1 auto; text-align: right;
  font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ===========================================================================
   V-66 — WordPeek swipe-to-dismiss.
   A grab handle above the head, plus the head itself as a drag surface. Only
   the drag surfaces take `touch-action: none`; the sheet body keeps its own
   scrolling, so a pan over the definition still reads the definition.
   =========================================================================== */
:where(.vocab-app) .deck-peek-grab {
  touch-action: none;
  margin: -6px 0 2px;
  padding: 6px 0 2px;
  display: flex; justify-content: center;
  cursor: grab;
}

:where(.vocab-app) .deck-peek-grabline {
  width: 40px; height: 4px; border-radius: 999px;
  background: var(--ink-10, rgba(0, 0, 0, 0.12));
}

/* The head is a drag surface too (an iOS sheet drags from its title), except
   for the close button, which keeps its own tap. */
:where(.vocab-app) .deck-peek-head { touch-action: none; }
:where(.vocab-app) .deck-peek-close { touch-action: manipulation; }

/* While the finger is down the sheet follows it with no transition; on release
   it settles back on the house tokens (0ms under reduced motion). */
:where(.vocab-app) .deck-peek.is-dragging { transition: none; }
:where(.vocab-app) .deck-peek.is-settling {
  transition: transform var(--dur-settle, 220ms) var(--ease-settle, ease);
}

/* ================= ROOT DRILL PANE (VB-09 / VB-10) ================= */
/* The word-part drill's body takes the same measure its choice list already
   takes — `--content-measure`, the app-wide "one column for one question"
   token (app-surface.css). Without it the pane drew in three widths at
   tablet/desktop: choices at the measure, the breakdown panel and the Next
   button at the full content column, and the per-root counter right-aligned to
   the widest of them, i.e. floating far right of the content it counts.
   Capped, never centred — the left edge stays the column's, so the header, the
   eyebrow and the pane all start at one x (the same reason .quiz-body above is
   capped without `margin-inline: auto`). No rule at phone: the column is
   already narrower than the measure there. */
html[data-surface="tablet"] :where(.vocab-app) .vroot-body,
html[data-surface="desktop"] :where(.vocab-app) .vroot-body { max-width: var(--content-measure); }
