/* ===========================================================================
 * 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;

  /* SOFTWARE KEYBOARD (US iOS study 2026-10-03 U01). surface.js stamps
   * --kb-inset (layout px the keyboard covers, 0 without one). Sticky
   * footers inside the zoomed main.stage lift by --kb-lift: their lengths
   * are multiplied by the Aa zoom, so it is divided back out here. */
  --kb-lift: calc(var(--kb-inset, 0px) / var(--a11y-zoom, 1));
  /* The home-indicator pad those footers carry; the keyboard covers the
   * indicator, so it drops to 0 while one is up (no gap above the keys). */
  --kb-safe: env(safe-area-inset-bottom, 0px);
}
html.kb-open { --kb-safe: 0px; }

@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; }
/* The math-lessons product's rail also HOSTS the Lab's chapter list (owner,
 * 2026-09-24, LAB-30 — index.html __labRailHost): what used to be a 232 shell
 * rail plus a 272 chapter column beside it is now one rail. 288 = the old
 * chapter column's content width (264) plus this rail's 12px gutters, so a
 * chapter label wraps exactly where it did. The content plane gains the rest
 * (~236px at 1180). `.lab-rail` is stamped pre-paint from the product cap, so
 * the rail never changes width when the Lab mounts. */
html.lab-rail[data-surface="desktop"] { --sidenav-w: 288px; }

/* ---------- 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.
 */
/* THE VIEWPORT FILL, and the only box that carries it. The column below is a
 * flex one so `main.stage` can simply GROW to the viewport instead of being
 * told a pixel height: the old floor was a literal `calc(874px - 68px)` on
 * main.stage, .subject-root and .settings-page (the dev iPhone frame), which
 * on any shorter phone was scrollable dead space under every screen. It has to
 * be `.shell-body` and not main.stage: main.stage carries the Aa control's
 * `zoom`, so no viewport unit inside it is honest (see the NO VIEWPORT UNITS
 * note below). How tall the viewport IS comes from the two shells — `100%` of
 * the device frame in dev (index.html), `100dvh` in production (build.js
 * shellCSS) — and everything from here down is shared. */
.shell-body { display: flex; flex-direction: column; min-width: 0; }
main.stage { flex: 1 0 auto; }
/* 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: calc(100dvh - env(safe-area-inset-top, 0px));
}
/* 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;
  /* VBI-42 — the rail was ALREADY its own scroller (it scrolls programmatically:
     scrollTop held, scrollHeight 1165 vs client 768 at ipad-ls), but on a
     packaged iPad the WebView handed the touch to the page scroller instead and
     the rows below the fold — four word-part drills in the Vocabulary Builder —
     were unreachable by gesture. Claim the vertical gesture for this scrollport
     (`touch-action: pan-y`), stop the fling from chaining out to the page once
     the rail hits its end (`overscroll-behavior: contain`), and keep the legacy
     momentum hint for older WebKit. Horizontal is deliberately NOT claimed: the
     rail sits on the left screen edge, where the native shell's edge-pan
     back gesture starts. */
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  touch-action: pan-y;
  /* 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; }
/* …and while a full test's clock is running (US2-29). The runner stamps
   `data-sealed-live` from Start to its last exit (full-test.jsx); the body
   takes the width, and the runner's own Pause is the way out. */
html[data-sealed-live="1"] .sidenav { display: none !important; }
html[data-sealed-live="1"][data-surface="desktop"] body::after { display: none; }

/* 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; }

/* N10 (Builder review 2026-10-21, finding 8) — A SCROLLED WELCOME FADES INTO
   THE STRIP. `body::before` paints paper over the status-bar inset, and a line
   passing under it was cut mid-glyph ("Sign in" half under the clock). While a
   standalone product's welcome is scrolled (html.welcome-scrolled, set by
   WelcomeScreen in subjects/learning-lab/home.jsx), the strip runs on for a
   short tail that fades to clear, so content dissolves into it the way a
   native scroll edge does. At rest there is no tail: the topbar meets the strip
   square. The tail is min(20px, inset), so in a browser (env() is 0) the strip
   stays 0 tall; it lets taps through, since it now covers content. */
html.welcome-scrolled body::before {
  height: calc(env(safe-area-inset-top, 0px) + min(20px, env(safe-area-inset-top, 0px)));
  background: linear-gradient(var(--bg) calc(100% - min(20px, env(safe-area-inset-top, 0px))), transparent);
  pointer-events: 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;
}

/* EP-11 (exam-parts study, 2026-10-01) — THE PREP RAIL HAS THREE PARTS. The
   head (wordmark, test row, the six home tabs) is pinned: auto-scrolling to an
   active section row used to carry Ana sayfa…Gelişim out of view. Only the
   middle region scrolls, between the head and the footer, so its last row can
   never sit under the footer (the footer is no longer drawn over a shared
   scrollport — it is the next box down). Soft shadows at the region's edges
   say there is more; they appear only where there is (background-attachment
   local vs scroll, the classic scroll-shadow pair). The rail itself keeps
   `overflow-y: auto` as a fallback for a window too short for head + foot. */
