/* ===========================================================================
 * app-surface.css — responsive shell: phone → iPad → iPad landscape → desktop
 * ===========================================================================
 * Loaded by BOTH the dev shell and the production build (it is a real file, so
 * there is no dev/prod copy to drift). Everything here keys off the
 * `data-surface` attribute that surface.js stamps on <html> — never off a raw
 * media query — so the dev device-frame preview and a real iPad resolve the
 * same rules from the same measurement.
 *
 *   phone    < 700   the shell as it always was: full-bleed, two-tier topbar
 *   tablet   700+    same chrome, wider column, roomier gutters. EVERY iPad in
 *                    portrait lands here, the 13" (1024) included — see the
 *                    VB-08 note in surface.js for why the boundary is 1100.
 *   desktop  1100+   the topbar is REPLACED by a persistent left sidebar
 *
 * The layout box the app actually lives in is:
 *   .shell            scroll container (flex row at desktop)
 *     .sidenav        desktop only — sticky nav rail
 *     .shell-body     topbar + main.stage
 * ========================================================================= */

:root {
  /* LIGHT-PAPER ONLY (CLAUDE.md: "cream backdrop everywhere, never dark").
   * Declared so the UA knows it: under an OS dark preference an undeclared
   * `color-scheme: normal` leaves scrollbars, form-control internals and
   * autofill grounds painted dark against cream. This is the whole of the
   * dark-mode story and is not a dark theme. */
  color-scheme: light;

  --content-max: 560px;
  --content-wide: 560px;
  --gutter: 16px;
  --sidenav-w: 248px;
  /* THE READABLE MEASURE for one question's content — stem, choices and the
   * explanation all cap here at tablet/desktop so a question draws in ONE
   * column instead of three (study P2.8). 622px is what 62ch of the UI face
   * resolved to, i.e. the width the stem already had. */
  --content-measure: 622px;

  /* MOTION TOKENS. The app is a quiet-paper surface: motion is a rare accent,
   * never ambience. Two durations and one curve is the whole vocabulary —
   * `quick` for a state that has already happened (a mark landing), `settle`
   * for a value growing into place (a progress width). --ease-settle is a
   * decelerating curve: it leaves fast and arrives slowly, so the eye reads
   * the END state rather than the travel. Spend these only where a value
   * genuinely CHANGED under the student; never on hover, entry or layout.
   * Under prefers-reduced-motion both durations collapse to 0ms, which turns
   * every transition using them into an instant swap with no code branch. */
  --ease-settle: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur-quick: 120ms;
  --dur-settle: 220ms;
}

@media (prefers-reduced-motion: reduce) {
  :root { --dur-quick: 0ms; --dur-settle: 0ms; }
}
html[data-surface="tablet"] { --content-max: 680px; --content-wide: 820px; --gutter: 24px; }
html[data-surface="desktop"] { --content-max: 760px; --content-wide: 1120px; --gutter: 32px; }

/* A STANDALONE product's rail is not the prep app's. It carries no test group
 * and no Study group — a wordmark, Reading comfort, Settings, and the
 * Today/Deck block when it has something to say — so it can hand width back to
 * the content plane, which on the math tutor is the tightest column in the app
 * (the stepped problem splits what is left into a stem ledger and a step pane).
 * Measured at 1180x820 with the worst-case stats block forced on, in both
 * standalone products: the wordmark is the widest thing in the rail and holds
 * its current TWO lines down to 224px ("ISEE/SSAT Vocabulary Builder" breaks to
 * three below that); the 3-figure Deck row stops fitting under 220px; no
 * .sidenav-item wraps down to 200px. 232px keeps every one of those with a
 * cushion. The prep app is untouched at 248 — its rail also lists the tests and
 * the Study destinations.
 */
html.standalone-app[data-surface="desktop"] { --sidenav-w: 232px; }

/* ---------- Shell skeleton ------------------------------------------------
 * `.shell-body` exists so the sidebar can be a sibling of the topbar+main
 * stack rather than a peer of the topbar. It is layout-neutral below desktop.
 */
.shell-body { display: block; min-width: 0; }
/* viewport-fit=cover extends the layout viewport under the sensor housing, so
   in landscape the leading edge of the readable column sits beneath the notch
   (47pt on a notched iPhone against a 16px gutter). Inset the whole content
   stack once, here: the topbar, main.stage and every sticky bar inside it
   inherit it, and the cream fill still paints edge to edge (padding is inside
   the background box). Browsers with no insets resolve these to 0. */
