/* v2 reader chrome (Phase 3). The BOOK styles live inside the iframe; this
 * file is only the frame around it: top bar, surface, progress row. */

.r2-page {
  display: grid;
  grid-template-rows: auto 1fr auto auto;
  /* Let the single column shrink below its content: without minmax(0,1fr)
     the nowrap book title sets the track's min width and the whole reader
     overflows sideways on narrow phones (found at the 320px check). */
  grid-template-columns: minmax(0, 1fr);
  height: 100vh;
  height: 100dvh;
  /* Clip to the viewport: the grid rows own their scroll (the book's iframe,
     the game's transcript). Without this, content that exceeds a row spills
     onto the body and the whole page scrolls — sliding the top bar and command
     line off screen (reported on the game). */
  overflow: hidden;
  background: var(--md-sys-color-surface);
  color: var(--md-sys-color-on-surface);
}

/* Explicit row placement: grid auto-placement assigns items by ORDER, so
   hiding the top bar (immersive) slid the surface into the auto first row
   and collapsed the book to the iframe's intrinsic 150px. Pinning each
   region to its row makes hiding any bar safe. Descendant selectors, not
   child: the landmark wrappers (main/footer) use display:contents, so grid
   items are no longer direct children. */
/* Every row child is placed in column 1 EXPLICITLY: the notice line also
   sits in row 2, column 1 (over the surface), and an item placed by row
   alone is auto-placed into the next free column beside it — which split
   the page into two columns (Stage 2 QA, read in the screenshots). */
.r2-page .r2-top { grid-row: 1; grid-column: 1; }
.r2-page .r2-surface { grid-row: 2; grid-column: 1; }
.r2-page .r2-progress { grid-row: 3; grid-column: 1; }
.r2-page .r2-transport { grid-row: 4; grid-column: 1; }

.r2-top {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: calc(0.5rem + env(safe-area-inset-top, 0px))
    calc(0.75rem + env(safe-area-inset-right, 0px))
    0.5rem
    calc(0.75rem + env(safe-area-inset-left, 0px));
  /* outline, not outline-variant: the variant measured 2.0:1 on the dark
     surface, under the 3:1 UI-component minimum (WCAG 1.4.11). */
  border-bottom: 1px solid var(--md-sys-color-outline);
}

.r2-title { min-width: 0; }
.r2-title h1 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 600;
  /* Wrap to two lines before ever ellipsizing: a one-line nowrap title cut
     real book names to a single word on the user's phone. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;   /* an unbroken token wraps, never widens the bar */
  text-wrap: wrap;   /* not balance: it computes on the UNCLAMPED text here */
}
.r2-chapter {
  margin: 0;
  font-size: 0.85rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.r2-surface { display: block; min-height: 0; }
.r2-frame { display: block; width: 100%; height: 100%; border: 0; background: transparent; }

/* Audiobooks: the same surface slot, holding cover art instead of text. The
   art is contained, never cropped or stretched, and never grows past the
   space between the bars — the chrome must stay put at any window size. */
.ap-cover-surface {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  overflow: hidden;
}
.ap-cover {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgb(0 0 0 / 0.35);
}
/* No cover on file: a plain mark rather than a broken image. */
.ap-cover-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  width: min(60vw, 16rem);
  aspect-ratio: 1;
  border-radius: 8px;
  border: 1px solid var(--md-sys-color-outline);
  background: var(--md-sys-color-surface-variant, transparent);
  color: var(--md-sys-color-on-surface);
}
.ap-cover-fallback .material-icons { font-size: 64px; }

.r2-progress {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem
    calc(1rem + env(safe-area-inset-right, 0px))
    0.5rem
    calc(1rem + env(safe-area-inset-left, 0px));
  border-top: 1px solid var(--md-sys-color-outline);
}

/* The native range track was near-invisible on the dark surface (the other
   1.4.11 failure from the contrast audit): draw it explicitly at outline
   contrast with a primary thumb, both engines. */
.r2-slider {
  flex: 1;
  min-height: 44px;
  accent-color: var(--md-sys-color-primary);
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
}
.r2-slider::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 3px;
  background: var(--md-sys-color-outline);
}
.r2-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  margin-top: -7px;
  border-radius: 50%;
  border: none;
  background: var(--md-sys-color-primary);
}
.r2-slider::-moz-range-track {
  height: 6px;
  border-radius: 3px;
  background: var(--md-sys-color-outline);
}
.r2-slider::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border: none;
  border-radius: 50%;
  background: var(--md-sys-color-primary);
}
.r2-slider:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

.r2-progress { padding-bottom: 0; border-bottom: 0; }
/* Ticking read-outs keep fixed-width digits so nothing shifts as they run. */
.r2-timeleft { white-space: nowrap; font-size: 0.9rem; font-variant-numeric: tabular-nums; }