html[data-surface="desktop"] .sidenav.is-split .sidenav-head {
  position: sticky;
  top: 0;
  z-index: 2;
  flex: 0 0 auto;
  background: var(--bg-deep);
}
html[data-surface="desktop"] .sidenav.is-split .sidenav-head .sidenav-brand { position: static; }
html[data-surface="desktop"] .sidenav.is-split .sidenav-head .sidenav-brand::after { display: none; }
html[data-surface="desktop"] .sidenav.is-split .sidenav-head .sidenav-tabs { margin-bottom: 8px; }
html[data-surface="desktop"] .sidenav.is-split .sidenav-scroll {
  flex: 1 1 0;
  min-height: 140px;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
  margin: 0 -12px;
  padding: 8px 12px;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background:
    linear-gradient(var(--bg-deep) 30%, transparent) top / 100% 28px no-repeat local,
    linear-gradient(transparent, var(--bg-deep) 70%) bottom / 100% 28px no-repeat local,
    radial-gradient(farthest-side at 50% 0, rgba(42, 38, 32, 0.16), rgba(42, 38, 32, 0)) top / 100% 12px no-repeat scroll,
    radial-gradient(farthest-side at 50% 100%, rgba(42, 38, 32, 0.16), rgba(42, 38, 32, 0)) bottom / 100% 12px no-repeat scroll,
    var(--bg-deep);
}
html[data-surface="desktop"] .sidenav.is-split .sidenav-foot { flex: 0 0 auto; margin-top: 0; }
html[data-surface="desktop"] .sidenav.is-split .sidenav-foot::before { display: 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;
  /* 6, not 2 (VBI-45). The rows are 38pt and carry a 44pt ::after hit box (see
     "Minimum touch boxes" below); at a 2px gap that box reached 3pt into its
     neighbour's, so two adjacent rows contested 4pt of each other's target and
     the effective pitch was 40. A 6px gap makes the PITCH exactly 44 — every
     row gets the whole floor, nothing overlaps, and the rows keep their own
     visual size and weight. The cost is 4pt of rail height per row, which is
     why this lands with the rail's scroll fix (VBI-42). */
  gap: 6px;
  margin-bottom: 12px;
}
.sidenav-group[hidden] { display: none; }
/* The Lab's chapter rail slot (index.html __labRailHost). Nothing portalled in
   — not the desktop surface, or the Lab is not the screen — means no box. */