.shell-body {
  box-sizing: border-box;
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

html[data-surface="desktop"] .shell {
  display: flex;
  align-items: flex-start;
}
html[data-surface="desktop"] .shell-body {
  flex: 1;
  min-width: 0;
  /* The sidebar carries the app's identity now, so the content plane gets the
     cream page tone edge-to-edge. */
  background: var(--bg);
  min-height: 100dvh;
}
/* Row 1/2 of the topbar ARE the sidebar's content at desktop — showing both
   would be two navigations for the same destinations. */
html[data-surface="desktop"] header.topbar { display: none !important; }

/* ---------- Sidebar (desktop only) --------------------------------------- */
.sidenav { display: none; }
html[data-surface="desktop"] .sidenav {
  display: flex;
  flex-direction: column;
  flex: 0 0 var(--sidenav-w);
  width: var(--sidenav-w);
  box-sizing: border-box;
  align-self: flex-start;
  position: sticky;
  /* The rail pins against the PAGE scroller: `.shell` in dev, the window in the
     production/native bundle (see index.html / build.js). At rest it sits below
     `.shell`'s `padding-top: env(safe-area-inset-top)`; once the page scrolls,
     sticky pins it at `top`, and `top: 0` is the window edge — UNDER the status
     bar, behind the fixed `body::before` strip, so the wordmark slid beneath the
     clock on iPad (2026-09-12, native en-vocab). Pin at the inset instead, and
     take the same inset off the height so the pinned rail still ends at the
     viewport bottom (a 100dvh rail pinned below the inset would overhang, and
     the containing block would shove it back up at the end of the page).
     Browsers resolve env() to 0, so this is the old rule everywhere else. */
  top: env(safe-area-inset-top, 0px);
  height: calc(100dvh - env(safe-area-inset-top, 0px));
  /* When the shell is given a definite height — the dev device frame, and any
     packaged native shell that insets the web view — 100dvh is the WINDOW, not
     the box we actually live in, and the pinned footer (Settings) falls off the
     bottom. In production .shell is auto-height, where this resolves to none. */
  max-height: 100%;
  overflow-y: auto;
  /* Vertical padding lives on the two PINNED ends (.sidenav-brand /
     .sidenav-foot below): `position: sticky` pins to this scrollport's
     PADDING box, so a padding-top here would be a gap the middle section
     scrolls through, above the pinned wordmark. */
  padding: 0 12px;
  background: var(--bg-deep);
  border-right: 1px solid var(--line);
  gap: 2px;
}
/* Hidden while the first-run scrim owns the screen. */
html.fresh-user .sidenav { display: none !important; }

/* SP-14 — THE RAIL'S GROUND RUNS TO THE TOP OF THE WINDOW.
   index.html's `body::before` paints a fixed strip over `env(safe-area-inset-top)`
   so content passes under a legible status bar — but it paints the PAGE ground
   (`--bg`) across the FULL width. On a native iPad in portrait that left a
   32pt band of page cream above the rail (sampled: #F4F0E3 to y=31.5pt, then
   the rail's #ECE5D0), so the rail read as a panel starting below a stripe
   rather than as the edge of the window. Repaint the rail's own `--bg-deep`
   over the rail's width, in front of that strip, and carry its hairline up with
   it. `env()` resolves to 0 in browsers, so this strip has no height there and
   the web is unchanged; landscape moves the inset to the leading edge, where it
   is likewise 0 tall. Not painted while the rail is hidden (first-run scrim).
   Keep the z-index one above `body::before`'s 52 and below the overlays. */
html[data-surface="desktop"] body::after {
  content: '';
  position: fixed;
  top: 0; left: 0;
  width: var(--sidenav-w);
  height: env(safe-area-inset-top, 0px);
  box-sizing: border-box;
  background: var(--bg-deep);
  border-right: 1px solid var(--line);
  z-index: 53;
  pointer-events: none;
}
html.fresh-user body::after { display: none; }

/* VL-01 + VL-06 — the rail's two fixed ends stay put while the middle scrolls.
   Once a student has answered anything the rail grows TODAY/DECK blocks and runs
   past a landscape viewport (measured 1169 in 1024 at 1366×1024): `Aa · Reading
   comfort` and `Settings` left the screen with no other door, and reaching them
   by scrolling took the wordmark and the first destinations with them. The rail
   was always its own scroller (`overflow-y: auto` above) — what was missing is
   that the brand and the footer are CHROME, not list. Pinning both is scroll-
   model agnostic (dev `.shell` scroll vs the production document scroll: neither
   is the rail's scrollport) and independent of the safe-area top inset, which
   sits on the rail box itself. */
html[data-surface="desktop"] .sidenav-brand {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bg-deep);
  padding-top: 20px;
}
/* Says "there is more under here" without a hard rule across the rail. */
html[data-surface="desktop"] .sidenav-brand::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: 10px;
  background: linear-gradient(var(--bg-deep), rgba(0, 0, 0, 0));
  pointer-events: none;
}
html[data-surface="desktop"] .sidenav-foot {
  position: sticky;
  bottom: 0;
  z-index: 2;
  /* Short rail (no stats yet, and the standalone products render no
     .sidenav-spacer): the footer still belongs at the bottom. */
  margin-top: auto;
  background: var(--bg-deep);
  padding-bottom: calc(14px + env(safe-area-inset-bottom));
}
html[data-surface="desktop"] .sidenav-foot::before {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 100%;
  height: 10px;
  background: linear-gradient(rgba(0, 0, 0, 0), var(--bg-deep));
  pointer-events: none;
}

.sidenav-brand {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px 10px 16px;
}
.sidenav-brand-name {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 19px;
  letter-spacing: -0.015em;
  color: var(--ink);
  line-height: 1.15;
}
.sidenav-brand-sub {
  font-size: 11px;
  color: var(--ink-soft);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 600;
}

.sidenav-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 12px;
}
.sidenav-group[hidden] { display: none; }
/* --ink-faint is a hairline tone for dividers, not text: at 10px uppercase on
   the rail it measured under 3:1. --ink-soft is the readable quiet ink (4.7:1). */
.sidenav-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 6px 10px 4px;
}

/* Nav items follow the app's surface language: a selected row is the RAISED,
   tappable card tone against the deeper rail; the rest are quiet text rows. */