/* The design row: unit chip left, playback centered, voice chip right.
   Under 480px it becomes two tidy rows (chips above, playback below) so the
   FULL unit name and rate always fit — no abbreviations, no clipping. */
.r2-transport {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.5rem;
  /* The bottom bar reaches the screen edge: keep it clear of the home bar
     and rounded corners (safe-area insets are 0 everywhere else). */
  padding: 0.35rem
    calc(0.75rem + env(safe-area-inset-right, 0px))
    calc(0.6rem + env(safe-area-inset-bottom, 0px))
    calc(0.75rem + env(safe-area-inset-left, 0px));
}
.r2-transport > .menu-anchor { justify-self: start; }
.r2-controls { display: flex; align-items: center; justify-content: center; gap: 0.6rem; }
.r2-transport > .r2-voice-chip { justify-self: end; }
@media (max-width: 479px) {
  .r2-transport { grid-template-columns: 1fr 1fr; }
  .r2-transport > .menu-anchor { grid-row: 1; grid-column: 1; }
  .r2-transport > .r2-voice-chip { grid-row: 1; grid-column: 2; }
  .r2-controls { grid-row: 2; grid-column: 1 / -1; }
}
/* The speed chip shows a ticking number ([ and ] change it), so its digits
   are fixed-width like every other read-out (sweep 2026-09-04). */
.r2-voice-chip { font-variant-numeric: tabular-nums; }

/* Original-layout PDFs take the SAME transport as the reflowed text (Stage
   8): the page counter simply stands where the skip-unit chip stands, so
   Play, the arrows and the voice chip keep their places in both views. Fixed
   width and tabular figures so the buttons never shift as the digits change.
   It was a one-column row of page arrows while this view could not read. */
.r2-pdf-count {
  min-width: 5.5rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
  justify-self: start;
}
@media (max-width: 479px) {
  .r2-transport > .r2-pdf-count { grid-row: 1; grid-column: 1; }
}

/* The notice line (abl-notice): spoken through its live region and SHOWN
   above the transport for a few seconds from the same string. The element
   itself takes no grid row (display: contents); the line floats. When not
   visible the text stays in the region, clipped away like .visually-hidden,
   so the region never disappears from the tree. */
/* A grid item in the SURFACE row (row 2), start-aligned and centred, so it
   sits just under the header whatever the header's height — a fixed offset
   covered the chapter line where the title wraps at 320 (Stage 2 QA). The
   bottom of the screen is out: it holds the progress row, the transport and
   the immersive show-controls button (Stage 2 gate). */