.sidenav-lab { min-width: 0; margin-bottom: 12px; }
.sidenav-lab:empty { 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: 11px;
  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;
  min-height: 44px;   /* iPad review TN7: the rows measured 41–43 */
  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-lessons:empty { display: none; }
/* LESSON DRAWERS (index.html paintLessonRail) — the Learn tab's drawers in
   the rail. The header is an ordinary row plus a caret; its lessons hang off
   a hairline, one line each, so an open drawer reads as that lab's list. */
.sidenav-drawer .sidenav-item-text { flex: 1 1 auto; }
.sidenav-caret { opacity: 0.55; transition: transform var(--dur-quick) ease; }
.sidenav-drawer[aria-expanded="false"] .sidenav-caret { transform: rotate(-90deg); }
.sidenav-drawer-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: -2px 0 4px 14px;
  padding-left: 8px;
  border-left: 1px solid var(--line);
}
.sidenav-lesson {
  font-size: 13px;
  font-weight: 500;
  padding: 7px 8px;
  gap: 8px;
}
.sidenav-lesson-n {
  flex: 0 0 auto;
  align-self: flex-start;
  padding-top: 1px;
  min-width: 18px;
  font-size: 11px;
  font-weight: 700;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
/* Two lines, not one: at the rail's 232px a one-line ellipsis cut most
   lesson names to three words. */
.sidenav-lesson-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
/* A lesson waiting on the student (started, or offered after a miss). */
.sidenav-lesson-dot {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
}
/* "All N <lab> lessons" — the Learn tab's in-card footer (LessonDrawer,
   home.jsx) at the rail's density: a RAISED row (raised = tappable — the
   window.__raised triple in CSS), dark ink, a chevron, naming the section it
   opens. It keeps a lesson line's padding, so the drawer grows by 4px, not a card. */
.sidenav-lesson-all,
.sidenav-lesson-all:hover:not(:disabled),
.sidenav-lesson-all[aria-selected="true"] {
  background: var(--card);
  color: var(--ink);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 6px 18px rgba(0, 0, 0, 0.045);
}
.sidenav-lesson-all { border-color: var(--line); font-weight: 700; margin-top: 4px; justify-content: space-between; }
.sidenav-lesson-all[aria-selected="true"] { border-color: var(--line-strong); }
.sidenav-lesson-chev { flex: 0 0 auto; opacity: 0.45; }
.sidenav-spacer { flex: 1 1 auto; min-height: 12px; }
.sidenav-foot {
  display: flex;
  flex-direction: column;
  gap: 6px;   /* same 44pt pitch as .sidenav-group (VBI-45) */
  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: 13px;
  font-weight: 500;
  color: var(--ink-soft);
  line-height: 1.2;
}

/* ---------- Main stage ---------------------------------------------------
 * NO VIEWPORT UNITS INSIDE main.stage. The stage carries the text-size
 * control's `zoom` (default textScale 1.0 on a phone, 1.1 on tablet/desktop —
 * window.__a11yDefaultScale, index.html), and a `dvh` resolves in layout px
 * and is THEN scaled — so `100dvh` here painted 1.1× the window and left a
 * phantom scrollbar of exactly 0.10×viewport. Desktop was fixed first; the
 * tablet copy carried the leak on (with an unaudited `-96px` of topbar math),
 * and phone had the same bug in pixels — `calc(874px - 68px)`, the dev device
 * frame. All three are gone: main.stage is a flex ITEM of `.shell-body` now
 * (above) and grows to whatever the viewport actually is, which is a number
 * neither the zoom nor a guess at the topbar can get wrong.
 */
html[data-surface="desktop"] main.stage {
  padding-top: 22px;
}

/* ---------- 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;
}
/* The rows and the head inset by the gutter, as every tab's cards do, so
   Settings starts on the same edge as the page that opened it (iPad review
   TN1 for the Vocabulary Builder; every product since 2026-10-24). */
html[data-surface="tablet"] .settings-header,
html[data-surface="desktop"] .settings-header,
html[data-surface="tablet"] .settings-body,
html[data-surface="desktop"] .settings-body {
  padding-left: var(--gutter);
  padding-right: var(--gutter);
  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.
 */
/* TABLET TOO (study LGS-18 / ISEE-19). An iPad held upright is a `tablet`
 * surface, and it was getting the phone's 560 sheet: a narrow column floating
 * in the middle of an 834pt screen, with the blurred app behind it peeking out
 * on both sides and reading as a rendering artefact rather than as depth. The
 * same 680 column the desktop rule already gives, at the surface that has the
 * room for it. Height is capped, never set — the sheet hugs its content (see
 * updateOnboardScrollCue). */
@media (min-height: 501px) {
  html[data-surface="desktop"] .onboard-shell,
  html[data-surface="tablet"] .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,
html[data-surface="tablet"] .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); }
/* …AND THE STACK SPANS THE CARD (TR presubmit P2.11). The 280px buttons and
 * the 300px "Hesapsız başlayabilirsin…" line sat at half the iPad card's width
 * under a full-width CTA — one left edge, two measures. They now take the
 * card's width, as the CTA and the test rows do; the left edge is unchanged. */
html[data-surface="tablet"] .onboard-step .auth-stack,
html[data-surface="desktop"] .onboard-step .auth-stack { align-items: stretch; --gsi-overlay-w: 100%; }
html[data-surface="tablet"] .onboard-step[data-step="setup"] .onboard-sync-note,
html[data-surface="desktop"] .onboard-step[data-step="setup"] .onboard-sync-note { max-width: none; }
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.
   CAVEAT (study L1-28): side-by-side is the only safe arrangement. In a
   WRAPPING row the growth is vertical into the next line, and where two
   overlays overlap the later one wins — the report-a-problem reason chips
   (29px tall, 7px apart) measured 35, not 44. A chip row that wants the floor
   has to pay for it in its own box: ≥15px of vertical gap, or a taller chip. */
.btn-text::after {
  content: ''; position: absolute;
  left: 0; right: 0; top: 50%; transform: translateY(-50%);
  height: 100%; min-height: 44px;
}
/* -13 horizontally, not -10: the back affordance drops its LABEL on a narrow
 * phone while a deneme clock is up (runner Header), and a bare chevron with -10
 * measures 38 wide — under the floor on the only way out of a live
 * 100-question sitting (study P1.10). P1.10 wrote -12 against a 20px glyph;
 * the shell draws it at 18 (shared-app-shell.jsx), so -12 measured 42. -13 is
 * what 18 needs, and it is what the rail already used.
 * VERTICALLY it is stated as a RESULT, not as an inset (2026-09-19, package G's
 * handoff): -12 on a 19px-tall label box lands at 43, and the study measured
 * 42. An inset is a bet that the control's own height never changes; the
 * .btn-text recipe below-the-floor-proofs it instead — `height: 100%` plus a
 * 44px floor, centred on whatever the button actually measures. */
.home-link-btn::after, .settings-back::after {
  content: ''; position: absolute; left: -13px; right: -13px;
  top: 50%; transform: translateY(-50%); height: 100%; min-height: 44px;
}
/* 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, 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. The gap between rows is 6px rather than
   2 (.sidenav-group / .sidenav-foot, VBI-45), so 44 on a 38 row lands exactly
   inside the 44pt pitch: no two expanders overlap and no row is contested. */
.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 (VB-11). Since the Builder review's N7
   (2026-10-22) deck.jsx draws each letter box 44 wide itself; this expander
   stays as the floor for any narrower box. 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; }

/* The ones still under 44 in the iPhone review (2026-10-18, #25, N1),
 * measured as the review's appendix says — with elementFromPoint across each
 * control, which counts these overlays. Most controls it listed already
 * cleared 44 that way (header Aa/gear and the rail's flag 44, the eliminator
 * 45.5x46.5, Settings Back 89x44.5, Switch and Reset 296x44.5, the toggles
 * 45+x47.5); the rectangle alone is what read 36 or 28.
 *   a.brand-name (one test)  27.5x45 -> 44x45   the strip it sits in was only
 *                            as wide as the word and clips sideways; with no
 *                            pills beside it there is nothing to scroll, so it
 *                            stops clipping and the box is centred at 44. A
 *                            standalone product's wordmark (--fit) is wide
 *                            already and keeps the clip __fitBrand measures.
 *   .tap-44--lead            a disclosure at a card's left edge (topic "+",
 *                            37.5 wide): the card's rounded clip ate the
 *                            centred box's left 7px, so the box starts at the
 *                            control instead and takes the row's first 14px.
 *   a.tap-line               a link alone on its line (Settings > About:
 *                            support mail 17.5 tall, privacy policy 17.5) — a
 *                            44 band on the line. The two sit on adjacent
 *                            lines at opposite ends, so the bands never meet. */
.test-tabs:not([role="tablist"]):has(> a.brand-name:not(.brand-name--fit)) { overflow: visible; }
.test-tabs:not([role="tablist"]) > a.brand-name:not(.brand-name--fit)::after {
  left: 50%; right: auto; width: 100%; min-width: 44px; transform: translateX(-50%);
}
.tap-44--lead::after { left: 0; transform: translateY(-50%); }