.sidenav-item {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 10px;
  cursor: pointer;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-soft);
  text-align: left;
  padding: 9px 10px;
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  box-sizing: border-box;
  line-height: 1.25;
  transition: background var(--dur-quick) ease, color var(--dur-quick) ease;
}
.sidenav-item:hover:not(:disabled) { background: rgba(42, 38, 32, 0.05); color: var(--ink); }
.sidenav-item[aria-selected="true"] {
  background: var(--card);
  border-color: var(--line);
  color: var(--ink);
  box-shadow: 0 1px 2px rgba(42, 38, 32, 0.05);
}
.sidenav-item:disabled { opacity: 0.45; cursor: default; }
.sidenav-item-sub {
  display: block;
  font-size: 11px;
  font-weight: 500;
  color: var(--ink-soft);   /* same reason as .sidenav-label */
  margin-top: 2px;
}
.sidenav-item-text { min-width: 0; }
.sidenav-spacer { flex: 1 1 auto; min-height: 12px; }
.sidenav-foot {
  display: flex;
  flex-direction: column;
  gap: 2px;
  border-top: 1px solid var(--line);
  padding-top: 8px;
}
.sidenav-item svg { flex: 0 0 auto; opacity: 0.75; }
.sidenav-streak {
  font-size: 11px;
  color: var(--ink-soft);
  padding: 4px 10px 6px;
  font-weight: 600;
}
.sidenav-streak[hidden] { display: none; }

/* Today block — the rail's one piece of state (questions answered today + the
   day streak, app-wide). Read-only by construction, so it wears the inset
   treatment: darker fill, radius 4, no border, no shadow, nothing to tap.
   Desktop-only for free — the whole rail is. Hidden at 0/0 by the renderer:
   a rail of zeros is worse than cream. */