.r2-notice {
  grid-row: 2;
  grid-column: 1;
  justify-self: center;
  align-self: start;
  z-index: 6;
  pointer-events: none;
  padding-top: 0.5rem;
  max-width: min(90vw, 40rem);
}
.r2-notice .notice-line {
  margin: 0;
  padding: 0.5rem 0.9rem;
  border-radius: 0.6rem;
  background: var(--md-sys-color-inverse-surface, #313033);
  color: var(--md-sys-color-inverse-on-surface, #f4eff4);
  font-size: 0.95rem;
  line-height: 1.35;
  text-wrap: balance;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.r2-notice .notice-line.is-alert {
  background: var(--md-sys-color-error-container, #8c1d18);
  color: var(--md-sys-color-on-error-container, #f9dedc);
}
/* The notice's action (Undo): a real button on the line, so it takes the
   pointer the line itself refuses, at a 44px target, in the line's colours
   (its border and text are currentColor, so it holds contrast on both the
   status and the alert background). */
.r2-notice .notice-action {
  pointer-events: auto;
  margin-inline-start: 0.6rem;
  min-height: 44px;
  min-width: 44px;
  padding: 0.3rem 0.8rem;
  border: 1px solid currentColor;
  border-radius: 999px;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.r2-notice .notice-action:hover { background: color-mix(in srgb, currentColor 15%, transparent); }
/* The ring in the line's own text colour: the primary colour over the
   inverse surface fell below 3:1 in both themes (gate). */
.r2-notice .notice-action:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 2px;
}
/* A user's own words in a dialog heading (a rule pattern, a long title)
   wrap rather than widen the dialog at 320. */
abl-confirm-dialog [slot="headline"], abl-field-dialog [slot="headline"] { overflow-wrap: anywhere; }
.r2-notice .notice-line:not(.is-visible) {
  position: absolute;
  width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap;
  box-shadow: none;
}
/* Immersive: the header row is gone, so the same grid placement puts the line
   at the top edge (the show-controls button lives at the bottom right). */
.r2-immersive .r2-notice { padding-top: calc(0.75rem + env(safe-area-inset-top, 0px)); }

.r2-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font: inherit;
  color: inherit;
  background: none;
  border: 1px solid var(--md-sys-color-outline);
  border-radius: 999px;
  padding: 0.35rem 0.6rem;
  min-height: 44px;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;   /* the IF player's ended-game chip is a link to the shelf */
}
/* Its own hover: the shell's button:hover never reaches an <a> chip. */
.r2-chip:hover:not(:disabled) { background: color-mix(in srgb, currentColor 10%, transparent); }
.r2-chip:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.r2-chip:disabled { opacity: 0.5; cursor: default; }
.r2-unit { flex-shrink: 0; }

.r2-skip { min-width: 44px; min-height: 44px; }
.r2-skip .material-icons { font-size: 26px; }

.r2-play {
  min-width: 64px;
  min-height: 64px;
  border-radius: 999px;
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
}
.r2-play .material-icons { font-size: 36px; }
.r2-play:hover:not(:disabled) { background: color-mix(in srgb, var(--md-sys-color-on-primary-container) 10%, var(--md-sys-color-primary-container)); }
.r2-play:disabled { opacity: 0.5; }
/* Preparing state: the instant Play is pressed the button shows an
   hourglass and pulses, and a visible line under the transport says what is
   happening, until audio actually starts. Nobody should see a Play button
   that "did nothing" while the voice list, the prepare call and the first
   synthesis run (user-reported: three presses). The pulse is a SCALE, not
   an opacity fade: fading the glyph dropped it under 4.5:1 mid-beat (the
   baseline reader-playing contrast FAIL). .r2-reduce stops the pulse. */
.r2-play[aria-busy="true"] .material-icons { animation: r2-pulse 1s ease-in-out infinite; }
@keyframes r2-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(0.8); } }
.r2-tts-status { grid-column: 1 / -1; margin: 0; text-align: center; font-size: 0.95rem; }

.vs-note { font-size: 0.9rem; margin: 0; }

@media (max-width: 400px) {
  .r2-transport { gap: 0.3rem; padding-left: 0.3rem; padding-right: 0.3rem; }
  .r2-chip { padding: 0.3rem 0.45rem; font-size: 0.9rem; }
  .r2-play { min-width: 56px; min-height: 56px; }
  .r2-play .material-icons { font-size: 32px; }
}

.r2-percent-label { display: inline-flex; align-items: center; gap: 0.25rem; }
/* The percent box's own error line: beside the box, full contrast. */
.r2-field-err { font-size: 0.9rem; font-weight: 600; color: var(--md-sys-color-error); }
.r2-field-err:empty { display: none; }
.r2-percent {
  width: 3.5rem;
  min-height: 40px;
  text-align: right;
  font: inherit;
  font-variant-numeric: tabular-nums;   /* AFTER the shorthand, which resets it */
  color: inherit;
  background: var(--md-sys-color-surface-container, transparent);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: 6px;
  padding: 0 0.4rem;
}
.r2-percent:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

.r2-map-btn { margin-left: auto; }
.r2-map { display: contents; }
/* Top-bar icons carry an explicit on-surface colour: inherit over the
   open-menu tint measured below 4.5:1 (QA axe: #r2-more-btn icon). */
.r2-top .icon-btn, #r2-more-btn { color: var(--md-sys-color-on-surface); }

.map-toc { list-style: none; margin: 0; padding-left: 0; }
.map-toc .map-toc { padding-left: 1.25rem; }
/* Long lists (a page list is hundreds of rows) lay out and paint only what is
   near the scrollport. auto, never hidden: the rows stay in the accessibility
   tree, and focus or a geometry query renders one on demand. The intrinsic
   size is the remembered row height once a row has been seen.
   Paint containment clips ink at the row's PADDING edge — without the padding
   below, every focus ring in these rows vanished (gate, reviewer A, verified
   in Chromium). The negative margins cancel it, so nothing moves. */
.map-toc li, .map-hits li {
  content-visibility: auto;
  contain-intrinsic-size: auto 3rem;
  padding: 5px 6px;
  margin: -5px -6px;
}
.map-toc-item {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  border-radius: 6px;
  padding: 0.55rem 0.5rem;
  min-height: 44px;
  cursor: pointer;
  overflow-wrap: anywhere;   /* long chapter names wrap inside the dialog */
}
.map-toc-item:hover { background: var(--md-sys-color-surface-container-high, rgba(127,127,127,0.15)); }
.map-toc-item:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.map-toc-item:disabled { opacity: 0.55; cursor: default; }

/* Wrap, never overflow: at 320 the third tab was clipped at the dialog edge
   and tapping it scrolled the row, cutting the list labels off on the left. */
.map-tabs { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; }
.map-tab {
  font: inherit;
  color: inherit;
  background: none;
  border: 1px solid var(--md-sys-color-outline);
  border-radius: 999px;
  padding: 0.4rem 1rem;
  min-height: 40px;
  cursor: pointer;
}
.map-tab[aria-selected="true"] {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  border-color: transparent;
}
.map-tab:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

.map-search-row { display: flex; gap: 0.5rem; align-items: center; }
/* min-width: 0 or the field's intrinsic input width becomes its floor and
   the row overflows a 320px dialog (sweep 2026-09-04). */
.map-search-row md-outlined-text-field { flex: 1; min-width: 0; }
/* The Contents tab's generated-TOC note (v1 parity). Full contrast (no
   dimmed text, ever): it is information; only the size differs. */
.map-gen-note { margin: 0 0 0.5rem; font-size: 0.85rem; }
/* G3 color toggles inside the annotation editor. */
.map-edit-colors { display: flex; gap: 0.5rem; margin: 0.5rem 0; }
.map-edit-colors .r2-sel-color[aria-pressed="true"] {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.map-hits { list-style: none; margin: 0.75rem 0 0; padding: 0; }
/* Full contrast (no dimmed text, ever): the chapter name is information,
   only the size differs. */
.map-hit-chapter { display: block; font-size: 0.8rem; }
.map-hit-text { display: block; }

/* min() keeps the grid from forcing a dialog wider than a 320 px screen. */
.rs-grid { display: grid; gap: 1rem; min-width: min(18rem, 100%); }
/* Speed slider (voice sheet + reader settings). A constant "Speed" label with
   a live value read-out beside it; the slider itself announces its value, so
   the read-out is aria-hidden to avoid saying the number twice. */
.vs-speed { display: flex; flex-direction: column; gap: 0.25rem; }
.vs-speed-head { display: flex; justify-content: space-between; align-items: baseline; }
.vs-speed-label { font-size: 1rem; color: var(--md-sys-color-on-surface); }
.vs-speed-val { font-size: 1rem; font-weight: 700; color: var(--md-sys-color-on-surface); font-variant-numeric: tabular-nums; }
.vs-speed-slider { width: 100%; --md-slider-active-track-color: var(--md-sys-color-primary); }
/* Voice picker: a labelled button opening ONE menu of engines whose voices
   expand inline under the engine row. The button stretches to the sheet's
   width so a long "name · engine" reads on one line where there is room and
   wraps where there is not; the menu is fitted to the viewport by Material
   and scrolls as one list. */
.voice-menu { display: flex; flex-direction: column; gap: 0.25rem; position: relative; }
.voice-menu-label { font-size: 1rem; color: var(--md-sys-color-on-surface); }
/* The picker button is a native button that WRAPS its text: a long
   "name · engine" inside the Voice sheet must fold onto two lines, not run
   off the edge of the dialog. Outlined like the Material buttons beside it. */
.voice-menu-btn {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; max-width: 100%; min-height: 48px; padding: 10px 12px 10px 16px;
  font: inherit; text-align: left; white-space: normal; overflow-wrap: anywhere;
  color: var(--md-sys-color-primary); background: transparent;
  border: 1px solid var(--md-sys-color-outline); border-radius: 12px; cursor: pointer;
}
.voice-menu-btn:focus-visible { outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; }
.voice-menu-text { flex: 1 1 auto; }
.voice-menu md-menu { --md-menu-container-shape: 12px; min-width: 16rem; max-width: calc(100vw - 24px); }
/* Phones: the same menu, docked to the bottom of the screen as a sheet (the
   M3 menus spec's adaptation for compact screens) but still a MENU to a
   screen reader. The anchor is a zero-size point fixed at the bottom-left;
   the menu hangs its bottom-left corner on it and grows upward. */
.voice-sheet-anchor { position: fixed; left: 0; bottom: 0; width: 0; height: 0; }
.voice-menu .voice-sheet-menu {
  min-width: 100vw;
  max-width: 100vw;
  max-height: 92vh;
  --md-menu-container-shape: 28px 28px 0 0;
}
.voice-engine { --md-menu-item-label-text-weight: 700; }
/* Voices sit under their engine, indented, so the two levels read as two
   levels with no colour needed. */
.voice-inline { --md-menu-item-leading-space: 40px; }
/* An expanded engine can hold 100+ voice rows in one menu: paint only the
   ones near the scrollport (same rules as the map lists above). */
md-menu-item.voice-inline {
  content-visibility: auto;
  contain-intrinsic-size: auto 4.5rem;
}
.voice-close { border-top: 1px solid var(--md-sys-color-outline-variant); }
.voice-menu md-menu-item .material-icons { font-size: 22px; }
/* Choice menu (abl-choice-menu): the voice picker's shape for a flat list of
   options — the shelf's Bookshelf and Sort by, which were select dropdowns
   applying on change (WCAG 3.2.2). The label and the current choice both sit
   inside the button so a toolbar stays one row; the menu, the sheet and the
   Close row are the voice menu's. */
abl-choice-menu { display: block; }
.choice-menu { position: relative; }
.choice-menu-btn {
  display: flex; align-items: center; gap: 8px;
  width: 100%; max-width: 100%; min-height: 48px; padding: 10px 12px 10px 16px;
  font: inherit; text-align: left; white-space: normal; overflow-wrap: anywhere;
  color: var(--md-sys-color-primary); background: transparent;
  border: 1px solid var(--md-sys-color-outline); border-radius: 12px; cursor: pointer;
}
.choice-menu-btn:focus-visible { outline: 3px solid var(--md-sys-color-secondary); outline-offset: 2px; }
/* The label is full-size on-surface text, never dimmed (project rule). */
/* The label never breaks from its colon: in a squeezed flex row (the discover
   Topic menu) "Topic" and ":" landed on two lines (Stage 3 screenshots). */
.choice-menu-label { color: var(--md-sys-color-on-surface); white-space: nowrap; flex: none; }
.choice-menu-label::after { content: ':'; }
.choice-menu-text { flex: 1 1 auto; font-weight: 700; }
.choice-menu md-menu { --md-menu-container-shape: 12px; min-width: 14rem; max-width: calc(100vw - 24px); }
.choice-menu .choice-sheet-menu {
  min-width: 100vw;
  max-width: 100vw;
  max-height: 92vh;
  --md-menu-container-shape: 28px 28px 0 0;
}
.choice-close { border-top: 1px solid var(--md-sys-color-outline-variant); }
.choice-menu md-menu-item .material-icons { font-size: 22px; }
/* Project rule: text is never dimmed — Material's on-surface-variant
   floating labels measured 3.99:1 in light mode (axe sweep). */
md-outlined-select, md-outlined-text-field {
  --md-outlined-field-label-text-color: var(--md-sys-color-on-surface);
}
.rs-section { margin: 0.5rem 0 0; font-size: 1rem; }
.rs-toggle { display: flex; gap: 0.5rem; align-items: center; min-height: 44px; }
.rs-toggle input { width: 20px; height: 20px; accent-color: var(--md-sys-color-primary); }
/* One clear ring around the WHOLE row, not a small one on the 20px box.
   The inner removal is gated on :has support: without the gate, a browser
   that lacks :has would strip the box's ring and draw no replacement
   (gate 2026-08-31). */
.rs-toggle:has(input:focus-visible), .r2-kbd-toggle:has(input:focus-visible) {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
  border-radius: 8px;
}
@supports selector(:has(a)) {
  .rs-toggle input:focus-visible, .r2-kbd-toggle input:focus-visible { outline: none; }
}
.rs-colors { display: flex; gap: 1rem; flex-wrap: wrap; }
.rs-color { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.9rem; }
.rs-color input { width: 56px; height: 44px; border: 1px solid var(--md-sys-color-outline);
                  border-radius: 6px; background: none; padding: 2px; }
#rs-contrast { margin: 0; font-size: 0.9rem; }
.rs-contrast-warn { color: var(--md-sys-color-error); }

/* Selection popover: color dots + Add note + Read from here over the
   selected text. Wraps and stays inside the viewport: at 320-400 px the
   Read button was pushed past the right screen edge (Stage 1 eyes-on). */
.r2-sel-pop {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  max-width: calc(100vw - 16px);
  border-radius: 1.25rem;
  background: var(--md-sys-color-surface-container-high, #2b2930);
  border: 1px solid var(--md-sys-color-outline);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  z-index: 6;
}
.r2-sel-color {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 2px solid var(--md-sys-color-outline);
  cursor: pointer;
  transition: transform 0.1s ease;
}
/* Hover must survive the inline swatch background: grow and ring instead
   of a background change the inline style would beat. */
.r2-sel-color:hover { transform: scale(1.15); border-color: var(--md-sys-color-primary); }
.r2-sel-note:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
.r2-sel-color:focus-visible, .r2-sel-note:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.r2-sel-note {
  font: inherit;
  color: inherit;
  background: none;
  border: 1px solid var(--md-sys-color-outline);
  border-radius: 999px;
  padding: 0.3rem 0.7rem;
  min-height: 32px;
  cursor: pointer;
  white-space: nowrap;
}

.r2-note-input {
  width: 100%;
  min-width: min(16rem, 100%);
  font: inherit;
  color: inherit;
  background: var(--md-sys-color-surface-container-low, transparent);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: 6px;
  padding: 0.5rem;
}

.r2-pron-label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.95rem; }
/* The error under the box it names (STANDARDS, Forms), for the native inputs
   the reader's own dialogs keep; Material fields carry their own. */
.r2-field-error { color: var(--md-sys-color-error); font-weight: 600; font-size: 0.9rem; }
.r2-pron-select {
  font: inherit;
  color: inherit;
  background: var(--md-sys-color-surface-container-low, transparent);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: 6px;
  min-height: 44px;
  padding: 0 0.5rem;
}
.r2-pron-list { list-style: none; margin: 0; padding: 0; }
.r2-pron-row { display: flex; align-items: center; gap: 0.5rem; min-height: 44px;
  /* A reader can hold hundreds of pronunciation rules; only the rows on
     screen are painted (same rule as .voice-row). */
  content-visibility: auto; contain-intrinsic-size: auto 44px; }
.r2-pron-desc { flex: 1; min-width: 0; overflow-wrap: anywhere; }

.map-ann-controls { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem; }
.map-ann-filter { display: flex; gap: 0.4rem; align-items: center; }
.map-ann-filter select, .map-ann-search {
  font: inherit;
  color: inherit;
  background: var(--md-sys-color-surface-container-low, transparent);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: 6px;
  min-height: 40px;
  padding: 0 0.5rem;
}
.map-ann-search { flex: 1; min-width: 8rem; }
/* The two export DOWNLOAD links, dressed as the text buttons they replaced:
   same height and colour, underlined on hover so they still read as links. */
.map-export {
  display: inline-flex;
  align-items: center;
  /* inline-flex turns the label's text nodes into anonymous items and eats
     the space before the format span ("ExportJSON" — Stage 2 eyes-on); the
     gap puts the word space back. */
  gap: 0.3ch;
  min-height: 40px;
  padding: 0 0.75rem;
  border-radius: 20px;
  color: var(--md-sys-color-primary);
  font: inherit;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}
/* The export links move as one unit where they fit; with three of them
   (by-tag joined in Stage 2) the unit itself must wrap inside narrow
   dialogs instead of running off the edge (eyes-on). */
.map-exports { display: inline-flex; flex-wrap: wrap; gap: 0.25rem; flex: 0 1 auto; min-width: 0; }
.map-export:hover { text-decoration: underline; }
.map-export:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.map-note-edit { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
/* The tags field grows into the row's free space beside Save/Cancel and
   wraps only when it must — a full-row basis pushed the buttons below the
   panel fold, and a fixed one squeezed it to ~12 characters (eyes-on). */
.map-note-edit .map-ann-search { flex: 1 1 10rem; }
.map-note-body { font-style: italic; }

/* Reading ruler: a tinted band on the pointer's reading line (v1 #abr-ruler,
   ported; pointer-events none so it never blocks a tap). */
.r2-ruler {
  position: fixed;
  left: 0;
  right: 0;
  height: 48px;
  background: color-mix(in srgb, var(--md-sys-color-primary) 16%, transparent);
  border-top: 1px solid color-mix(in srgb, var(--md-sys-color-primary) 45%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--md-sys-color-primary) 45%, transparent);
  pointer-events: none;
  z-index: 4;
}

/* In-app reduce motion kills chrome transitions too, not just JS motion
   (v1 abr-reduce, ported). */
.r2-reduce, .r2-reduce * { transition: none !important; animation: none !important; }

/* Touch behaviour on the reader's own controls: no double-tap zoom delay,
   and a deliberate tap flash in the app's own colour rather than the
   platform grey. Sheets and menus keep their scroll to themselves. */
.r2-chip, .r2-skip, .r2-play, .map-toc-item, .map-tab, .r2-sel-color, .r2-sel-note,
.r2-notice .notice-action, .map-export, .r2-percent, .index-letter, #if-cmd, .if-keywait {
  touch-action: manipulation;
  -webkit-tap-highlight-color: color-mix(in srgb, var(--md-sys-color-primary) 20%, transparent);
}
/* overscroll-behavior works only on the element that scrolls, and Material's
   dialog and menu scrollers live in closed shadow roots a page rule cannot
   reach (a host rule is inert — gate 2026-08-31). The real scrollers here
   (.if-surface, .shelf-index) carry it; the body scroll lock
   (core/popup-scroll-lock.ts) is what actually pins the page behind every
   Material overlay. */

/* Auto-immersive while listening: the chrome leaves entirely (display:none =
   out of the accessibility tree), a progress hairline and a faded escape
   hatch remain. */
.r2-immersive .r2-top,
.r2-immersive .r2-progress,
.r2-immersive .r2-transport { display: none; }
.r2-hairline {
  position: fixed;
  left: 0;
  bottom: env(safe-area-inset-bottom, 0px);
  height: 4px;
  background: var(--md-sys-color-primary);
  pointer-events: none;
  z-index: 5;
}
/* Unobtrusive through a translucent BACKGROUND, not element opacity: at
   opacity 0.4 the icon and border of this enabled control sat under the
   3:1 non-text minimum (WCAG 1.4.11); the glyph and ring stay full. */
.r2-hatch {
  position: fixed;
  right: calc(0.75rem + env(safe-area-inset-right, 0px));
  bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--md-sys-color-outline);
  background: color-mix(in srgb, var(--md-sys-color-surface-container-high, #2b2930) 75%, transparent);
  color: var(--md-sys-color-on-surface);
  /* A dual boundary that reads on ANY page colour: a dark outer ring for
     light pages, the light border for dark ones (the outline-variant border
     alone measured 2:1 against the chip's own fill — gate arithmetic). */
  box-shadow: 0 0 0 1px rgb(0 0 0 / 0.5);
  cursor: pointer;
  z-index: 6;
}
.r2-hatch:hover, .r2-hatch:focus-visible { background: var(--md-sys-color-surface-container-high, #2b2930); }
.r2-hatch:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

.map-note-row { display: flex; align-items: center; gap: 0.5rem; }
.map-note-row .map-toc-item { flex: 1; min-width: 0; }

.r2-kbd-toggle { display: flex; gap: 0.5rem; align-items: center; min-height: 44px; }
.r2-kbd-toggle input { width: 20px; height: 20px; accent-color: var(--md-sys-color-primary); }
.r2-kbd-note { font-size: 0.9rem; margin: 0.25rem 0 0.75rem; }
.r2-kbd-table { border-collapse: collapse; }
.r2-kbd-table th { text-align: left; padding: 0.35rem 0.75rem 0.35rem 0; white-space: nowrap; vertical-align: top; }
.r2-kbd-table td { padding: 0.35rem 0; }

.r2-error {
  display: grid;
  place-content: center;
  gap: 1rem;
  min-height: 100vh;
  min-height: 100dvh;   /* the URL bar's collapse must not clip the message */
  text-align: center;
}

/* ── Interactive fiction player ──────────────────────────────────────────
   The transcript sits in the reader's surface slot; the command line takes
   the two transport rows, sticky at the bottom so it never scrolls away. */
/* Pin the whole IF interface to the viewport so the CHROME can never scroll.
   height:100dvh + overflow:hidden only CLIPS; a real scroll/overscroll gesture
   still moves the document and drags the interface with it (the reported bug).
   Fix the HOST element (a custom element, otherwise inline with no intrinsic
   height): display:block gives it a real box, position:fixed + inset:0 sizes it
   to the viewport and makes it immovable regardless of document scroll. Only the
   inner .if-surface scrolls. Scoped to the IF host, so the book reader is
   untouched. */
abl-if-player {
  display: block;
  position: fixed;
  inset: 0;
}
/* The transcript is the ONLY scrolling region: min-height:0 lets the grid's
   1fr row shrink below content so it scrolls inside .if-surface instead of the
   page. overscroll-behavior:contain stops its scroll from chaining to the
   document at the top/bottom. It carries the chosen reading theme (var set on
   it by applyReadingVars, painted by the shared [data-r2-theme] palettes), so
   the game gets the same sepia/dark/contrast reading backgrounds as the book. */
.if-page .if-surface {
  display: block;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
  background: var(--r2-bg, var(--md-sys-color-surface));
  color: var(--r2-fg, var(--md-sys-color-on-surface));
}
.if-transcript {
  max-width: 70ch;
  margin-inline: auto;
  /* Typography from the reader's own settings variables (size/line/spacing/
     margin/align), identical to the book. */
  padding: 0.75rem var(--r2-margin, 6%) 1rem;
  font-size: calc(17px * var(--r2-scale, 1));
  line-height: var(--r2-line, 1.6);
  letter-spacing: var(--r2-letter, 0em);
  word-spacing: var(--r2-word, 0em);
  text-align: var(--r2-align, left);
}
/* Force the chosen face only when one IS chosen (publisher default otherwise),
   mirroring the book's html[data-r2-font] rule. */
.if-surface[data-r2-font] .if-transcript,
.if-surface[data-r2-font] .if-transcript *:not(.material-icons) { font-family: var(--r2-font); }
/* Never let the reading-font override reach icon glyphs, or the ligature
   renders as the literal word (e.g. "volume_up"). */
.if-transcript .material-icons { font-family: 'Material Icons'; }
.if-transcript a[href] { color: var(--r2-link, var(--md-sys-color-primary)); text-decoration: underline; }
.if-transcript:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: -2px;
}
/* A long session's transcript is unbounded; old turns above the scrollport
   skip layout and paint. auto keeps them in the accessibility tree, and the
   read-aloud surface's rect queries render a sought turn on demand. The
   horizontal padding (cancelled by the margins) keeps a flush link's focus
   ring from clipping at the turn's paint boundary. */
.if-turn {
  margin: 0 -5px;
  padding: 0 5px;
  content-visibility: auto;
  contain-intrinsic-size: auto 8rem;
}
/* Containment stops line margins collapsing ACROSS turns (each turn is its
   own formatting context now): zero the first block's top margin or every
   turn boundary doubles to 1.2em. */
.if-turn > :first-child { margin-block-start: 0; }
.if-turn > .if-turn-body:first-child > :first-child { margin-block-start: 0; }
.if-line { margin-block: calc(0.6em * var(--r2-para, 1)); }
.if-echo { margin: 0.6rem 0 0.3rem; font-weight: 600; }
.if-header { font-size: 1.2rem; margin: 0.8rem 0 0.4rem; }
.if-subheader { font-size: 1.05rem; margin: 0.7rem 0 0.35rem; }
.if-pre { font-family: ui-monospace, monospace; white-space: pre-wrap; margin: 0 0 0.6rem; overflow-x: auto; }
.if-blank { height: 0.6rem; }
.if-alert { color: inherit; }
.if-note { }   /* full contrast: game notes are content, never dimmed */
.if-quote { font-style: italic; }
.if-clear { border-top: 2px solid var(--md-sys-color-outline); margin: 1rem 0; }

/* The transport row (row 3) carries the Speech toggle and the reader's own
   voice-and-speed chip; the command line (row 4) sits below it, at the bottom
   where it is easiest to reach — the same shape as the audiobook transport. */
.if-page .if-transport {
  grid-row: 3;
  grid-column: 1;   /* explicit: the notice line shares the grid (see .r2-page) */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem
    calc(0.75rem + env(safe-area-inset-right, 0px))
    0.35rem
    calc(0.75rem + env(safe-area-inset-left, 0px));
  border-top: 1px solid var(--md-sys-color-outline);
}
.if-speak-chip { flex-shrink: 0; }
.if-page .if-input-bar {
  grid-row: 4;
  grid-column: 1;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem
    calc(0.75rem + env(safe-area-inset-right, 0px))
    calc(0.5rem + env(safe-area-inset-bottom, 0px))
    calc(0.75rem + env(safe-area-inset-left, 0px));
  border-top: 1px solid var(--md-sys-color-outline);
  position: sticky;
  bottom: 0;
  background: var(--md-sys-color-surface);
}
.if-cmd-form { display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0; }
#if-cmd {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  font: inherit;
  color: inherit;
  background: var(--md-sys-color-surface-container, transparent);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: 8px;
  padding: 0 0.6rem;
}
#if-cmd:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 1px;
}
.if-keywait { min-height: 44px; }
.if-ended, .if-wait { margin: 0; }
.if-status { white-space: normal; font-variant-numeric: tabular-nums; }   /* score/turn tick without jitter */

/* Read-aloud highlighting over the transcript (CSS Custom Highlight API): the
   spoken sentence gets a soft tint, the current word a solid mark — the same
   look as the book reader. */
::highlight(abl-if-sentence) {
  background-color: color-mix(in srgb, var(--r2-hl-bg, #ffd700) 30%, transparent);
}
::highlight(abl-if-word) {
  background-color: var(--r2-hl-bg, #ffd700);
  color: var(--r2-hl-fg, #1a1a1a);
}
/* The current-turn card follows the READING THEME itself — its background and
   text come straight from the theme vars, so it is the very thing that carries
   the theme (sepia/light/dark/contrast/custom) rather than the app's chrome
   colours. The read-aloud word/sentence highlight is the "currently reading"
   cue, so the card needs no separate tint. */
.if-turn.if-reading {
  background: var(--r2-bg, var(--md-sys-color-surface));
  color: var(--r2-fg, var(--md-sys-color-on-surface));
  border-radius: 8px;
  padding-inline: 0.4rem;
}
.if-turn-body { display: contents; }

/* The read controls in the transport row: the skip-unit rotor, then
   repeat / play-pause / next. */
.if-read-controls { display: flex; align-items: center; gap: 0.35rem; justify-self: center; }

/* Skip-unit rotor (Sentence / Line / Turn): a chip like the book reader's
   #r2-unit-btn, sitting with the read controls so Back and Next step by the
   chosen unit across the whole transcript. */
.if-unit-anchor { display: inline-flex; flex-shrink: 0; }
.if-unit { flex-shrink: 0; gap: 0.1rem; }
.if-unit .material-icons { margin-inline-start: -0.1rem; }
.if-play { min-width: 48px; min-height: 48px; border-radius: 999px;
  background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); }
.if-play .material-icons { font-size: 28px; }
.if-skip { min-width: 44px; min-height: 44px; }
.if-page .if-transport > .if-speak-chip { justify-self: start; }
.if-page .if-transport > .if-read-controls { justify-self: center; }
.if-page .if-transport > .r2-voice-chip { justify-self: end; }
/* Narrow screens: two columns, chips share row 1 (both stay visible), the
   play/repeat/next controls drop to a full-width row 2. Scoped to
   .if-page so it outranks the base .if-page .if-transport grid. */
@media (max-width: 560px) {
  .if-page .if-transport { grid-template-columns: 1fr 1fr; }
  .if-page .if-transport > .if-speak-chip { grid-row: 1; grid-column: 1; justify-self: start; }
  .if-page .if-transport > .r2-voice-chip { grid-row: 1; grid-column: 2; justify-self: end; }
  .if-page .if-transport > .if-read-controls { grid-row: 2; grid-column: 1 / -1; justify-self: center; }
}