/* THE LGS APP (docs/LGS_REDESIGN_2026_10.md §3.1; persona panel 2026-10-24).
 * html[data-lgs="1"] is stamped by subjects/practice/lgs-shell.jsx only where
 * the school-year cap holds, so nothing here reaches another market.
 *   - Tap targets of 48, not 44 (§3.1, finding 11): the shared overlays that
 *     carry the floor grow to 48 — "Bilmiyorum", "Soruları gör", the quiet
 *     links, the back controls.
 *   - The one-test wordmark never wears the strip's fade (finding 15): its
 *     44px hit box made the strip's scrollWidth exceed its width, so the
 *     overflow fade cut "LGS" to "LG" and a ghost S. (Now every app.)
 *   - A collapsed topbar's controls leave the page (finding 24): the runner
 *     showed two "Aa", the topbar's still there at height 0. (Now every app.) */
html[data-lgs="1"] .tap-44::after { min-width: 48px; min-height: 48px; }
html[data-lgs="1"] .home-link-btn::after,
html[data-lgs="1"] .settings-back::after,
html[data-lgs="1"] .btn-text::after { min-height: 48px; }
/* The two below were LGS-only until review 2026-10-28 (M2): YKS showed the
 * same "YK" + ghost S and the same hidden second "Aa", so they hold for every
 * app. */
.test-tabs:has(> a.brand-name:not(.brand-name--fit))[data-fade] {
  -webkit-mask-image: none; mask-image: none;
}
header.topbar.hide-chrome { visibility: hidden; }
a.tap-line { position: relative; }
a.tap-line::after { content: ''; position: absolute; left: -4px; right: -4px;
  top: 50%; height: 44px; transform: translateY(-50%); }

/* ---------- Topic-browser ders strip ---------------------------------------
 * window.GroupTabs lays its pills out as a WRAPPING row, which is right for
 * four groups and wrong for ten ON A PHONE: TYT's ten ders names wrapped to
 * seven rows of sticky chrome above the list they exist to reach. One
 * scrolling row there instead. PHONE ONLY (owner, 2026-10-01): at tablet and
 * desktop the pills wrap — there is room, and a mouse cannot reach a pill past
 * the edge of a scrolling row (YKS topic browser on an iPad with a mouse).
 * The edge fade and the mouse wheel come from window.__hstrip (surface.js),
 * which GroupTabs attaches; the fade is [data-hfade] below, shown only on the
 * side that has more. The inline flex-wrap GroupTabs sets is why these need
 * !important.
 */
html[data-surface="phone"] .topics-strip .group-tabs {
  flex-wrap: nowrap !important;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
}
html[data-surface="phone"] .topics-strip .group-tabs::-webkit-scrollbar { display: none; }
html[data-surface="phone"] .topics-strip .group-tabs > button { flex: none; white-space: nowrap; }

/* Edge fade for any one-row strip window.__hstrip manages: the strip itself
   goes transparent at the edge it can still travel toward, so the cue is right
   on any surface it sits on. Same taper as the topbar's .test-tabs[data-fade]. */