.sidenav-stats {
  display: flex;
  flex-direction: column;
  gap: 7px;
  margin: 2px 2px 12px;
  padding: 10px 11px 11px;
  background: var(--bg-inset);
  border-radius: var(--radius-inset, 4px);
}
.sidenav-stats[hidden] { display: none; }
.sidenav-stats-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.sidenav-stats-row { display: flex; gap: 16px; }
.sidenav-stat { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.sidenav-stat-n {
  font-family: var(--font-head);
  font-size: 20px;
  font-weight: 700;
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.sidenav-stat-l {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--ink-soft);
  line-height: 1.2;
}

/* ---------- Main stage ---------------------------------------------------
 * The phone shell pins main.stage to the 874px device height. On a real
 * tablet/desktop viewport that number means nothing.
 *
 * NO VIEWPORT UNITS INSIDE main.stage. The stage carries the text-size
 * control's `zoom` (default textScale 1.1), and a `dvh` resolves in layout px
 * and is THEN scaled — so `100dvh` here paints 1.1× the window and leaves a
 * phantom scrollbar of exactly 0.10×viewport on every desktop screen. The
 * viewport fill belongs one box further out, on `.shell-body` (above), which
 * sits OUTSIDE the zoom and is therefore honest. Desktop is fixed; the tablet
 * rule below still carries the leak deliberately — its topbar math (the -96px)
 * hasn't been audited and is left for its own pass.
 */
html[data-surface="tablet"] main.stage,
html[data-surface="desktop"] main.stage {
  min-height: calc(100dvh - 96px);
}
html[data-surface="desktop"] main.stage {
  min-height: 0;
  padding-top: 22px;
}
html[data-surface="tablet"] .subject-root,
html[data-surface="desktop"] .subject-root { min-height: 0; }

/* ---------- Mode screens --------------------------------------------------
 * `.mode-screen` is the shared wrapper for every full-screen sub-surface
 * (skill drills, past papers, the Editing Lab, the vocab modes). One rule
 * centres all of them in the readable column instead of letting a phone
 * layout stretch to 1400px. A screen that genuinely wants the whole plane —
 * the two-pane runner, a year test — adds `.mode-screen--wide`.
 */
html[data-surface="tablet"] .mode-screen,
html[data-surface="desktop"] .mode-screen {
  max-width: var(--content-max);
  margin: 0 auto;
  width: 100%;
  box-sizing: border-box;
}
html[data-surface="tablet"] .mode-screen--wide,
html[data-surface="desktop"] .mode-screen--wide { max-width: var(--content-wide); }

/* ---------- Mode header --------------------------------------------------
 * PIN OR SCROLL — THE ONE RULE (SI-31, and the same call the Vocab study
 * deferred as VI-20; decided once, here, for both).
 *
 *   RUNNER AND DRILL PAGES PIN THEIR MODE HEADER.
 *   LONG READ-ONLY PAGES SCROLL IT.
 *
 * A drill page is any surface where the student is ANSWERING — the practice
 * runner, the Editing Lab stepper, the vocab drills. There the header is not
 * decoration: it carries the only exit, the round name and the position, and
 * a long reveal scrolls all three away exactly when they become useful.
 * Pinning costs a strip of the fold and buys back the controls.
 *
 * A read-only page is one the student is READING or BROWSING — Settings, the
 * progress report, the Lab's rule list, the topic browser, a plan, a summary.
 * There the header is chrome over content nobody is answering, and pinning it
 * only spends the fold. It scrolls; getting back out is one flick.
 *
 * MECHANISM: `sticky` on window.PracticeHeader / ModeHeader — it wraps the
 * bar, the progress track and the eyebrow in `.mode-headwrap--sticky` so the
 * three pin as ONE block (the wrapper paints, and lives in shared-chrome.css).
 * Every header also stamps `data-header="pinned|scroll"`, so which side of the
 * rule a screen chose can be read straight off the DOM. Nothing styles off the
 * attribute — do not start; the wrapper class is the pin. A screen whose own
 * ancestor pins the header (the practice runner's `.runner-rail`) passes
 * `sticky="outer"`: no wrapper, still reported as pinned.
 *
 * The shared back/progress/eyebrow header (window.ModeHeader) is a full-bleed
 * strip. Once the content below it is a centred column, a Back button pinned
 * to the far left of a 1400px window reads as belonging to something else —
 * so the header takes the same column and the same gutter as its content.
 * `.is-wide` (the split runner) opts into the wider budget.
 */
html[data-surface="tablet"] .mode-header,
html[data-surface="tablet"] .mode-header-eyebrow,
html[data-surface="tablet"] .progress-bar-track,
html[data-surface="desktop"] .mode-header,
html[data-surface="desktop"] .mode-header-eyebrow,
html[data-surface="desktop"] .progress-bar-track {
  max-width: var(--content-max);
  margin-left: auto;
  margin-right: auto;
  width: 100%;
  box-sizing: border-box;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}
/* Tablet too (VB-02): `.mode-screen--wide` widens the body at BOTH surfaces, so
   a header that only follows at desktop still orphans itself on an 11" iPad. */
html[data-surface="tablet"] .is-wide .mode-header,
html[data-surface="tablet"] .is-wide .mode-header-eyebrow,
html[data-surface="tablet"] .is-wide .progress-bar-track,
html[data-surface="desktop"] .is-wide .mode-header,
html[data-surface="desktop"] .is-wide .mode-header-eyebrow,
html[data-surface="desktop"] .is-wide .progress-bar-track { max-width: var(--content-wide); }

/* The in-bar eyebrow variant (shared-chrome.css .mode-header-eyebrow--inline)
 * ellipsises, which silently ate the topic name once the question header had a
 * second occupant. Nothing below it depends on the bar being exactly one line
 * tall, so let a long activity name wrap instead of vanishing. `html` prefix
 * only to out-specify shared-chrome.css, which loads after this file. */
html .mode-header-eyebrow--inline {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.25;
}

/* ---------- Settings ------------------------------------------------------
 * A settings form at 1200px wide is unreadable. Centre it in the content
 * column like every other surface, and let the body use the two-column
 * budget so long option lists don't become a mile of scrolling.
 */
html[data-surface="tablet"] .settings-page,
html[data-surface="desktop"] .settings-page {
  max-width: var(--content-wide);
  margin: 0 auto;
  padding: 28px var(--gutter) 64px;
  width: 100%;
  box-sizing: border-box;
}
/* Same zoom leak as main.stage — the settings page lives inside the stage, so
   its 100dvh painted 1.1× the window. Nothing is lost: the page's only reason
   to be tall was the cream fill, and `.shell-body` already carries that
   edge-to-edge at desktop in the same var(--bg). */
html[data-surface="desktop"] .settings-page { min-height: 0; }
html[data-surface="tablet"] .settings-body,
html[data-surface="desktop"] .settings-body { max-width: var(--content-max); margin: 0 auto; }
/* VB-04 — the title, the back link and the Account/About tabs are the HEAD of
   those cards, so they take the same measure. `.settings-page` keeps the wider
   budget (it is the page ground); only this header block was left outside the
   centring above, which put a 115px step between a heading and the thing it
   heads at 1366×1024. margin-left/right rather than the shorthand: both blocks
   carry their own bottom margin. */
html[data-surface="tablet"] .settings-header,
html[data-surface="desktop"] .settings-header,
html[data-surface="tablet"] .settings-tabs-wrap,
html[data-surface="desktop"] .settings-tabs-wrap {
  max-width: var(--content-max);
  margin-left: auto;
  margin-right: auto;
  width: 100%;
  box-sizing: border-box;
}
html[data-surface="desktop"] .settings-title { font-size: 34px; }

/* Settings data rows stack on a phone (C19): label + sub-hint on one side and
 * a button on the other left ~110px for the text, so "Fabrika ayarları…" and
 * its hint both wrapped to three lines. Below tablet the action goes full
 * width under the text. `width: 100%` rather than `align-items: stretch`
 * because two of the rows carry an inline align-items that would win. */
html[data-surface="phone"] .data-row {
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
}
html[data-surface="phone"] .data-btn-group { width: 100%; }
html[data-surface="phone"] .data-btn-group > button { flex: 1; }

/* ---------- Onboarding ----------------------------------------------------
 * The first-run flow is a modal column and stays one; it just gets air.
 *
 * 680/94% rather than 620/86% (2026-08-28 welcome-flow audit): on a 1180×820
 * iPad in landscape the old box left the pitch list scrolling with the product's
 * argument below the fold, on the device with the most room to show it. Now
 * three full pitch cards paint before any scroll.
 *
 * min-height guard: without it this rule (0,2,1) also beat index.html's
 * `@media (max-height: 500px)` full-bleed treatment (0,1,0), so a short, wide
 * window got a 620px letterbox instead of the intended edge-to-edge sheet.
 */
@media (min-height: 501px) {
  html[data-surface="desktop"] .onboard-shell {
    max-width: 680px;
    max-height: 94%;
  }
}

/* Hero tagline measure. 340px is right in the phone column; inside the 680px
 * desktop sheet it read as a narrow ragged block hanging under a full-width
 * wordmark. 480 keeps it a comfortable measure and lets the TR taglines (which
 * run longer than the EN one) settle in fewer, evener lines. Phone unchanged.
 * .onboard-legal keeps a FOOTNOTE measure of its own — see the SB-04 block below. */
html[data-surface="desktop"] .onboard-hero-tag { max-width: 480px; }

/* Step 2 is a left-aligned column — title, sub and the test rows all start at
 * the same edge — but the sync block's three text lines were centred (a phone
 * treatment, where the column IS the page). At desktop that put three ragged
 * centred lines under a left-aligned stack. Left-align them to the same edge.
 * Scoped to step 2 so the sign-in wall (#signin-gate-privacy, its own centred
 * modal) is untouched; the provider buttons keep their centred column. */
html[data-surface="desktop"] .onboard-step[data-step="setup"] .onboard-sync-label,
html[data-surface="desktop"] .onboard-step[data-step="setup"] .onboard-sync-note,
html[data-surface="desktop"] .onboard-step[data-step="setup"] .onboard-privacy {
  text-align: left;
  margin-left: 0;
  margin-right: auto;
}

/* SB-04 — ONE CARD, ONE LEFT EDGE, ONE MEASURE.
 * Measured at 1024x1366: the .onboard-step card is 628 wide and inside it sat a
 * full-width eyebrow (628), a 340px lede wrapping at ~46 characters with 288px
 * of the card unused beside it, a 254px sign-in button CENTRED, and a
 * full-width CTA. Four alignments and two measures in one card. 340px is a
 * PHONE measure (index.html .onboard-section-sub / .onboard-legal) that never
 * got a surface-aware token.
 *
 * Two rules, both scoped to the surfaces where the card is actually wide:
 *   1. open the prose to the readable measure (--content-measure, the same
 *      token the question column uses), capped at the card so nothing changes
 *      when the card is narrower than the measure;
 *   2. start the provider stack at the same left edge as the eyebrow and the
 *      CTA. Its buttons keep their fixed 280px width — they are controls, not
 *      prose — they just stop being centred against left-aligned siblings.
 * The sign-in GATE (#signin-gate) is a narrow centred dialog and is deliberately
 * untouched: there the centred stack IS the layout. */
html[data-surface="tablet"] .onboard-section-sub,
html[data-surface="desktop"] .onboard-section-sub {
  max-width: min(100%, var(--content-measure));
}
/* The trademark line keeps a FOOTNOTE measure — the rule above it used to say
 * "deliberately 340" and the intent stands; 340 is simply a phone number inside
 * a 589px card, which is what made the paragraph App Review reads wrap at ~46
 * characters. 480 is the hero tagline's measure, already proven on this card. */
html[data-surface="tablet"] .onboard-legal,
html[data-surface="desktop"] .onboard-legal { max-width: min(100%, 480px); }
html[data-surface="tablet"] .onboard-hero-tag { max-width: min(100%, 480px); }
html[data-surface="tablet"] .onboard-step .auth-stack,
html[data-surface="desktop"] .onboard-step .auth-stack { align-items: flex-start; }
html[data-surface="tablet"] .onboard-step .auth-stack > .onboard-social-btn,
html[data-surface="desktop"] .onboard-step .auth-stack > .onboard-social-btn { margin: 0; }
html[data-surface="tablet"] .onboard-step .auth-stack .gsi-overlay-host,
html[data-surface="desktop"] .onboard-step .auth-stack .gsi-overlay-host { justify-content: flex-start; }
/* Tablet gets step 2's left-aligned sync block too — same defect, same card,
 * the desktop rule above simply never covered the 820x1180 iPad. */
html[data-surface="tablet"] .onboard-step[data-step="setup"] .onboard-sync-label,
html[data-surface="tablet"] .onboard-step[data-step="setup"] .onboard-sync-note,
html[data-surface="tablet"] .onboard-step[data-step="setup"] .onboard-privacy {
  text-align: left;
  margin-left: 0;
  margin-right: auto;
}

/* SB-15 (layout half) — Settings → Account had the same two left edges: the
 * provider stack centred its 280px buttons inside a 799px section whose every
 * other row starts at the section's left edge. (The a11y half — one accessible
 * name for the Google control — is auth-sync.js's aria-hidden on the drawn
 * button; nothing to do here.) */
html[data-surface="tablet"] #settings-body .auth-stack,
html[data-surface="desktop"] #settings-body .auth-stack { align-items: flex-start; }
html[data-surface="tablet"] #settings-body .auth-stack > .onboard-social-btn,
html[data-surface="desktop"] #settings-body .auth-stack > .onboard-social-btn { margin: 0; }
html[data-surface="tablet"] #settings-body .auth-stack .gsi-overlay-host,
html[data-surface="desktop"] #settings-body .auth-stack .gsi-overlay-host { justify-content: flex-start; }
/* The stack's error line (SI-05) follows the edge the stack now starts at. */
html[data-surface="tablet"] .auth-stack-error,
html[data-surface="desktop"] .auth-stack-error { text-align: left; }
html[data-surface="tablet"] .signin-gate .auth-stack-error,
html[data-surface="desktop"] .signin-gate .auth-stack-error { text-align: center; }

/* ---------- Minimum touch boxes -------------------------------------------
 * Compact controls keep their small VISUAL size but grow an invisible hit
 * area to the 44px minimum (consultant-3 P0: the back affordance measured
 * 65×20 with zero padding — the primary exit on every sub-screen). The
 * ::after overlay hit-tests to its owner without shifting layout. Applied
 * here to the app-wide back look and the topbar icon button; the choice
 * eliminators get the same treatment in their component sheets.
 */
.home-link-btn, .settings-back, .icon-btn, .btn-text, .tap-xl { position: relative; }
/* Settings' Reset / Factory reset / Edit measured 40pt tall — the rows whose
   mis-tap costs the most (usability C27). The pill keeps its size; the box
   grows vertically only, so two side-by-side pills never overlap. */
.btn-text::after {
  content: ''; position: absolute;
  left: 0; right: 0; top: 50%; transform: translateY(-50%);
  height: 100%; min-height: 44px;
}
/* -12 on every side, not -10 horizontally: the back affordance drops its LABEL
 * on a narrow phone while a deneme clock is up (runner Header), and an
 * icon-only 20px chevron with -10 measures 40 wide — under the floor on the
 * only way out of a live 100-question sitting (study P1.10). */
.home-link-btn::after, .settings-back::after {
  content: ''; position: absolute; left: -12px; right: -12px; top: -12px; bottom: -12px;
}
/* Centred, never smaller than 44pt: a bare -4px inset only reaches 44 while
   the circle is exactly 36 (it is 28 in the shell's own base rule). */
.icon-btn::after {
  content: ''; position: absolute;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 100%; height: 100%; min-width: 44px; min-height: 44px;
}
/* The test-switcher chips are 29pt tall in a 38pt strip. Vertical growth only:
   the pills sit 2px apart horizontally, and the row above them holds the icon
   buttons at the other end of the bar, so nothing collides (D6). */
.test-tab { position: relative; }
.test-tab::after {
  content: ''; position: absolute;
  left: 0; right: 0; top: 50%; transform: translateY(-50%);
  height: 100%; min-height: 44px;
}
/* The rail's rows are 207×38 with a 2px gap, and at every iPad size the rail is
   a primary TOUCH target, not the mouse chrome it was built as (VB-11). Vertical
   growth only, same shape as .test-tab above: 44 on a 38 row reaches 3px into a
   2px gap, so neighbours touch rather than swallow each other. */
.sidenav-item { position: relative; }
.sidenav-item::after {
  content: ''; position: absolute;
  left: 0; right: 0; top: 50%; transform: translateY(-50%);
  height: 100%; min-height: 44px;
}
/* The deck's A-Z jump strip: 26 letters at a 35pt horizontal pitch, boxes 33
   wide (VB-11). The glyph column keeps its size; the box grows sideways to the
   44pt floor. The strip is inline-styled in subjects/vocab/deck.jsx, which is
   why the expander lives here (that file owns no stylesheet, and
   subjects/vocab/styles.css is another lane's). Expanders overlap by ~9px at
   the pitch — in the overlap the later letter wins, so every glyph keeps a
   full-pitch box and the gaps stop being dead. */
.deck-az-letter { position: relative; }
.deck-az-letter::after {
  content: ''; position: absolute;
  top: 0; bottom: 0; left: 50%; transform: translateX(-50%);
  width: 100%; min-width: 44px;
}
/* Utility for inline-styled compact pills (Flag, Scratchpad, İşaretle). */
.tap-xl::after { content: ''; position: absolute; left: -8px; right: -8px; top: -9px; bottom: -9px; }
/* GUARANTEED 44 (study P1.10). `tap-xl` adds a fixed margin, which is enough
 * for a pill and not enough for a 20×20 glyph or a 28px map chip. This one is
 * sized rather than inset: at least 44 in both axes, centred on the control, so
 * whatever the visual measures the hit box clears the floor. Neighbours must be
 * ≥44 apart on a grid that uses it (the exam map's gap is 16 for that reason)
 * or the boxes overlap. */
.tap-44 { position: relative; }
.tap-44::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 100%; height: 100%; min-width: 44px; min-height: 44px;
}

/* The controls a two-test market added or made load-bearing, all measured
 * under 44pt in the 2026-09 YKS study (P1.8). Every one keeps its drawn size —
 * the topbar is already 59px tall and the chips are a wrapping row — and grows
 * an outboard box only.
 *   .test-tab           56×31 → 64×44   the TYT↔AYT switcher, tapped daily,
 *                                       18px from the top edge. The two pills
 *                                       sit 2px apart, so the boxes cannot
 *                                       overlap enough to steal a tap.
 *   a.brand-name        32×26 → 40×46   the same control on a one-test market,
 *                                       where the wordmark IS the home link.
 *   .focus-chip         72×33 → 80×44   the alan chips (onboarding + Settings).
 *   .onboard-privacy a 110×16 → 122×44  the legal link App Review follows.
 * The two pill bands state the RESULT (a fixed 44px band, `.tap-44`'s shape)
 * rather than an inset: `inset` resolves against the PADDING box, so -7px on a
 * .test-tab's 26.8px-tall padding box measured 40.8, not 44.
 */
.test-tab, a.brand-name, .focus-chip, .onboard-privacy a { position: relative; }
.test-tab::after { content: ''; position: absolute; left: -4px; right: -4px;
  top: 50%; height: 44px; transform: translateY(-50%); }