[data-hfade="right"] {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - min(28px, 22%)), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - min(28px, 22%)), transparent);
}
[data-hfade="left"] {
  -webkit-mask-image: linear-gradient(to left, #000 calc(100% - min(28px, 22%)), transparent);
  mask-image: linear-gradient(to left, #000 calc(100% - min(28px, 22%)), transparent);
}
[data-hfade="both"] {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 min(28px, 22%), #000 calc(100% - min(28px, 22%)), transparent);
  mask-image: linear-gradient(to right, transparent, #000 min(28px, 22%), #000 calc(100% - min(28px, 22%)), transparent);
}

/* ---------- 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;
}
/* …and the same pills everywhere ELSE in Settings (study L1-03: telemetry
   Off/On measured 43×42 and 41×42 with no box at all). ±3 on every side, which
   is the exact ceiling: `.sb-row-opts` is a wrapping flex row with a 6px gap on
   BOTH axes, so two neighbours' overlays ABUT on the shared line and neither
   loses reach — anything past 3 would overlap, and where two overlays overlap
   the later one wins and the earlier control ends up with LESS than it started
   with. ±2 was tried first and left the narrowest pill ("On", a 40.5px box) at
   43.0px measured, a pixel under the floor. The a11y sheet keeps its own rule
   above (vertical only, shoulder-to-shoulder pills with no gap to spend). */
.sb-opt { position: relative; }
.sb-opt::after {
  content: ''; position: absolute; left: -3px; right: -3px; top: -3px; bottom: -3px;
}
/* The site footer's crawlable link rows — five exam links and About/Privacy,
   19–21px tall (study L1-07). REAL padding, not an ::after overlay: the rows
   WRAP, and two wrapped lines 2px apart cannot both carry a 44px overlay
   without one eating the other. Vertical only, because the `·` separator is
   drawn from the FOLLOWING anchor and sits inside its box — the anchors
   already abut and there is no horizontal room to take. `:not([hidden])` so
   the per-host filtering (the edge and setupSiteFooter hide the links this
   domain does not carry) still wins from the shell's own sheet. Touch
   surfaces only; a desktop footer stays compact. */
html[data-surface="phone"] .site-footer-row a:not([hidden]),
html[data-surface="phone"] .site-footer-meta a:not([hidden]),
html[data-surface="tablet"] .site-footer-row a:not([hidden]),
html[data-surface="tablet"] .site-footer-meta a:not([hidden]) {
  display: inline-block;
  padding: 13px 0;   /* 19px line box + 26 = 45 */
}

/* ---------- 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;
}

/* A TUS note's or card's TABLE fills the reading column on every surface
 * (owner, 2026-10-27: a table drawn at its own small width was hard to read).
 * A table is text in a grid, so scaling it to the column scales its type with
 * it; nothing else in the app opts in. subjects/tus/learn.jsx, recall.jsx. */
.tus-wide-table .figure-card { max-width: none; }
.tus-wide-table .math-fig-scale { max-width: none !important; }
.tus-wide-table .math-fig-scale > svg { max-width: 100% !important; max-height: none !important; }

/* ---------- 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. */
/* `.mcq-choices` is the SHARED answer row the four item renderers now all emit
   alongside their own class (package B, S-04) — without it named here the cap
   reached an mc single and skipped an analogy, a reading question and an SHSAT
   Editing B testlet, which is the same measure bug seen three more times. */
html[data-surface="tablet"] .mc-choices,
html[data-surface="desktop"] .mc-choices,
html[data-surface="tablet"] .mcq-choices,
html[data-surface="desktop"] .mcq-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"] .anchor-paper .mcq-choices,
html[data-surface="desktop"] .anchor-paper .mcq-choices,
html[data-surface="tablet"] .mc-choices.has-figs,
html[data-surface="desktop"] .mc-choices.has-figs,
html[data-surface="tablet"] .mcq-choices.has-figs,
html[data-surface="desktop"] .mcq-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; }

/* ---------- B-49 — a sticky bar paints the notch band above itself --------
 * A bar pinned at `top: env(safe-area-inset-top)` leaves a strip between the
 * top of the scrollport and the top of the bar. `body::before` (index.html /
 * build.js shellCSS) is supposed to own that strip, and on the practice runner
 * it measurably does not: scrolling content came THROUGH it, so the runner
 * already paints its own band (SI-12, runner.jsx). This is that fix as a
 * shared class, so the next sticky bar does not rediscover it — the sealed
 * paper's meta/clock row had the same half-cut line of passage text sitting
 * beside the status-bar clock.
 *
 * `bottom: 100%` of the sticky element is always the sliver immediately above
 * it, so the band travels with the bar. Zero-height in a browser (env() is 0),
 * so it costs nothing off-device. The ground defaults to the page's, and a bar
 * on a different ground sets `--safe-band-bg`.
 */
.safe-band-top { position: relative; }
.safe-band-top::before {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 100%;
  height: env(safe-area-inset-top, 0px);
  background: var(--safe-band-bg, var(--bg));
  pointer-events: none;
}

/* ---------- Bottom tab bar (phone + tablet) --------------------------------
 * An iOS-style tab bar for the home screens. LAYOUT ONLY: the cards it pages
 * between are unchanged, they just sit on one tab instead of one long scroll.
 *
 * STICKY, NOT FIXED (see window.BottomTabs, shared-app-shell.jsx): the page
 * scroller is `.shell` in dev and the document in the production bundle, and
 * dev renders inside an iPhone frame — a fixed bar would span the browser
 * window. As the last child of the screen root, `bottom: 0` pins it against
 * whichever ancestor scrolls and keeps it inside the shell column.
 *
 * The host root carries `.has-bottom-tabs`: a flex column so the bar can take
 * `margin-top: auto` and sit at the foot of a short page rather than floating
 * under two cards. The height that makes that work is MEASURED and applied
 * inline (window.useBottomTabsHost, shared-app-shell.jsx) — no viewport unit
 * can be used inside main.stage's zoom, see the "NO VIEWPORT UNITS" note.
 */
.has-bottom-tabs {
  display: flex;
  flex-direction: column;
}
/* WHERE THE SITE FOOTER SITS WHILE THE BAR IS UP. #site-footer is normally a
 * sibling of the subject roots, further down main.stage — i.e. OUTSIDE the
 * sticky bar's containing block, which is all it takes to drag the bar up off
 * the bottom of the screen at the end of every home scroll. The shell moves
 * the node in here instead (index.html placeSiteFooter), directly above the
 * bar, where it is the home's own last block and the bar is once again the
 * last thing on the page. The footer already centres itself in the readable
 * column and pads itself to the gutter; inside the host it is already in that
 * column, so it hands both back. */
/* …AND NOTHING MAY SIT BELOW THE BAR, an ancestor's bottom padding included.
 * A sticky element is only ever offset UP toward its stick edge — it never
 * travels past its own flow position — so every px of padding under the host
 * parks the bar exactly that far short of the screen edge at the end of the
 * scroll (the vocab app's `.app-main` pays 60, and it measured as a 60px gap
 * under the bar). Both hosts already zero their OWN bottom pad inline when
 * tabbed; this is the wrapper they sit in. The bar carries the home-indicator
 * inset itself, so there is nothing left for a parent to add. The leading
 * `html` is there to win: the wrappers this has to beat are single-class
 * rules in a stylesheet that loads after this one (subjects/vocab/styles.css
 * `:where(.vocab-app) .app-main`), and an element-qualified selector settles
 * it without naming any one app. */
html :has(> .has-bottom-tabs) { padding-bottom: 0; }

.site-footer-slot:empty { display: none; }
.site-footer-slot > .site-footer {
  max-width: none;
  margin: 40px 0 16px;
  padding-left: 0;
  padding-right: 0;
}
.bottom-tabs {
  margin-top: auto;
  position: sticky;
  bottom: 0;
  z-index: 6;
  display: flex;
  align-items: stretch;
  /* A bar, not a card edge: an OPAQUE card fill with a hairline on top (the
     hairline is what separates it from cream; the old 1px --line did not).
     Opaque, not the 2026-09-22 translucent iOS material: two reviewers in TR
     review 4 (2026-10-03) read text showing through the bar at the fold as
     letters cut in half. To restore the material, swap this line for
     color-mix(--card 84%) + backdrop-filter: saturate(180%) blur(20px). */
  background: var(--card);
  border-top: 0.5px solid color-mix(in srgb, var(--ink) 22%, transparent);
  /* The bar's own bottom inset — a home-indicator phone keeps the labels off
     the gesture bar. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
  /* It is pinned inside a padded column; the negative margins let the hairline
     and the fill run edge to edge like a real tab bar. The host sets
     --btabs-bleed to its OWN horizontal padding (the practice home's gutter is
     surface-driven, the vocab home's is a flat 20), so the bleed can never
     out- or under-run the column it sits in. */
  margin-left: calc(-1 * var(--btabs-bleed, var(--gutter, 20px)));
  margin-right: calc(-1 * var(--btabs-bleed, var(--gutter, 20px)));
}
.bottom-tab {
  flex: 1 1 0;
  min-width: 0;
  height: 56px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 0 2px;
  border: none;
  background: none;
  color: var(--ink-soft);
  font-family: var(--font-ui, inherit);
  cursor: pointer;
}
/* THE SELECTED TAB IS A SHAPE, NOT ONLY A HUE (owner, 2026-09-24). Accent
   ink alone on the card ground read as "slightly warmer grey" — the owner could
   not tell which tab was lit. So the active tab carries three channels: an
   indicator capsule (accent-soft, the top strip's own active ground), the
   tinted ink on it (never `accent` on `accent-soft`), and a heavier label.
   Inactive tabs stay ink-soft.
   THE CAPSULE HOLDS ICON AND LABEL (owner, 2026-09-28). It used to sit behind
   the icon alone — Material 3's navigation-bar indicator, which read as an
   Android control on iOS. iOS 26's tab bar lights the whole item, so the
   capsule is the button itself, inset 4px inside its cell. */
.bottom-tabs[data-placement="bottom"] { justify-content: space-evenly; }
/* Capped, so a three-tab bar does not stretch the capsule to a third of the
   screen and press it against the rounded display edge. A phone's bar holds
   four tabs at most (iPhone review O4), each well inside the cap. */
.bottom-tabs[data-placement="bottom"] .bottom-tab {
  flex: 0 1 92px;
  margin: 4px 2px;
  height: auto;
  min-height: 48px;
  padding: 4px 2px;
  border-radius: 24px;
}
/* BottomTabs' fit (shared-app-shell.jsx) steps this down only while a label
   would clip at the reading-size zoom; 1 everywhere else. */
.bottom-tab { zoom: var(--btab-zoom, 1); }
.bottom-tabs[data-placement="bottom"] .bottom-tab.is-active {
  background: var(--accent-soft);
  color: var(--ink-on-tint, var(--ink));
}
.bottom-tab-icon { position: relative; display: block; line-height: 0; }
.bottom-tab.is-active .bottom-tab-label { color: var(--ink-on-tint, var(--ink)); font-weight: 700; }
.bottom-tab-label {
  /* iOS proportions: a ~25px glyph over a 10px label (BottomTabs draws the
     bottom icon at 25). */
  font-size: 11px;
  font-weight: 600;
  line-height: 1.1;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The bottom bar lets a name take TWO lines rather than ellipsize ("Ana
   sayfa", "Çalışma kâğıtları" in a ~58px cell). The capsule grows to hold
   them, so every cell grows with the tallest; a one-word name that still
   cannot fit keeps the ellipsis on its second line. */
.bottom-tabs[data-placement="bottom"] .bottom-tab-label {
  /* --btab-label-k: BottomTabs' fit under an Android web view's system text
     zoom (shared-app-shell.jsx); 1 everywhere else. */
  font-size: calc(11px * var(--btab-label-k, 1));
  white-space: normal;
  text-align: center;
  /* 1.1 clipped the cedillas and breves of a clamped line (ç ş ğ). */
  line-height: 1.2;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow-wrap: normal;
  hyphens: none;
}
/* No 320-phone step-down any more: it existed for six ~49px cells, and four
   cells of ~76px hold "Ana sayfa" or "Soru çöz" at the bar's own 11px (iPhone
   review O4). The bar's fit() still steps its zoom down at the largest
   reading sizes, as before. */
/* A count, not a dot: the review queue's size is the reason to tap. */
.bottom-tab-badge {
  position: absolute;
  top: -3px; left: calc(100% - 8px);
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  box-sizing: border-box;
  border-radius: 999px;
  background: var(--accent);
  color: var(--card);
  font-size: 11px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* ---------- Top placement (tablet) -----------------------------------------
 * ONE NAVIGATION MODEL ACROSS ROTATION (owner, 2026-09-23). An iPad held
 * upright is the tablet surface; turned sideways it is desktop and the rail
 * carries the same tabs. Upright, the strip moves to the TOP of the column
 * (the iPadOS 18 idiom) instead of a phone-style bar inset to a 680 column.
 * It is the host's FIRST child, pinned at the top inset like the pinned mode
 * header (shared-chrome.css .mode-headwrap--sticky), and the host applies
 * none of the foot machinery above (no .has-bottom-tabs, no footer slot).
 * Pills on the page ground: inactive ink-soft, active on accent-soft with the
 * tinted ink (never `accent` on `accent-soft` — CLAUDE.md accent-tile note).
 * No transition: a tab swap is a page swap, not a value growing. */
.bottom-tabs[data-placement="top"] {
  margin-top: 0;
  margin-bottom: 16px;
  position: sticky;
  top: env(safe-area-inset-top, 0px);
  bottom: auto;
  z-index: 6;
  height: 48px;
  box-sizing: border-box;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 0 8px;
  background: var(--bg);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-top: none;
  border-bottom: 1px solid var(--line);
}
/* Still too wide at BottomTabs' zoom floor (Aa Largest, six pills): wrap to
   two rows rather than clip Home/Progress off the ends (US iOS study U03). */
.bottom-tabs[data-placement="top"][data-wrap="1"] {
  flex-wrap: wrap;
  height: auto;
  min-height: 48px;
  row-gap: 4px;
  padding-top: 4px;
  padding-bottom: 4px;
}
.bottom-tabs[data-placement="top"] .bottom-tab {
  flex: 0 0 auto;
  height: auto;
  flex-direction: row;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  position: relative;
}
/* 44pt to the finger (iPad review TN7): the pill is ~35px tall, so its hit
   area reaches 5px past it top and bottom, inside the strip's 48px. The pill
   you see does not change. */
.bottom-tabs[data-placement="top"] .bottom-tab::after {
  content: ''; position: absolute; left: 0; right: 0; top: -5px; bottom: -5px;
}
.bottom-tabs[data-placement="top"] .bottom-tab.is-active {
  background: var(--accent-soft);
  color: var(--ink-on-tint);
}
/* The whole pill is the indicator up here, so the icon's own is dropped. */
.bottom-tabs[data-placement="top"] .bottom-tab.is-active .bottom-tab-label { color: var(--ink-on-tint); }
.bottom-tabs[data-placement="top"] .bottom-tab-label {
  font-size: 13px;
  line-height: 1.2;
}
/* Desktop: the rail IS the strip (index.html renderSidenav, the unlabelled
   tab group), so the host renders its tabbed page with no strip of its own. */
html[data-surface="desktop"] .bottom-tabs { display: none; }
/* THE TOAST SITS ABOVE THE BAR (US apps study 2026-10-03, finding 2). The
   fixed AppToast's own bottom clears only the home indicator, so the "Paused"
   toast covered the tabs. --btabs-h is the bar's MEASURED height (BottomTabs,
   shared-app-shell.jsx), safe-area padding included, so nothing is added for
   the inset here. Screens with no bar keep the toast's own default. */
html:not([data-surface="desktop"]):has(.bottom-tabs[data-placement="bottom"]) .app-toast,
html:not([data-surface="desktop"]):has(.bottom-tabs[data-placement="bottom"]) .shell-toast {
  /* .shell-toast too: "Sorular indirildi" covered the tab labels on YKS for
     four seconds (review 2026-10-28 L7). */
  --app-toast-bottom: calc(var(--btabs-h, 72px) + 8px);
}

/* ---------- Controls are not text (exam-parts study BUG-4, 2026-10-01) ----
   In the native app a long press on "Bu soruyu bilmiyorum" or "Devam" raised
   iOS selection handles over the label: a button's caption is chrome, never
   something to copy. One rule for every button-shaped control, so the runner,
   the diagnostic, the sealed paper and the sheets all behave alike. Inputs and
   prose are untouched. */
button, [role="button"], [role="tab"], .tap-44 {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* ---------- Text actions and the row dismiss (study EP-20) ----------------
   One link style for every quiet text action that sits in or beside an inset
   panel ("Raporunu gör · Soruları incele · Yeniden başla", "Gelişim raporu",
   "Başka bir konu seç", "AYT konuları ›"): accent ink, weight 700, an
   underline on hover so a mouse user sees it is live, and the 44pt floor.
   `.row-dismiss` is the × on a resume row: labelled by the caller
   (aria-label "Listeden kaldır" + title), set apart from the row's own target
   by a gap and a hairline, and never smaller than 44×44. The home's surfaces
   (subjects/practice/home.jsx) use the same two class names. */
.exam-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 44px;
  padding: 0;
  background: none;
  border: none;
  color: var(--accent);
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
  text-decoration: none;
  text-underline-offset: 3px;
}
.exam-link:hover:not(:disabled) { text-decoration: underline; }
.exam-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.row-dismiss {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin-left: 6px;
  padding: 0;
  background: none;
  border: none;
  border-left: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.row-dismiss:hover { color: var(--ink); background: rgba(42, 38, 32, 0.05); }
.row-dismiss:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ---------- Exam part areas (owner, 2026-10-01) -----------------------------
 * "When looking at the page you know you are in the TYT or AYT area." On a
 * one-exam market (docs/EXAM_PARTS.md §5) every part-grouped tab stacks the
 * parts in exam order; each part's block is ONE band. A SECTION treatment,
 * never a mode: nothing hides, nothing switches, and the exam's chrome
 * (topbar, tab strip, rail) keeps the exam tint (foundation.js examTintKey).
 *
 * TWO LAYERS, so the treatment is reusable on a single card:
 *   [data-part="<test>"]   the part's colour variables only
 *   .part-area             the band look: paper, 3px rule, chip re-ink,
 *                          sticky head (.part-area-head)
 *
 * The colours WERE the parts' own TEST_TINTS (TYT ochre, AYT plum), with TUS
 * borrowing the pair in exam order (Temel ochre, Klinik plum). Since
 * 2026-10-24 every test wears the one violet skin (themes.js), and the pair
 * stays here as a SECTION cue inside one app — it is no longer any test's
 * identity, so do not derive it from TEST_TINTS again.
 * Paper is the pair's own paper temperature, a step LIGHTER than the page so
 * inset panels inside keep (in fact gain) their read-only contrast:
 *   ink 13.5:1 · inkSoft 5.3:1 · ochre 4.7:1 · plum 5.8:1 on either paper;
 *   bgInset vs paper 1.13–1.15 (vs page 1.10). Warm family only — both hues
 *   are the vetted TEST_TINTS accents, ≥30° OKLCH from success and danger.
 * Single-test markets render no band, so they are byte-for-byte unchanged. */
[data-part] {
  --part-accent: var(--accent);
  --part-paper: var(--bg);
  --part-soft: var(--accent-soft);
}
[data-part="tyt"], [data-part="tus-ttbt"] {
  --part-accent: #85682F;
  --part-paper: #F9F5E8;
  --part-soft: #DFD3B4;
}
[data-part="ayt"], [data-part="tus-ktbt"] {
  --part-accent: #7D5266;
  --part-paper: #F8F1F3;
  --part-soft: #DBC9D3;
}
.part-area {
  position: relative;
  /* Bleed 10px into the gutter so the band costs the content ~3px, not 20. */
  margin-left: -10px;
  margin-right: -10px;
  padding: 0 10px 16px 7px;
  border-left: 3px solid var(--part-accent);
  border-radius: 0 var(--radius-inset, 4px) var(--radius-inset, 4px) 0;
  background: var(--part-paper);
}
/* The head pins under whatever already pins at the top (nothing on a phone
   or the desktop page — the rail is beside it — and the 48px top tab strip
   on a tablet), and leaves with its band: sticky is bounded by the section. */
.part-area-head {
  position: sticky;
  top: env(safe-area-inset-top, 0px);
  z-index: 4;
  margin: 0 -10px 10px -7px;
  padding: 12px 10px 8px 7px;
  background: var(--part-paper);
}
html[data-surface="tablet"] .part-area-head {
  top: calc(env(safe-area-inset-top, 0px) + 48px);
}
/* Two-faced card (docs/EXAM_PARTS.md §13): FOLDER TABS on the card's top
   edge, never a pill switch (owner, 2026-10-02 — a pill reads as a mode
   toggle). The tab row and the card's 3px top rule pin TOGETHER on an opaque
   page-paper strip, so content never shows beside the tabs; the active tab is
   filled with its part's accent and runs into the rule, the other is a raised
   (tappable) outlined tab in its own part's accent. */
.part-flip {
  position: relative;
  margin-left: -10px;
  margin-right: -10px;
}
.part-flip-head {
  position: sticky;
  top: env(safe-area-inset-top, 0px);
  z-index: 4;
  display: flex;
  align-items: flex-end;
  gap: 10px;
  padding: 10px 10px 0 12px;
  background: var(--bg);
  border-bottom: 3px solid var(--part-accent);
}
html[data-surface="tablet"] .part-flip-head {
  top: calc(env(safe-area-inset-top, 0px) + 48px);
}
.part-flip-tabs { display: flex; gap: 6px; align-items: flex-end; flex: none; }
.part-flip-tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 40px;
  padding: 0 16px;
  border: 1.5px solid var(--part-accent);
  border-bottom: 0;
  border-radius: 10px 10px 0 0;
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
}
html[data-surface="phone"] .part-flip-tab { min-height: 44px; }
.part-flip-tab[aria-selected="true"] {
  background: var(--part-accent);
  color: #fff;
  cursor: default;
}
.part-flip-tab[aria-selected="false"] {
  background: var(--pf-card, #FBF8F2);
  color: var(--part-accent);
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03), 0 -3px 10px rgba(0, 0, 0, 0.05);
}
html[data-surface="desktop"] .part-flip-tab[aria-selected="true"]:hover { filter: none; }
.part-flip-tab:focus-visible { outline: 2px solid var(--part-accent); outline-offset: 2px; }
.part-flip-badge {
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--part-accent);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
}
.part-flip-extra {
  min-width: 0;
  padding-bottom: 10px;
  font-size: 13px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.part-area.part-flip-face {
  margin: 0;
  border-radius: 0 0 var(--radius-inset, 4px) var(--radius-inset, 4px);
  padding: 12px 10px 16px 7px;
}
.part-area-dot {
  display: inline-block;
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--part-accent);
}
.part-area-label { color: var(--part-accent); }
/* Part-coloured chips: an icon chip (one hue — the accent glyph on the soft
   accent, home.jsx IconChip, O5) and a monochrome section glyph inside a band
   wear the PART's pair instead, so the AYT list reads differently from the
   TYT list at a glance. A translucent chip on an accent-filled lead row, and a
   muted chip on a read-only panel, keep their own (data-chip-fixed).
   !important: the chips set their fill and ink inline. */
.part-area .icon-chip:not([data-chip-fixed]) { background: var(--part-soft) !important; color: var(--part-accent) !important; }
.part-area .section-chip { color: var(--part-accent) !important; }
/* The "TYT ↓ · AYT ↓" anchors carry the same dots and ink. */
.part-jump { color: var(--part-accent) !important; gap: 6px !important; }
/* Screen only — a printed page never carries the band. */
@media print {
  .part-area { background: none; border-left: 0; padding: 0; margin-left: 0; margin-right: 0; }
  .part-area-head { position: static; background: none; }
  .part-flip-head { position: static; background: none; }
}