a.brand-name::after { content: ''; position: absolute; inset: -10px -4px; }
.focus-chip::after { content: ''; position: absolute; left: -4px; right: -4px;
  top: 50%; height: 44px; transform: translateY(-50%); }
.onboard-privacy a { display: inline-block; }
.onboard-privacy a::after { content: ''; position: absolute; left: -6px; right: -6px; top: -14px; bottom: -14px; }

/* ---------- Topic-browser ders strip ---------------------------------------
 * window.GroupTabs lays its pills out as a WRAPPING row, which is right for
 * four groups and wrong for ten: TYT's ten ders names wrapped to seven rows of
 * sticky chrome above the list they exist to reach. One scrolling row instead,
 * with a soft edge so a clipped pill reads as "more this way" (the same
 * affordance the attached tab strip uses). The inline flex-wrap GroupTabs sets
 * is why these need !important.
 */
.topics-strip .group-tabs {
  flex-wrap: nowrap !important;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
}
.topics-strip .group-tabs::-webkit-scrollbar { display: none; }
.topics-strip .group-tabs > button { flex: none; white-space: nowrap; }

/* ---------- Settings rows on a phone --------------------------------------
 * .sb-row is a two-column flex row (label + control). The telemetry row's
 * 60-word privacy disclosure then renders as a 164px newspaper column, ten
 * lines at ~20 characters (E10). Below 480 the row wraps and the hint runs
 * full width UNDER the label and its control, which is where a paragraph of
 * explanation belongs.
 */
@media (max-width: 479px) {
  /* `display: contents` promotes the label block's children to flex items of
     the row itself, which is the only way a nested hint can claim the row's
     full width without restructuring the markup every settings surface emits.
     Scoped to the Settings page — the Aa popover stacks these rows already. */
  .settings-page .sb-row { flex-wrap: wrap; }
  .settings-page .sb-row > div:first-child { display: contents; }
  .settings-page .sb-row > div:first-child > .sb-row-label { flex: 1 1 auto; min-width: 0; }
  .settings-page .sb-row > div:first-child > .sb-hint { flex-basis: 100%; order: 3; margin-top: 8px; }
  .settings-page .sb-row > .sb-row-opts { order: 2; }
}

/* Topbar wordmark + test tabs (measured 50×26) and the onboarding Back (62×34)
   — study P1.14. Vertical only for the tabs: they sit shoulder to shoulder, so
   a horizontal overlay would swallow the neighbour's taps. The strip clips
   overflow-y, so this only reaches 44 because .topbar-row1 is 44 tall. */
.test-tabs .brand-name, .test-tab, .onboard-topbar-back { position: relative; }
.test-tabs .brand-name::after, .test-tab::after {
  content: ''; position: absolute; left: 0; right: 0; top: -12px; bottom: -12px;
}
.onboard-topbar-back::after {
  content: ''; position: absolute; left: -6px; right: -6px; top: -5px; bottom: -5px;
}
/* Reading-comfort sheet options (measured 78x42 and 120x42, no hit box at all)
   — study P1.14. Vertical only: the three pills of a segmented control sit
   shoulder to shoulder, so a horizontal overlay would eat the neighbour's
   taps. -3px each side takes 42 to 48; the pill's own size is unchanged. */
.a11y-pop .sb-opt { position: relative; }
.a11y-pop .sb-opt::after {
  content: ''; position: absolute; left: 0; right: 0; top: -3px; bottom: -3px;
}

/* ---------- Pointer affordances ------------------------------------------
 * A desktop user hovers before they click. The phone build has almost no hover
 * states because nothing hovers on a touchscreen; add the minimum so the app
 * doesn't feel inert under a cursor. Scoped to real pointers so a Magic
 * Keyboard iPad gets it and a finger doesn't.
 */
@media (hover: hover) and (pointer: fine) {
  html[data-surface="desktop"] button:not(:disabled),
  html[data-surface="tablet"] button:not(:disabled) { transition: filter var(--dur-quick) ease, transform var(--dur-quick) ease; }
  /* Perceptible, theme-aware: dims on paper, brightens under the lamp
     (stamped by activateTheme; the old 0.985 read as no feedback at all). */
  html[data-surface="desktop"] button:not(:disabled):hover { filter: var(--hover-filter, brightness(0.96)); }
}

/* ---------- Wide-surface scrollbars --------------------------------------
 * The rail and the content plane scroll independently at desktop; keep the
 * bars quiet so the app doesn't grow two loud gutters.
 */
html[data-surface="desktop"] .sidenav::-webkit-scrollbar { width: 8px; }
html[data-surface="desktop"] .sidenav::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 4px;
}
html[data-surface="desktop"] .sidenav::-webkit-scrollbar-track { background: transparent; }

/* ---------- Figure stimulus ----------------------------------------------
 * A figure never grows past its own display width (FigureCard caps it), so on
 * a wide column the inset panel around it was a full-width band with a small
 * drawing marooned in the middle. Hug the figure instead and centre the panel;
 * `--fig-hug` is the figure's width + its gutters, published by FigureCard and
 * absent for the escape-hatch kinds, which keep the full column.
 */
html[data-surface="tablet"] .figure-card,
html[data-surface="desktop"] .figure-card {
  max-width: var(--fig-hug, none);
  margin-inline: auto;
}

/* ---------- Prose measure -------------------------------------------------
 * 14–16px text set across the whole 680–760px column runs 85–100 characters
 * per line, past the ~90 ceiling — and the explanation is the text a student
 * reads most carefully. Cap the TEXT, never its panel: the why panel and the
 * item keep the full column, the prose just wraps sooner, still left-aligned.
 *
 * `ch` is the width of "0" — ~1.2x the average character in Lexend/Iowan — so
 * 62ch measures ~75 chars/line in English. Measured, not assumed: 68ch lands
 * at 83 and misses the window; Turkish runs ~10 above English at the same
 * physical width, its letters being narrower. Same 62ch that .mc-why /
 * .mc-why-tldr already carry in mc-item.css.
 *
 * Deliberately uncapped: choice rows (a wrapped choice is harder to scan than
 * a long one), figures, the passage pane (its own designed measure), the
 * bordered stimulus blocks .mc-quote / .mc-boxed (capping would move the
 * border, and the quote holds hard-broken verse), and .mc-insight (its top
 * rule must span the panel). The testlet stems .pi-stem / .pei-stem need no
 * entry of their own: their text blocks ARE .mc-stem (McItem's block renderer)
 * and are capped by the rule below like any other stem.
 */
html[data-surface="tablet"] .mc-stem,
html[data-surface="desktop"] .mc-stem,
html[data-surface="tablet"] .pi-why-body,
html[data-surface="desktop"] .pi-why-body,
html[data-surface="tablet"] .pei-why-body,
html[data-surface="desktop"] .pei-why-body {
  max-width: var(--content-measure);
}
/* ONE COLUMN FOR ONE QUESTION (study P2.8). At 1280 the same question drew in
 * three different widths — stem 622 (62ch of UI type), choice rows 766 (the
 * whole content column), why block 569 (62ch of SERIF type) — so the answers
 * were wider than the question they belong to and a one-word option sat in a
 * 766px row. `ch` cannot be the token: it resolves against each element's own
 * font, which is what produced 622 and 569 from the same declaration. The
 * token is px, and px inside `main.stage` is already scaled by the reading-size
 * zoom, so it tracks the text size like everything else.
 *
 * Choice rows were deliberately uncapped before ("a wrapped choice is harder to
 * scan than a long one"); that reasoning holds INSIDE the measure and stops
 * being about scanning past it — nothing is wrapped by matching the stem. */
html[data-surface="tablet"] .mc-choices,
html[data-surface="desktop"] .mc-choices,
html[data-surface="tablet"] .mc-why-block,
html[data-surface="desktop"] .mc-why-block {
  max-width: var(--content-measure);
}
/* The year-test paper sets its own booklet measure (below) and the two-column
   figure/short-answer grids need the room they were given. */
html[data-surface="tablet"] .anchor-paper .mc-choices,
html[data-surface="desktop"] .anchor-paper .mc-choices,
html[data-surface="tablet"] .mc-choices.has-figs,
html[data-surface="desktop"] .mc-choices.has-figs { max-width: none; }
/* The year-test paper sets its own booklet type and has not had its desktop
   pass; leave its stem at the paper's width. */
html[data-surface="tablet"] .anchor-paper .mc-stem,
html[data-surface="desktop"] .anchor-paper .mc-stem { max-width: none; }

/* ---------- Attached group tabs -------------------------------------------
 * The strip is one horizontally scrolling row; the edge fades are the "more
 * this way" cue, so the bar itself is noise on top of a 40px-tall strip.
 */
.group-tabs--attached .gt-scroll { scrollbar-width: none; }
.group-tabs--attached .gt-scroll::-webkit-scrollbar { display: none; }

/* ---------- Short viewports — a phone in landscape ------------------------
 * 390pt of height under 68pt of two-tier chrome pushed the home's single
 * action below the fold (usability D12). The SURFACE deliberately stays
 * `tablet` — the landscape runner is better in two columns, all five choices
 * visible — so only the vertical budget shrinks here. Nothing is hidden: the
 * test switcher (tier 2) is the sole way to change tests without a rail.
 */
html[data-short="1"] header.topbar { padding-top: 4px; padding-bottom: 4px; }
html[data-short="1"] .topbar-row1 { height: 24px; padding-top: 0; padding-bottom: 0; }
html[data-short="1"] .topbar-row2 { height: 30px; }
html[data-short="1"] main.stage { padding-top: 4px; }

/* ---------- Focus ring — shell-wide ---------------------------------------
 * The item renderers ship a designed ring (components/items/focus-rings.css)
 * and .test-tab has its own; every other keyboard-reachable control fell back
 * to the UA's 1px near-black outline at zero offset, which is invisible on an
 * accent- or ink-filled button (usability D14). :where() keeps specificity at
 * 0, so any element's own ring still wins.
 */
:where(button, a[href], summary, [role="button"], [tabindex]:not([tabindex="-1"])):focus-visible {
  outline: var(--item-focus-width, 2px) solid var(--item-focus-color, var(--accent));
  outline-offset: var(--item-focus-offset, 2px);
}

/* ---------- Touch behaviour (iOS) ----------------------------------------
 * Three declarations separate "a web page in a wrapper" from an app
 * (usability D19): the system tap flash ignores the 14-20px radii the surface
 * language rests on; a double-tap on a choice card is otherwise zoom-eligible
 * (`manipulation` still allows pinch-zoom, which is the point of D2); and the
 * shell's rubber-band otherwise chains into the web view's own bounce.
 */
:root { -webkit-tap-highlight-color: transparent; }
:where(button, a, [role="button"], summary, label) { touch-action: manipulation; }
.shell { overscroll-behavior-y: contain; }
