/*
 * apparatus.css — the one global stylesheet.
 *
 * Carries the Apparatus design-system tokens (ported from
 * design/recur-apparatus.html, and since moved on from it — the studies there
 * are frozen; design/recur-design-system.md is the palette's live home) plus
 * the component classes shared across every surface: masthead, route nav,
 * shared buttons. Per-surface stylesheets
 * (library.css, read.css, …) are eager-imported in surfaces.ts and hold only
 * what is specific to that surface, scoped under `.ap-surface[data-surface=…]`.
 *
 * Plain CSS — no preprocessor, no modules, no scoping machinery (spec 07).
 * Square corners everywhere; hairlines from the rule tokens; one accent.
 */

:root {
  color-scheme: light dark;

  /* Colour — the same instrument by day and at night (#99). Each token is ONE
   * declaration carrying both schemes via `light-dark()`. A
   * `@media (prefers-color-scheme: dark)` block redeclaring the set would be a
   * second copy of the palette to keep in step, and a manual override later
   * would need a third; with this shape the override is one line — set
   * `color-scheme: light` or `dark` on the root element.
   *
   * How the dark column was derived, and why four tokens deviate from that
   * derivation: design/recur-design-system.md §2, which owns the reasoning and
   * the ratios. Its § Color table is pinned to these values by
   * test/palette.test.ts (#121); the derivation prose under that table is not,
   * so moving a value here means updating those figures by hand. Properties
   * the values must hold are asserted in the same test. Only the notes that
   * matter AT THE POINT OF USE are repeated here. */
  --paper: light-dark(#fbfaf7, #1c1b17);
  --plate: light-dark(#fcfbf7, #252420);
  --ink: light-dark(#1a1a17, #ededeb);
  --prose-ink: light-dark(#242019, #e8e5de); /* reading-body ink — a hair warmer than --ink, for the reader's own prose */
  --muted: light-dark(#5e5b54, #a5a196);
  --faint: light-dark(#716d64, #938e83); /* both columns lifted to clear 4.5:1 on every ground they paint on, --muted with them to keep the step (#103 light, #111 dark) */
  --rule: light-dark(#e4dfd2, #38352e);
  --rule-strong: light-dark(#d3cdbe, #4c483e);
  --accent: light-dark(#1f4c6b, #75afd7); /* one accent, hue 204° in both — re-anchored to a dark ground, not a second hue */
  --accent-ink: light-dark(#153a52, #a8cde6); /* accent hover: darker in light, LIGHTER in dark — the direction inverts */
  --accent-wash: light-dark(#ecf1f4, #1c2a33);
  --on-accent: light-dark(#f7fafb, #11181d); /* ink on an accent-filled surface (button hover) */
  --good: light-dark(#3c6b54, #569a79);
  --warn: light-dark(#b85400, #f28c38); /* a status colour beside --good, not a second accent */

  /* Motion. The receding chrome's slide (#653): the masthead here and the read surface's
   * reading bar in read.css move on this one timing, so they cannot drift apart. */
  --ap-chrome-slide: 0.2s ease-out;

  /* Type. */
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Charter, Georgia, serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
  /* THE CHROME'S VOICE (#711) — the quiet sans everything the APP says is set in:
   * the nav, a label, a button, a row, and a model's words. `system-ui` first, so
   * the app speaks in the face the device already uses for its own interface and
   * nothing is fetched for it.
   *
   * NOT `--os-face`, which holds the same families today and is a different thing:
   * that one is a reading FACE the reader can pick, and it has to go on meaning
   * "whatever this OS is set in" on the day the chrome moves to a face of its own.
   * Two tokens because they can diverge, not because they differ now. */
  --ui-face: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* The eight faces a reader can put the reading column in instead of the serif
   * (#351, #371, #387), in three groups. THE FIRST TWO name faces the DEVICE
   * already has: the sans is a stack of the usual suspects, and `--os-face` hands
   * the column to whatever the OS itself is set in, which is where a reader who
   * has already chosen a system-wide accessible face has said what they want.
   * Both always resolve, because both end in a generic. */
  --sans: "Avenir Next", Avenir, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --os-face: system-ui, sans-serif;
  /* The next five are BUNDLED — OFL families served from this origin as woff2
   * (#371, #387), declared by the `@font-face` blocks below and precached with
   * the shell. Still no external request: the files are ours, same-origin, which
   * is why `default-src 'self'` already covers them and `public/_headers` did not
   * have to move. Where each file came from, what its licence requires and what
   * the payload costs: `public/fonts/PROVENANCE.txt`.
   *
   * EVERY ONE IS A STACK, NOT A BARE FAMILY, and the tail is a `var()` so the
   * device stacks above are not written down twice. Two things need the tail:
   * browsers fall back PER GLYPH, so Literata's Latin subset sets Greek in the
   * designed serif instead of tofu; and if a woff2 ever fails to load, the prose
   * still sets in something chosen rather than in the UA's default. */
  --literata: "Literata", var(--serif);
  --atkinson: "Atkinson Hyperlegible", var(--sans);
  --opendyslexic: "OpenDyslexic", var(--sans);
  --lexend: "Lexend", var(--sans);
  --excalifont: "Excalifont", var(--serif);
  /* And the last two are SYSTEM FACES THAT CARRY NO FILES (#387). There is no
   * `@font-face` for either, no directory under `public/fonts/`, and no request
   * to anywhere — the device holds these under its own licence, which is exactly
   * what leaves us with no licence obligation to meet. Apple's downloadable
   * fonts are not redistributable, so bundling New York was never available.
   *
   * Which means EITHER MAY BE ABSENT, and an absent one is invisible here: an
   * unknown family and an unimplemented generic are both simply skipped, so the
   * tail paints and the reader gets the designed serif under a button promising
   * something else. `faceAvailability.ts` is what keeps that button off the
   * screen; the tails below are what make the failure harmless if it ever gets
   * there.
   *
   * `ui-serif` and not a family name, because New York is installed as
   * `.New York` — a dot-prefixed hidden system family no CSS name reaches. The
   * generic is the only route, and only WebKit implements it. */
  --charter: Charter, var(--serif);
  --newyork: ui-serif, var(--serif);

  /* THE READING DIALS (#351) — the reading face, its size, and its leading, each
   * as the DESIGNED value plus a whole number of steps either side of it. The
   * reader's preference is one integer per dial, written onto the root element
   * through the CSSOM (`appearance.ts`); the designed centre and the size of one
   * step never leave this file, and neither is a fallback for a missing
   * preference — an unset dial is `0` and resolves to exactly what shipped.
   *
   * How far a dial may travel is NOT here: the popover has to disable a stepper
   * at its bound, so the bounds live where the control can read them
   * (`storage/readingStyle.ts › DIALS`). */
  /* LITERATA IS THE DESIGNED FACE (ADR 0045). A bundled family, so the designed
   * typography is the same on every device instead of resolving to whatever serif
   * a phone happens to hold. The stack's tail is the device serif, unchanged: a
   * woff2 that has not landed yet still paints something chosen. */
  --reading-face: var(--literata);
  --reading-size: calc(19px + var(--reading-size-steps, 0) * 1px);
  --reading-leading: calc(1.72 + var(--reading-leading-steps, 0) * 0.06);
  /* WHAT THE DIALS REACH, said once here because it is a decision and not a fallout.
   * They are declared on `:root` and consumed by `body` and by the shared essay title,
   * so the app's reading type moves as ONE THING rather than the reading surface
   * drifting away from the Library it was opened from — and `--measure-reading` was
   * always the measure of every prose surface (read/import/review), so a reader
   * who narrows their column narrows it wherever prose is set.
   * What the face dial deliberately does NOT reach is the READER'S OWN VOICE
   * (`THE THREE VOICES` below, which owns that argument) or the instrument mono.
   * THE LIST IS ABOUT THE FACE AND NOTHING ELSE. Naming a family directly holds an element's face
   * still; it says nothing about that element's size or its line. A predict station is
   * where that shows: the reader-voice italic standing in the reading column takes the SIZE
   * and the SPACING dials, and so does the machine-authored type beside it — the station's
   * prompt, the check's questions and its answer buttons are prose the reader reads in this
   * column, and they follow it (`read.css › --ap-station-size`, #381). The margin's and the dock's
   * italics are a separate matter and no dial reaches them at all: they are chrome, and
   * they state their own scale in literals where they are declared. */

  /* Measures (design system §2).
   *
   * THE ONE CONTENT COLUMN (#711) — what a surface IS wide, border-box, so the
   * inset below is inside it and not beside it. 680px is the prototype's number
   * and it is a border box there too (`design/recur-contents.html › .shelf`,
   * which every one of its views wears, the reading view included).
   *
   * IT CARRIES THE WIDTH DIAL, and the reading measure is derived from it rather
   * than declared beside it. Two independent numbers is how the column came to be
   * 680 while the prose inside it asked for 638 — a reader's first width step
   * would have moved nothing at all, because the column was already the narrower
   * of the two. One declaration, one step, and the prose is whatever is left
   * inside the column: 616px at the designed setting, 640px on a phone, where the
   * inset narrows and the column is wider than the screen anyway. */
  --measure-page: calc(680px + var(--measure-steps, 0) * 34px);
  --measure-reading: calc(var(--measure-page) - 2 * var(--page-inset)); /* the prose inside the column */
  /* The page's own horizontal inset — the masthead's padding and every surface's,
   * which is why anything pinned to the viewport's edge aligns to it (`read.css ›
   * .ap-position`). A token rather than a repeated literal, because a per-surface
   * copy would be a second home for a number this file owns. The phone tier below
   * narrows it: 32px on each side of a 393px screen costs a sixth of the line. */
  --page-inset: 32px;
  /* The thumb floor (#494, #516): the least a control a reader taps may measure.
   * `dev/playwright-env.mjs › TOUCH_TARGET` is the drivers' independent copy. */
  --touch-target: 44px;
}

/* —— THE BUNDLED FACES (#371, #387) ——————————————————————————————————————————
 *
 * One block per cut, for four families: `@font-face` is a declaration and not a
 * loop, and this project writes plain CSS with no preprocessor (spec 07), so
 * the repetition is the format's and there is nothing here to factor out. What
 * IS said once is the tokens above — a surface names `--literata`, never a file.
 * How many cuts each family HAS is not a number here; `readingDials.test.ts`
 * carries it per family, because Lexend has two where the others have four.
 *
 * `src` is a ROOT-ABSOLUTE, SAME-ORIGIN path into `public/fonts/`, which Vite
 * copies verbatim into the build. That is the whole reason this issue changed no
 * security boundary: `default-src 'self'` covers font loads, so nothing was
 * added to `public/_headers` and `test/csp.test.ts` and `dev/e2e-csp.mjs` judge
 * an unchanged policy.
 *
 * `font-weight` and `font-style` are declared per FILE, not per family, because
 * a family that claims only 400/normal makes the browser SYNTHESISE bold and
 * italic — smearing the outlines sideways — while the real cuts sit unused in the
 * cache the reader already paid for. `font-display: swap` because a face that is
 * not in cache yet must never blank the prose: the fallback stack in the token
 * paints immediately and is replaced when the file lands.
 *
 * `test/bundledFonts.test.ts` reads these blocks and the files beside them;
 * `dev/verify-fonts.mjs` weighs what shipped. */

@font-face {
  font-family: "Literata";
  src: url("/fonts/literata/Literata-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Literata";
  src: url("/fonts/literata/Literata-Italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Literata";
  src: url("/fonts/literata/Literata-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Literata";
  src: url("/fonts/literata/Literata-BoldItalic.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Atkinson Hyperlegible";
  src: url("/fonts/atkinson-hyperlegible/AtkinsonHyperlegible-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  src: url("/fonts/atkinson-hyperlegible/AtkinsonHyperlegible-Italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  src: url("/fonts/atkinson-hyperlegible/AtkinsonHyperlegible-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  src: url("/fonts/atkinson-hyperlegible/AtkinsonHyperlegible-BoldItalic.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "OpenDyslexic";
  src: url("/fonts/opendyslexic/OpenDyslexic-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "OpenDyslexic";
  src: url("/fonts/opendyslexic/OpenDyslexic-Italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "OpenDyslexic";
  src: url("/fonts/opendyslexic/OpenDyslexic-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "OpenDyslexic";
  src: url("/fonts/opendyslexic/OpenDyslexic-BoldItalic.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

/* TWO BLOCKS, NOT FOUR — LEXEND HAS NO ITALIC (#387). The family ships nine upright
 * weights and no oblique at any of them, so there is no italic file to point at and
 * the dial takes 400 and 700.
 *
 * What happens at an <em> is the reason this is right rather than merely unavoidable:
 * CSS font matching stays INSIDE the family and synthesizes an oblique by slanting the
 * upright face. The refused alternative is an italic block whose `src` is the upright
 * file — it parses, it satisfies a four-cuts count, and it switches synthesis OFF, so
 * emphasis would render identical to body text and the reader would lose the
 * distinction entirely. `test/readingDials.test.ts` carries the expected cut count per
 * family so the count is checked without being fixed at four, and
 * `dev/e2e-reading-style.mjs › emphasisStaysInLexend` is what proves the synthesis
 * actually keeps emphasis in Lexend rather than handing it to the next family. */
@font-face {
  font-family: "Lexend";
  src: url("/fonts/lexend/Lexend-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Lexend";
  src: url("/fonts/lexend/Lexend-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Excalifont";
  src: url("/fonts/excalifont/Excalifont-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
}

body {
  font-family: var(--reading-face);
  font-size: var(--reading-size);
  line-height: var(--reading-leading);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a {
  color: var(--accent);
  text-decoration: none;
}
a:hover {
  color: var(--accent-ink);
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The selection wears the app's accent (#711), on every surface rather than only in
 * the reading column — the loudest colour on a page should not be the browser's blue.
 * 26% over transparent so the prose reads through it; `color-mix` on the accent and
 * NOT a fifteenth colour token, because the value is derived from one the palette
 * already holds and a token would be a fifteenth row in three hand-written copies of
 * it. `transparent` is a keyword deferring to what is underneath, not a colour, which
 * is why the literal scan in `test/palette.test.ts` passes it. */
::selection {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  color: inherit;
}

.ap-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Masthead — hairline-bottom bar. Pinned to the top (#58) so the route nav is never
 * scrolled away down a long column — an EPUB runs to thousands of blocks. On the read
 * surface it recedes while the reader reads forward and a small scroll back returns it
 * (#653, the rule after this one), which keeps #58's promise one flick away. `sticky`,
 * not `fixed`: the bar stays in flow, so no surface reserves its height and no
 * first block boots hidden underneath it.
 * Stacking, low to high: the reading bar 30 (read.css) < masthead 50 < the
 * reading dock 60 — where the Review bar (review.css) sits beside it, the same role
 * at the same edge on a surface the dock never reaches < the Prompt-craft overlay 70
 * (read.css) — a full-screen overlay still covers the bar, and the reading bar tucks
 * under it. Rung 30 held TWO tenants until #711, the position readout at the
 * viewport's top-left corner and the Aa control (#351) at the top-right; they are one
 * bar now, which is one element on the rung instead of two that had to agree. Above the whole
 * ladder and off it: the two panels that hang from the reading bar — the Contents
 * (`read.css › .ap-contents`) and the Type control's popover (`read.css › .ap-tune-pop`)
 * — are `popover="auto"`, so the TOP LAYER puts them over everything with no number, and
 * their seat is measured off the chrome's own height rather than the screen's top edge.
 * Rung 60's dock is where a Mark's entries are read since ADR 0046, so nothing beside a
 * run needs the top layer any more. A new element that needs to clear the bar picks a
 * number here; one that needs to clear everything uses the top layer and says so. The
 * install nag below sits above the masthead in flow and scrolls away with the
 * page; the bar then takes the top edge the nag vacated. */
.ap-masthead {
  position: sticky;
  top: 0;
  z-index: 50;
  /* Opaque now that it overlaps: the in-flow bar needed no background, a pinned
   * one must let the scrolling column pass behind rather than through it. */
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  transition: transform var(--ap-chrome-slide);
}

/* RECEDED (#653) — the read surface takes the bar out of the way while the reader reads
 * forward, and a small scroll back returns it. Written by `surfaces/read.ts ›
 * markChromeReceded` and removed by that surface's route teardown, so no other surface sees
 * it. The reading bar in `read.css` slides on the same `--ap-chrome-slide`, so the two
 * pieces of chrome move as one. A transform, not a change of `position`: the bar keeps its
 * layout height, so the column never reflows under the reader and every seat measured from
 * the bar's height stays true.
 * KEYBOARD FOCUS HOLDS IT. While `:focus-visible` is anywhere in this bar, or in the read
 * surface's reading bar, the chrome stays on screen whatever the painter marked: a keyboard
 * reader who tabs to a link and then pages on must not lose the focus they stand on (WCAG
 * 2.4.11), and a Shift+Tab into the reading bar scrolls nothing that could bring it back.
 * `:focus-visible`, not `:focus-within`: a mouse click leaves focus on the link it clicked,
 * the bar survives the route change, and a `:focus-within` hold kept it on screen for good on
 * the way back. The global reduced-motion rule stills the slide. */
.ap-masthead[data-receded]:not(:has(:focus-visible)):not(:has(~ .ap-surface .ap-reading-bar :focus-visible)) {
  transform: translateY(-100%);
}

/* A bar that keyboard focus brings back arrives at once rather than sliding. The browser
 * decides whether to scroll a focused link into view from where the bar is at that moment,
 * and a bar still starting its slide from off screen made one Tab scroll the reader half a
 * screen back through the book. The corners need no such rule: they are `position: fixed`,
 * so focus on them cannot scroll the page. */
.ap-masthead:has(:focus-visible) {
  transition: none;
}

/* THE ROW STANDS ON THE CONTENT COLUMN (#711). The bar spans the window, because the
 * hairline it draws is the page's edge; the row inside it takes `--measure-page`,
 * because five small items parked in the window's corners belong to nothing, and
 * these belong to the column beneath them. `1fr auto 1fr` is what holds the mark ON
 * the centre rather than wherever the two groups happen to leave it, and it is what
 * puts each group against its own edge of the column. */
.ap-mast {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 28px;
  max-width: var(--measure-page);
  margin: 0 auto;
  padding: 14px var(--page-inset) 12px;
}
.ap-nav {
  display: flex;
  gap: 30px;
}
.ap-nav-trail {
  justify-content: flex-end;
}

/* A Phosphor glyph (`icons.ts`), sized off the text it sits beside. `center`
 * keeps it out of a baseline-aligned row's baseline group: a replaced element's
 * baseline is its bottom edge, and a row that took it would lift its label off
 * the line its neighbours sit on. */
.ap-icon {
  flex: none;
  align-self: center;
  width: 1.25em;
  height: 1.25em;
}

/* THE MASTHEAD'S MARK — the home-screen icon's meander as one stroke in the page's ink,
 * with no tile (`mark.ts`, which carries its provenance). Drawn on the glyphs' own 256
 * grid at their own line weight, so the row is one family of five rather than four
 * glyphs and a heavier stranger. 22px and not their 20: the mark's ink is taller than
 * it is wide, so an identical box would read as a SMALLER mark rather than an equal one.
 *
 * `ap-mast-mark` AND NOT `ap-mark`, which the reading surface has owned all along for
 * the trace a Card or a Margin note leaves in the prose (CONTEXT § Mark, `read.ts ›
 * markSegment`). Two different things called the mark is the product's own ambiguity;
 * one class name for both turned every highlighted phrase into a grid box. */
.ap-mast-mark {
  display: grid;
  place-items: center;
  color: var(--ink);
}
.ap-mast-mark svg {
  display: block;
  width: 22px;
  height: 22px;
}

/* ONE MASTHEAD ITEM, worn by a destination and by the sync control alike: the glyph
 * with its name beside it, in the chrome's own sans, sentence case since #711 — a
 * destination is a category and not a control, and tracked capitals said the opposite
 * at every size. The name is present at every tier; the glyph is what makes the row
 * recognisable once the reader has stopped reading it. */
.ap-mast-item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 0;
  border: 0;
  background: none;
  font-family: var(--ui-face);
  font-size: 14.5px;
  font-weight: 400;
  color: var(--faint);
  cursor: pointer;
  transition: color 0.15s;
}
.ap-mast-glyph {
  position: relative;
  display: inline-flex;
}
.ap-mast-item .ap-icon {
  width: 20px;
  height: 20px;
}
.ap-nav-link:hover {
  color: var(--muted);
}
.ap-nav-link[aria-current="page"] {
  color: var(--accent);
}

/* CARDS ARE DUE: a dot on the Review glyph's corner, and never a count — the reminder
 * is that there is work, not how much of it is waiting (#711). It rides the glyph
 * rather than the name so it still reads on a phone, where the name is 11px under it.
 * An `outline` and not a `box-shadow` for the ring that lifts it off the glyph's own
 * strokes: this design has no shadows anywhere. */
.ap-nav-link[data-due] .ap-mast-glyph::after {
  content: "";
  position: absolute;
  top: -2px;
  right: -3px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  outline: 2px solid var(--paper);
}

/* The sync control (`syncStatus.ts`) is a masthead item like the rest, and carries the
 * constant word Sync: the state is in the glyph and its tone, which is why the TONE is
 * scoped to the glyph — an orange label would say the control itself was the warning,
 * and `--warn` is only ever allowed to ride a changed glyph. No hover colour, unlike a
 * destination: here a colour change is a change of standing, and a hover must not look
 * like one.
 *
 * TWO ELEMENTS WEAR IT since #711 — the masthead's control and Settings' Sync row,
 * which shows the same glyph in the same tone (story 81) — so each rule names both
 * wearers rather than floating free on the attribute alone. Anchored on the class the
 * way `.ap-nav-link[aria-current="page"]` is: a state selector says which component is
 * in that state, and an unanchored `[data-sync-tone]` would silently claim any element
 * that ever grows the attribute. What a tone LOOKS like still has this one home — a
 * settings file with its own copy of `--warn` would be the copy that drifts. */
.ap-sync-status[data-sync-tone="current"] .ap-icon,
.ap-listrow[data-sync-tone="current"] > .ap-icon {
  color: var(--ink);
}
.ap-sync-status[data-sync-tone="unsynced"] .ap-icon,
.ap-listrow[data-sync-tone="unsynced"] > .ap-icon {
  color: var(--warn);
}
.ap-sync-status[data-sync-tone="off"] .ap-icon,
.ap-listrow[data-sync-tone="off"] > .ap-icon {
  color: var(--faint);
}
.ap-sync-status[aria-busy="true"] .ap-icon,
.ap-listrow[data-sync-tone][aria-busy="true"] > .ap-icon {
  animation: ap-spin 1.2s linear infinite;
}

/* Supported phone tier (issue #494) — keep the complete surface navigation in the
 * frame. The masthead's wide arrangement has more intrinsic width than a 393px
 * viewport, so the row becomes five equal cells — two destinations, the mark,
 * Settings and Sync — each with its glyph over an 11px name, every one of them a
 * touch target. One rhythm for all five, which is what ADR 0033's amendment records. */
@media (max-width: 640px) {
  :root {
    --page-inset: 20px;
  }

  /* The thumb floor on a phone (#516), stated once: every control class listed here
   * is at least a thumb tall, and a new one joins the list. A class list and not
   * `button`, because three of these controls are `<label>`s and three are links —
   * which need no rule of their own any more, the shared button being `inline-flex`
   * since #711 and an inline box being what used to ignore `min-height`.
   * The floor is vertical only: a horizontal floor on every shared control is the
   * mechanism behind the document overflow #498 was filed against, so `min-width`
   * stays with the controls whose own drivers measured that it costs nothing. The
   * video timecode (`.ap-timecode`) is left out: at the floor its label centres below
   * the first line of the block it times, and a heading's row grows to hold it. The
   * reading view's card glyph (`.ap-cardmark`) is left out for the same kind of reason
   * and reaches the floor another way: it sits INSIDE a paragraph, so a `min-height`
   * would open its whole line box, and `read.css` draws the target out around it
   * instead. */
  .ap-btn-accent,
  .ap-btn-quiet,
  .ap-gate-decline,
  .ap-cost-skip,
  .ap-seg-b,
  .ap-nag-x,
  .ap-mast-item,
  .ap-mast-mark,
  .ap-tool,
  .ap-act,
  .ap-contents-b,
  .ap-contents-pin,
  .ap-gen-open,
  .ap-digest,
  .ap-dossier-copy,
  .ap-dossier-share,
  .ap-video button,
  .ap-predict-submit,
  .ap-predict-skip,
  .ap-predict-reveal,
  .ap-predict-response-retry,
  .ap-predict-del,
  .ap-gen-seat,
  .ap-craft-close,
  .ap-dock-close,
  .sync-confirm,
  .ap-video-paste-automatic,
  .ap-comprehension-option,
  .ap-mnote,
  .ap-imgtap,
  .ap-card-check-dismiss,
  .ap-field,
  .ap-listrow {
    min-height: var(--touch-target);
  }

  .ap-mast {
    grid-template-columns: repeat(5, minmax(0, 1fr));
    justify-items: center;
    gap: 0;
    padding: 10px var(--page-inset) 8px;
  }

  /* The two groups stop being boxes and let their items BE the row's cells, so all
   * five share one rhythm and one set of gaps rather than three nested ones. */
  .ap-nav {
    display: contents;
  }

  .ap-mast-item {
    flex-direction: column;
    justify-content: center;
    gap: 3px;
    width: 100%;
    min-width: 0;
    padding: 0 2px;
    font-size: 11px;
    text-align: center;
    overflow-wrap: anywhere;
  }

  /* The mark takes its whole cell here, as its four neighbours do: it is a link to the
   * Library, so what a thumb has to hit is the cell and not the 22px drawing in it. */
  .ap-mast-mark {
    width: 100%;
  }
}

/* Surface body — the one content column (#711). A surface that needs a different
 * width says so in its own file and says why; the reading surface is the one that
 * does, because its margins hang outside the column by design. */
.ap-surface {
  max-width: var(--measure-page);
  margin: 0 auto;
  padding: 40px var(--page-inset) 80px;
}

/* —— THE THREE VOICES (#711) ——————————————————————————————————————————————————
 *
 * Everything on a page belongs to one of three speakers, and the design's rule is
 * that no two of them are set alike. The rule was two voices until this issue; the
 * third is the model's, which had been sharing the app's.
 *
 *   THE AUTHOR, and anything that names or quotes a text — the reading Face,
 *   upright. It has no class because it is the DEFAULT: `body` and
 *   `.ap-surface-title` are set in `--reading-face`, and a page is in the author's
 *   voice unless one of the classes below takes it away.
 *
 *   THE READER — `.ap-voice` for what they wrote as prose (a Card, a Prediction),
 *   `.ap-voice-hand` for a Margin note.
 *
 *   THE APP, AND A MODEL — `--ui-face`, sentence case, which every chrome class in
 *   this file names; a model's words additionally stand behind the `sparkle` glyph
 *   (`.ap-machine`), so one sign means "a model wrote this" everywhere.
 *
 * Here and not in a per-surface file because a voice that each surface restated
 * would be a voice that drifts: the day the reading surface's italic and the
 * Library's disagree, the marker has stopped marking.
 *
 * THE READER'S VOICE DOES NOT FOLLOW THE FACE DIAL, and names `--literata` rather
 * than `--reading-face` on purpose: it is a voice MARKER, and a marker that moved
 * with the dial would stop marking anything (`THE READING DIALS` above). `--literata`
 * rather than `--serif` since ADR 0045 — the reader's italic is now the same family
 * as the designed author face, so the two voices differ by slope alone, and the
 * italic is bundled rather than whatever italic the device turned out to hold.
 *
 * NO SYNTHESIZED OBLIQUE (#522). The platform's emoji font has no italic cut, so an
 * italic run slants every emoji in it. Literata has a real italic, which this does
 * not touch; a face with none shows this voice upright. Not on reading-type
 * emphasis: Lexend has no italic and relies on the synthesis this refuses
 * (`TWO BLOCKS, NOT FOUR` above). */
.ap-voice {
  font-family: var(--literata);
  font-style: italic;
  font-synthesis-style: none;
}
.ap-voice-hand {
  font-family: var(--excalifont);
  font-style: normal;
}

/* A model speaking: its words in the app's own face, never in the author's, behind
 * the one glyph that says a model wrote them. A grid rather than an inline glyph, so
 * a line that wraps keeps its second line under the first and not under the sparkle. */
.ap-machine {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  align-items: start;
  font-family: var(--ui-face);
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--muted);
}
.ap-machine .ap-icon {
  align-self: start;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  color: var(--accent);
}
/* WHILE THE MODEL IS READING, THE SIGN PULSES (#711, stories 8 and 59). The glyph is
 * what says a model is speaking, so it is also what says one is working — a second
 * element, or a sentence, would be the app narrating what the sparkle already shows.
 * `aria-busy` and not a class: the state is announced to a screen reader either way,
 * and one encoding cannot disagree with the other (CONVENTIONS §A control's selected
 * state is an ARIA attribute). The reduced-motion rule at the foot of this file stills
 * it with every other animation. */
.ap-machine[aria-busy="true"] .ap-icon {
  animation: ap-pulse 1.1s ease-in-out infinite;
}

/* Shared surface H1 — the design system's essay title (§2). */
.ap-surface-title {
  margin: 12px 0;
  font-family: var(--reading-face);
  font-weight: 500;
  font-size: clamp(29px, 4.4vw, 43px);
  line-height: 1.06;
  letter-spacing: -0.012em;
  text-wrap: balance;
}

/* —— THE BUTTON (#711) ————————————————————————————————————————————————————————
 *
 * A control the reader presses has a shape: a square rectangle with a 1px border,
 * the chrome's sans at 15.5/600, and room beside the label for a leading glyph.
 * Content is never framed, which is exactly what lets a frame mean "press me" — so
 * no control in the app is bare text any more, and the one text-only pair left is
 * the reading view's foot actions, which carry glyphs.
 *
 * THREE KINDS, and a context has at most one of the first:
 *   FILLED (`.ap-btn-primary`, always riding `.ap-btn-accent`) — the one main action
 *     of its context, findable without reading.
 *   OUTLINED ACCENT (`.ap-btn-accent` alone) — an alternative to it.
 *   OUTLINED NEUTRAL (`.ap-btn-quiet`, `.ap-gate-decline`, `.ap-cost-skip`) — a
 *     minor action: declining, skipping, cancelling. It must not compete with the
 *     accent control it stands beside, which is the argument the bare-text tertiary
 *     used to make by having no shape at all — and a caption a reader cannot tell
 *     from a control is the worse failure of the two.
 * Two equal choices take the SAME kind, so nothing nudges a reader toward one Grade.
 *
 * THREE HEIGHTS: 46px in a form, which is the default; 58px in a bottom bar
 * (`.ap-btn-bar`); 66px for a button carrying a second line, such as a Grade and its
 * next interval (`.ap-btn-two-line`).
 *
 * DISABLED IS 35% OPACITY AND NOTHING ELSE. A disabled control keeps its shape and
 * its words, so a reader can see what will be there once it is theirs to press —
 * which is what lets Import show its send control from the start.
 *
 * The four base names are listed together rather than replaced by one `.ap-btn`:
 * they are the hooks a dozen drivers already press, and a rename buys nothing a
 * reader can see. `:not(:disabled)` rather than `:enabled` throughout, because two
 * of these are worn by an `<a>` and one by a `<label>`, which `:enabled` never
 * matches. */
.ap-btn-accent,
.ap-btn-quiet,
.ap-gate-decline,
.ap-cost-skip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  min-height: 46px;
  padding: 0 20px;
  border: 1px solid var(--accent);
  border-radius: 0;
  background: transparent;
  color: var(--accent);
  font-family: var(--ui-face);
  font-size: 15.5px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.ap-btn-accent:not(:disabled):hover,
.ap-btn-quiet:not(:disabled):hover,
.ap-gate-decline:not(:disabled):hover,
.ap-cost-skip:not(:disabled):hover {
  background: var(--accent-wash);
}
.ap-btn-quiet,
.ap-gate-decline,
.ap-cost-skip {
  border-color: var(--faint);
  color: var(--ink);
}
.ap-btn-primary {
  color: var(--on-accent);
  background-color: var(--accent);
}
.ap-btn-primary:not(:disabled):hover {
  background-color: var(--accent-ink);
  border-color: var(--accent-ink);
  color: var(--on-accent);
}
/* `[data-disabled]` rides along because a `<label>` cannot be `:disabled`, and two of
 * them are: the archive picker in Settings' Library group
 * (`surfaces/library-archive.ts › archive-import`) and the Import dialog's EPUB row.
 * Both are a `.ap-listrow` wrapping the picker, so the row joins the list rather than
 * restating 35% in its own file — the rule is what "disabled" LOOKS like here, and it
 * has one home. */
.ap-btn-accent:disabled,
.ap-btn-quiet:disabled,
.ap-btn-quiet[data-disabled],
.ap-gate-decline:disabled,
.ap-cost-skip:disabled,
.ap-listrow:disabled,
.ap-listrow[data-disabled] {
  opacity: 0.35;
  cursor: default;
}
/* 16px and not the base 15.5: a bar button is the whole width of a bottom bar with
 * one thing to say, and the prototype sets it there (`design/recur-contents.html ›
 * .btn.big`). It is the one place the button's type moves. */
.ap-btn-bar {
  min-height: 58px;
  font-size: 16px;
}
/* A two-line button stacks, so its label can no longer sit beside a leading glyph
 * on the button's own axis: the first child is the label line and carries the glyph,
 * the second is the quiet line under it — a Grade's next interval. Named by position
 * rather than by two more classes, because that IS the shape of the component. */
.ap-btn-two-line {
  flex-direction: column;
  gap: 3px;
  min-height: 66px;
  padding: 9px 12px;
}
.ap-btn-two-line > :first-child {
  display: inline-flex;
  align-items: center;
  gap: 9px;
}
.ap-btn-two-line > :last-child {
  font-size: 13px;
  font-weight: 400;
  color: var(--faint);
}

/* —— THE FIELD (#711) —————————————————————————————————————————————————————————
 *
 * A line is drawn only under something the reader types into, and that line is the
 * whole of the control: transparent ground, a 1px `--faint` underline — a control's
 * own boundary is `--faint` (#534) — and no box anywhere. Focus turns the underline
 * accent and doubles it, taking the extra pixel out of the padding so the row does
 * not grow taller and nothing under it moves. A border rather than a shadow, because
 * this design has no shadows at all and `dev/e2e-reading-style.mjs` enforces that
 * over the whole reading surface.
 *
 * THE NAME STAYS ABOVE IT AND STAYS THERE ONCE IT IS FILLED (`.ap-field-label`): a
 * placeholder is gone at exactly the moment the reader needs it, so a placeholder is
 * a hint here and never the only label.
 *
 * A textarea GROWS with what is in it, so the reader can see everything they have
 * written; a browser without `field-sizing` keeps whatever `rows` it was given.
 *
 * THE DOUBLED ACCENT UNDERLINE IS THE WHOLE FOCUS INDICATOR, for a text field and for
 * nothing else. It was written as "mouse focus shows the underline, keyboard focus
 * additionally keeps the ring", qualified by `:focus-visible`, and that premise was
 * false: a browser matches `:focus-visible` on ANY focused text input, however the
 * focus arrived, so the ring and the underline always painted together and a field
 * wore two indicators. One is the design's (`design/recur-contents.html`'s `.line`),
 * and it is the one that says which field, at reading size, without a box.
 *
 * Every OTHER control keeps the accent ring — a button, a row, a link, a segment have
 * no underline of their own to stand in for it — so the suppression below is scoped to
 * the two elements a reader types into, and never to `.ap-field` itself. */
.ap-field {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--faint);
  color: var(--faint);
}
.ap-field:focus-within {
  padding-bottom: 8px;
  border-bottom: 2px solid var(--accent);
  color: var(--accent);
}
.ap-field > input,
.ap-field > textarea {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  font-size: 19px;
  line-height: 1.45;
  resize: none;
}
/* A FIELD THE READER WRITES THEIR OWN WORDS INTO KEEPS ITS VOICE (#711). The reading Face
 * is what a field is set in — a link, a phrase, a search — but a Card, a Prediction and a
 * Margin note are the reader speaking, and the voice marker has to win. Stated as an
 * exception on THIS declaration rather than restated in `THE THREE VOICES` above: a
 * one-class `.ap-voice` loses to the two-class selector a field needs, and a second copy
 * of the voice is how two definitions of one marker come to disagree. */
.ap-field > input:not(.ap-voice, .ap-voice-hand),
.ap-field > textarea:not(.ap-voice, .ap-voice-hand) {
  font-family: var(--reading-face);
}
.ap-field > textarea {
  field-sizing: content;
  overflow: hidden;
}
.ap-field > input:focus,
.ap-field > textarea:focus {
  outline: none;
}
.ap-field ::placeholder {
  color: var(--faint);
  opacity: 1;
}

/* —— THE ROW (#711) ———————————————————————————————————————————————————————————
 *
 * A settings row or an action row: label at the leading edge, value at the trailing
 * edge in `--muted`, one `--rule` hairline beneath, and nothing boxed. 52px is the
 * floor, so a row is comfortably a thumb before the phone tier's floor is consulted
 * at all. An action row leads with a 20px glyph instead of ending in a value, and is
 * a `<button>`, an `<a>`, a `<label>` or a `<summary>` wearing the same class, because
 * "what it looks like" and "what it does" are two questions and only the second picks
 * the element — the Import dialog's EPUB row is a label, and the row IS the device's
 * file picker; Settings' Sync row is a summary, and the row IS the disclosure's offer.
 * A pressable row may still end in a value, which is how that Sync row says where the
 * Device stands. */
.ap-listrows {
  margin: 0;
  padding: 0;
  list-style: none;
}
.ap-listrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  min-height: 52px;
  border-bottom: 1px solid var(--rule);
  font-family: var(--ui-face);
  font-size: 15.5px;
  color: var(--ink);
}
.ap-listrow-v {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
}
button.ap-listrow,
a.ap-listrow,
label.ap-listrow,
summary.ap-listrow {
  width: 100%;
  justify-content: flex-start;
  gap: 14px;
  padding: 0;
  border-width: 0 0 1px;
  background: none;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
button.ap-listrow:hover,
a.ap-listrow:hover,
label.ap-listrow:hover,
summary.ap-listrow:hover {
  color: var(--accent);
}
/* A pressable row may still END in a value — Settings' Sync row says where the Device
 * stands at its trailing edge — so the value keeps the trailing edge the leading-edge
 * justification above took away from it. */
.ap-listrow > .ap-listrow-v:last-child {
  margin-left: auto;
}
/* A row's leading glyph is quieter than the words beside it. A row that reports a
 * standing takes its glyph's colour from the tone rules above instead, which name this
 * class and so outrank this by a whole attribute — no carve-out needed here. */
.ap-listrow > .ap-icon {
  width: 20px;
  height: 20px;
  color: var(--muted);
}

/* THE SEGMENTED CONTROL — the one selection idiom the app draws. A joined hairline
 * frame around a row of members, a hairline between them, no gap, square corners;
 * the selected member takes the wash fill and the accent ink, read off `aria-pressed`
 * alone (CONVENTIONS § A control's selected state is an ARIA attribute). Shared
 * because two surfaces draw it — the shelf's View and Order groups (`library.css`)
 * and every row of the Aa popover (`read.css`), where the steppers' readout stands
 * as a member that is not a button — and a frame that lived in each per-surface
 * file would drift a padding apart, the way `.ap-nag` was hoisted to prevent. What
 * a member's rhythm is (its padding, its size, whether it is a grid or a row) stays
 * with the surface; what makes the group ONE control is here. `.ap-seg > *` and not
 * `.ap-seg-b`, so the popover's readout carries the same hairline as the buttons
 * either side of it. Placed after `.ap-btn-accent` so a member that also wears that
 * skin loses its own border and hover to the group's. */
.ap-seg {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--faint);
}
.ap-seg > * {
  border: 0;
  border-right: 1px solid var(--faint);
  border-radius: 0;
}
.ap-seg > :last-child {
  border-right: 0;
}
.ap-seg-b {
  font-family: var(--ui-face);
  font-size: 13.5px;
  color: var(--muted);
  background: none;
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
}
.ap-seg-b:hover:enabled {
  color: var(--accent);
  background: var(--accent-wash);
}
.ap-seg-b[aria-pressed="true"] {
  color: var(--accent);
  background: var(--accent-wash);
}
.ap-seg-b:disabled {
  color: var(--faint);
  cursor: default;
}


/* THE NAMED GLYPH — the shape every tool in this design wears: the drawing over its own
 * name, no frame, because a row of framed buttons under a framed masthead is the
 * dashboard #711 is about. The name is visible at every width, so the glyph is shorthand
 * a reader learns rather than a puzzle they have to solve.
 *
 * Here rather than per surface since #711 slice 3, because two surfaces draw one — the
 * reading bar's About, Contents and Type, and the shelf's Add, Find and Sort — and the
 * two copies had already drifted a transition and a horizontal floor apart. What a
 * surface keeps is the gaps around its own row and the size of its own glyphs.
 *
 * `min-width` as well as the phone tier's `min-height`: a named glyph is narrower than
 * it is tall, and this is one of the controls whose own driver measured that the
 * horizontal floor costs no overflow. */
.ap-tool {
  display: grid;
  place-items: center;
  gap: 3px;
  min-width: var(--touch-target);
  padding: 0 6px;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s;
}
.ap-tool:hover,
.ap-tool[aria-expanded="true"] {
  color: var(--ink);
}
.ap-tool > span {
  font-family: var(--ui-face);
  font-size: 11px;
  line-height: 1;
}

/* The quiet line naming what a surface or a block is. #711 retired two things it
 * wore: the tracked monospace capitals, which labelled a category in the same voice
 * as a control and made neither legible, and the accent dot in front of it — an
 * ornament on a line whose whole job is to recede. What is left is the app speaking
 * quietly, in sentence case, which is what the copy register always said it was. */
.ap-eyebrow {
  margin: 0 0 20px;
  font-family: var(--ui-face);
  font-size: 12.5px;
  color: var(--faint);
}

/* THE HINT — a line that teaches the method, or explains what a control costs, beside
 * the control it belongs to: the chrome's sans in `--faint`, as the design system's
 * § Components states it. Here rather than per surface, because a hint set `--faint` on
 * one screen and `--muted` on the next is two registers sharing one name, and a reader
 * learns the register, not the screen. A surface adds only its own rhythm around it.
 *
 * THE MEASURE IS THE HINT'S OWN (#711 slice 8). Every seat that had one had written
 * `58ch` again — twice in `settings.css`, once in `review.css` — because a line of
 * explanation across the full 680px column is a line nobody finishes. That is a fact
 * about this voice and not about any of those screens. */
.ap-hint {
  max-width: 58ch;
  font-family: var(--ui-face);
  font-size: 13px;
  line-height: 1.4;
  color: var(--faint);
}

/* THE NAME OF A BAND OF ROWS (#711 slice 8) — a Settings group, the Ledger's two
 * retention figures. It is the chrome's sans one step firmer than a field's caption
 * below, because it names a band and not one control: bigger, weight 600, `--muted`
 * rather than `--faint`. That is the prototype's `.group`
 * (`design/recur-contents.html`), and it matched NEITHER of the two dresses the app was
 * using — Settings had no such level at all and the Ledger had borrowed the field
 * caption — so the two surfaces wear this one instead of a name each. Muted and not
 * ink: the rows under it are what the reader came to read. A surface adds only its own
 * margin. */
.ap-group-name {
  display: block;
  font-family: var(--ui-face);
  font-size: 13.5px;
  line-height: 1.4;
  font-weight: 600;
  color: var(--muted);
}

/* The caption above a control, and the block headings that share its voice.
 * Cross-surface since #319 gave Settings its own screen — Import and Settings both
 * render it, so it lives here rather than in one of their per-surface files
 * (CONVENTIONS § shared classes in apparatus.css). It is the `.ap-eyebrow` recipe
 * one step quieter: smaller, and captioning a field rather than a surface.
 * It PERSISTS once the field under it is filled (#711): a placeholder disappears at
 * exactly the moment a reader wants to know what they are typing into. */
.ap-field-label {
  display: block;
  font-family: var(--ui-face);
  font-size: 12.5px;
  line-height: 1.3;
  color: var(--faint);
  margin-bottom: 6px;
}

/* A `<select>` in the house skin. The UA's arrow is a second visual language on
 * a page drawn in hairlines, so the control drops its native appearance and the
 * wrapper draws the chevron: two borders of a rotated square, the same glyph the
 * reading dock's grip uses, because the CSP that serves the app admits no
 * `data:` image and the wrapper is the only element a `<select>` can lend a
 * pseudo-element to. Padding on the right keeps the value clear of the glyph.
 * The wrapper is a block and the select fills it, so it takes whatever width its
 * seat gives it — the cost gate's grid cell.
 *
 * Shared still, though the Cost gate is the only seat left: Settings' selects became
 * rows in #711, where the value stands at the trailing edge behind the `caret-down`
 * glyph rather than in a plate with a chevron. This stays here, not in the gate's own
 * file, because the gate is rendered on two surfaces (`generationGate.ts`). */
.ap-select {
  position: relative;
  display: block;
}
.ap-select > select {
  width: 100%;
  appearance: none;
  -webkit-appearance: none;
  padding-right: 40px;
  cursor: pointer;
}

.ap-select::after {
  content: "";
  position: absolute;
  right: 16px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -6px;
  border-right: 1.5px solid var(--muted);
  border-bottom: 1.5px solid var(--muted);
  transform: rotate(45deg);
  pointer-events: none;
}

/* The boot-tier strips — a bar above the masthead, in the app's own sans since #711,
 * dismissible from the `x` glyph at its right edge. Flow content (not fixed), so a strip
 * pushes the app down instead of covering the reading dock. Two exist and both
 * wear this frame, keeping only what differs: `.ap-install-nag` (#30) asks an
 * in-Safari iOS device to install, `.ap-update-nag` (#325) offers an update the
 * browser has parked and may never activate by itself. One home for the frame,
 * so the second strip cannot drift a padding away from the first — the identity
 * class stays on each element because the per-strip rules and the PWA driver
 * both read it. */
.ap-nag {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px var(--page-inset);
  background: var(--accent-wash);
  border-bottom: 1px solid var(--rule);
}
.ap-nag-text {
  margin: 0;
  flex: 1;
  font-family: var(--ui-face);
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--accent-ink);
}
.ap-nag-text strong {
  font-weight: 600;
}
/* The dismiss is the `x` GLYPH, not a typed `✕` (#711): the app has one sign for closing
 * — the dock wears it too — and a character borrowed from the font was the strip saying
 * it in a second alphabet. Its accessible name is what names it, as the design system
 * asks of every glyph that is a control. */
.ap-nag-x {
  display: grid;
  place-items: center;
  flex: none;
  line-height: 1;
  color: var(--accent);
  background: none;
  border: none;
  padding: 6px;
  cursor: pointer;
}
.ap-nag-x .ap-icon {
  width: 16px;
  height: 16px;
}
.ap-nag-x:hover {
  color: var(--accent-ink);
}
.ap-install-nag-kbd {
  font-weight: 600;
  white-space: nowrap;
}
/* The update strip's one addition: the control that takes the update. It wears the
 * shared button, inset tighter and shorter than the form height — it stands in a
 * 12px bar rather than in a panel, and the bar's height is what the pinned masthead
 * sits under, so the form's 46px would push the whole app down a strip's worth. */
.ap-update-nag-take {
  flex: none;
  font-size: 13.5px;
  padding: 0 14px;
}
/* Above the phone tier only: on a phone the strip's control is a control a thumb has
 * to hit, and the floor wins over the strip's proportions. */
@media (min-width: 641px) {
  .ap-update-nag-take {
    min-height: 34px;
  }
}

/* The schema gate (#67) — the reset prompt shown INSTEAD of the app when stored
 * data is at another schema version, and the boot-failure note beside it. Lives
 * here rather than in a per-surface file because it is not a surface: it renders
 * straight into #app with no masthead and no route, which is the visible half of
 * "nothing renders as if the data were readable". Reuses the shared title,
 * eyebrow, and accent button; only the frame and the two prose weights are new. */
.ap-gate {
  max-width: var(--measure-reading);
  margin: 0 auto;
  padding: 64px var(--page-inset) 80px;
}
/* What the store actually said, which is the line a reader must read before deciding.
 * SAID BETWEEN TWO HAIRLINES, not in a flagged wash (#711): the accent-washed block with
 * a 2px accent flag was a panel drawn around content, and the accent flag is the READER'S
 * marker in this design, so the app was wearing the reader's voice. This is the same form
 * the other takeover uses for the one fact it has to state (`read.css ›
 * .ap-hydrating-state`), so the two screens a reader meets instead of the app agree. */
.ap-gate-said {
  margin: 20px 0 0;
  padding: 14px 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  font-family: var(--ui-face);
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink);
}
.ap-gate-note {
  margin: 20px 0 0;
  font-size: 17px;
  color: var(--muted);
}
/* The boot note, before the store has answered — the same frame with nothing to
 * decide yet, so it carries no title and sits where the eyebrow alone reads. */
.ap-gate-booting .ap-gate-note {
  margin-top: 0;
}
.ap-gate-actions {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  margin-top: 28px;
}
/* The minor move beside an accent control — declining, or cancelling — is the
 * OUTLINED NEUTRAL button above and states nothing of its own here. Shared because
 * two surfaces make that offer: the schema gate's "Not now" (#67) and the recovery
 * phrase's Cancel (#191). `.ap-gate-decline` stays as the gate's own hook for its
 * driver. Until #711 it was bare text, which read as a caption rather than a choice. */

/* The pre-billing predict-generation gate (#80) is shared by Import and the
 * Library. It is intentionally a compact instrument panel, not a modal: the
 * source is already safely saved, so the reader can skip or leave at any time. */
.ap-cost-gate {
  margin-top: 22px;
  padding: 18px;
  border: 1px solid var(--faint);
  background: var(--accent-wash);
}
.ap-cost-model {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  font-family: var(--ui-face);
  font-size: 12.5px;
  color: var(--faint);
}
.ap-cost-model select {
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--faint);
  border-radius: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--ui-face);
  font-size: 13.5px;
}
.ap-cost-status,
.ap-cost-estimate,
.ap-cost-note {
  margin: 14px 0 0;
}
/* Cost status is factual rather than a verdict, so it uses the prose voice. */
.ap-cost-status,
.ap-cost-note {
  color: var(--muted);
  font-size: 14px;
  line-height: 1.5;
}
/* A figure, not an instrument mark: the sans with tabular numerals, which is what
 * actually makes a quote line up (#711 retired the mono chrome; `font-variant-numeric`
 * is the part that was doing the work). */
.ap-cost-estimate {
  font-family: var(--ui-face);
  font-size: 14px;
  color: var(--accent-ink);
  font-variant-numeric: tabular-nums;
}
.ap-cost-actions {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  margin-top: 18px;
}
/* Skip wears the outlined neutral button above and states nothing of its own: it was
 * the last bare-text control on the gate, and a control with no shape beside one that
 * has a shape reads as a caption. */

/* The one shared entry animation — a margin stamp minting, a review card being
 * dealt. Stilled by the reduced-motion rule below. */
@keyframes ap-rise {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The one shared waiting animation — a model's sparkle while it reads (`.ap-machine`
 * above). A fade rather than a motion, because the glyph it rides is 16px and a moving
 * one at that size reads as a fault. Stilled by the reduced-motion rule below. */
@keyframes ap-pulse {
  50% {
    opacity: 0.25;
  }
}

/* The one shared busy animation — the sync control's glyph while a pass runs.
 * Stilled by the same rule; the control's name still says it is syncing. */
@keyframes ap-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
}
.ap-video {
  position: fixed;
  z-index: 55;
  right: 20px;
  bottom: var(--video-bottom, 12px);
  width: 320px;
  max-width: calc(100vw - 24px);
  background: var(--paper);
  color: var(--ink);
  /* A FLOATING SURFACE TAKES ONE EDGE (#711, §Frames): a 1px `--faint` border, which is
   * what a control boundary is in this design, and the chrome inside it speaks the app's
   * own sans rather than the instrument mono it wore. */
  border: 1px solid var(--faint);
  font: 13.5px var(--ui-face);
}
.ap-video[data-dragged] { right: auto; bottom: auto; }
.ap-video[hidden], .ap-video [hidden] { display: none; }
.ap-video[data-size="medium"] { width: 480px; }
.ap-video[data-size="large"] { width: 640px; }
.ap-video-bar { padding: 10px 12px 4px; cursor: move; touch-action: none; user-select: none; }
.ap-video-title { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.ap-video-controls { display: flex; align-items: center; gap: 4px; padding: 4px 8px 8px; }
/* A CONTROL THE READER PRESSES HAS A SHAPE (#711): these were bare text in the player's
 * own bar, which is the one thing this design refuses outright. They wear the shared
 * outlined-neutral button (`apparatus.css › .ap-btn-quiet`) rather than rebuilding it —
 * the border, the square corner, the weight, the hover and the 35% disabled are all its,
 * and a local copy is how a player came to disable at 45% while the rest of the app
 * disabled at 35%. Only the TYPE and the INSET are the player's — the bar is 320px wide
 * and holds six of them, so they are narrower than a form's button but exactly as tall,
 * which is also what keeps the phone tier's thumb floor satisfied by the shared rule
 * rather than fought by a local `min-height`. */
.ap-video button { font-size: 13px; padding: 0 9px; }
.ap-video button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-wash); color: var(--accent-ink); }
.ap-video-time { margin-right: auto; font-variant-numeric: tabular-nums; }
.ap-video iframe { display: block; width: 100%; aspect-ratio: 16 / 9; min-height: 200px; border: 0; pointer-events: none; }
.ap-video-unavailable { padding: 18px; min-height: 120px; }
.ap-video-unavailable a { color: inherit; }
.ap-surface[data-surface="read"] .ap-row > .ap-timecode {
  grid-column: 1;
  grid-row: var(--ap-row, auto);
  align-self: start;
  justify-self: end;
  margin-top: var(--ap-lead);
  border: 0;
  padding: 4px 0 4px 4px;
  background: transparent;
  color: var(--faint);
  /* THE ONE MONO LEFT IN THE READING COLUMN BESIDES THE `§` MARKER, and for the same
   * reason: a timecode IS a video source's Anchor, which is the second job the design
   * system keeps the instrument face for. */
  font: 11px var(--mono);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.ap-surface[data-surface="read"] .ap-row > .ap-timecode[data-lead="heading"] { margin-top: var(--ap-head-lead); }
.ap-surface[data-surface="read"] .ap-row[data-video] > .ap-rail { grid-row: 1; }
.ap-surface[data-surface="read"] .ap-timecode:focus-visible { outline: 2px solid var(--accent); }
@media (max-width: 1119px) {
  .ap-surface[data-surface="read"] .ap-row[data-video] > .ap-block { grid-row: var(--ap-row, auto); }
}
@media (max-width: 640px) {
  .ap-video, .ap-video[data-size] { width: 320px; right: 12px; bottom: var(--video-bottom, 12px); }
  .ap-video-bar { cursor: default; touch-action: auto; }
  .ap-video-size { display: none; }
  .ap-surface[data-surface="read"] .ap-row[data-video] { grid-template-columns: 42px minmax(0, 1fr); column-gap: 10px; }
  .ap-surface[data-surface="read"] .ap-row[data-video] > * { grid-column: 2; }
  .ap-surface[data-surface="read"] .ap-row[data-video] > .ap-timecode { grid-column: 1; font-size: 10px; }
  .ap-surface[data-surface="read"] .ap-row[data-video] > .ap-rail { grid-column: 1 / -1; }
}
/* Per-surface stylesheet: Library (#/library) — the reader's texts and nothing else
 * (#711). Two bands: Continue reading, set larger, and the shelf, one line a Source,
 * under a heading row that Find takes over. Nothing is boxed and nothing is counted.
 * Shared tokens and the three components live in apparatus.css; only the Library's own
 * rhythm is here, scoped under the surface attribute so nothing leaks. The column is
 * `.ap-surface`'s own — this surface asks for no width of its own. */

/* —— Continue reading ————————————————————————————————————————————————————
 * Up to three, so it may be roomy. Each row names the Section the reader's Reading
 * position is in: a place, drawn behind a bookmark, never a ratio or a duration. */
.ap-surface[data-surface="library"] .lb-current {
  margin-bottom: 36px;
}
.ap-surface[data-surface="library"] .lb-row.lb-open {
  display: block;
  padding: 20px 0 21px;
}
.ap-surface[data-surface="library"] .lb-row.lb-open:first-child {
  padding-top: 0;
}
.ap-surface[data-surface="library"] .lb-row.lb-open .t {
  display: block;
  font-size: 25px;
  line-height: 1.22;
  letter-spacing: -0.006em;
}
.ap-surface[data-surface="library"] .lb-row.lb-open .m {
  display: block;
  margin-top: 3px;
}
/* Where the reader was. The glyph is the accent's one job on this row; the name beside
 * it is the Section's, in the app's own voice, because the app is the one saying it. */
.ap-surface[data-surface="library"] .lb-at {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-top: 10px;
  font-family: var(--ui-face);
  font-size: 16px;
  line-height: 1.4;
  color: var(--muted);
}
.ap-surface[data-surface="library"] .lb-at .ap-icon {
  flex: none;
  width: 16px;
  height: 16px;
  margin-left: -2px;
  color: var(--accent);
  transform: translateY(2.5px);
}

/* —— The shelf's heading row ——————————————————————————————————————————————
 * The switch on one side, the three shelf actions on the other, and one hairline under
 * them. Find opens INTO this row at the row's own height, so nothing below it moves;
 * the hairline turns accent to say which row the query belongs to. */
/* It wraps only where it must. At the maintained phone baseline the switch and the
 * three actions fit one line, which is what lets Find take the row over at the row's own
 * height; below that — 320px, the narrowest phone in common use — the actions drop to a
 * second line rather than pushing the document sideways. */
.ap-surface[data-surface="library"] .lb-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  min-height: 60px;
  border-bottom: 1px solid var(--rule);
}
.ap-surface[data-surface="library"] .lb-head[data-finding] {
  border-bottom-color: var(--accent);
}
.ap-surface[data-surface="library"] .lb-seg-b {
  padding: 7px 14px;
  font-size: 13.5px;
}
.ap-surface[data-surface="library"] .lb-tools {
  display: flex;
  align-items: center;
  gap: 20px;
}
@media (max-width: 640px) {
  .ap-surface[data-surface="library"] .lb-tools {
    gap: 14px;
  }
}
/* The shared named glyph (`apparatus.css › .ap-tool`) at this row's size — only the
 * glyph's own measure is the shelf's. */
.ap-surface[data-surface="library"] .ap-tool .ap-icon {
  width: 20px;
  height: 20px;
}
/* Sort's own picker is the platform's: a real `<select>` lying invisible over the
 * glyph, so a phone opens its wheel and a desktop its list, and the glyph underneath is
 * the state. 16px on the select because below that iOS zooms the page on focus. */
.ap-surface[data-surface="library"] .lb-sort {
  position: relative;
}
.ap-surface[data-surface="library"] .lb-sort select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  font-size: 16px;
  cursor: pointer;
}
.ap-surface[data-surface="library"] .lb-sort:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Find, open. It is the one thing a reader types into on this surface, so it wears the
 * shared field's indicator and not the row's: `--faint` under it at rest, the doubled
 * accent when focus is inside it (`apparatus.css › .ap-field`). The heading row's own
 * hairline turning accent says which ROW the query belongs to and is on for as long as
 * Find is; it cannot also say where the caret is. The field is not `.ap-field` itself
 * because that one is 46px tall in a form, and this one has to fit the row it takes
 * over at the row's own height — which is the whole point of the takeover. */
.ap-surface[data-surface="library"] .lb-find {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--faint);
  color: var(--faint);
}
.ap-surface[data-surface="library"] .lb-find:focus-within {
  padding-bottom: 6px;
  border-bottom: 2px solid var(--accent);
  color: var(--accent);
}
.ap-surface[data-surface="library"] .lb-find > .ap-icon {
  flex: none;
  width: 18px;
  height: 18px;
}
.ap-surface[data-surface="library"] .lb-find-in {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--reading-face);
  font-size: 17px;
  line-height: 1.4;
}
.ap-surface[data-surface="library"] .lb-find-in:focus {
  outline: none;
}
.ap-surface[data-surface="library"] .lb-find-in::placeholder {
  color: var(--faint);
  opacity: 1;
}
.ap-surface[data-surface="library"] .lb-find-in::-webkit-search-cancel-button {
  -webkit-appearance: none;
}
.ap-surface[data-surface="library"] .lb-find-n {
  flex: none;
  font-family: var(--ui-face);
  font-size: 13.5px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* —— The shelf ———————————————————————————————————————————————————————————
 * One line a Source: the title in the reading Face, the author right-aligned on its
 * baseline. It is the last thing on the page, so nothing waits under a list that grows. */
.ap-surface[data-surface="library"] .lb-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 24px;
  align-items: baseline;
  padding: 10px 0 9px;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
}
.ap-surface[data-surface="library"] .lb-row .t {
  min-width: 0;
  font-size: 17.5px;
  line-height: 1.35;
  text-wrap: pretty;
  color: var(--ink);
  transition: color 0.12s;
}
.ap-surface[data-surface="library"] .lb-row:hover .t {
  color: var(--accent);
}
.ap-surface[data-surface="library"] .lb-row .m {
  font-family: var(--ui-face);
  font-size: 14.5px;
  line-height: 1.4;
  color: var(--faint);
}
/* Find reads the whole library, so a Source the reader has finished with can answer a
 * query — muted, because it has already had its turn. */
.ap-surface[data-surface="library"] .lb-row.is-digested .t {
  color: var(--muted);
}
/* A text whose questions have not been made yet. Faint, and after the title rather than
 * before it, so a shelf of them still reads as a list of titles. */
.ap-surface[data-surface="library"] .lb-unmade {
  display: inline-flex;
  vertical-align: -2px;
  margin-left: 8px;
  color: var(--faint);
}
.ap-surface[data-surface="library"] .lb-unmade .ap-icon {
  width: 14px;
  height: 14px;
}

.ap-surface[data-surface="library"] .lb-empty,
.ap-surface[data-surface="library"] .lb-standing {
  margin: 40px 0 0;
  color: var(--muted);
  font-size: 19px;
  line-height: 1.5;
}

/* The invitation a library with nothing in it stands on. */
.ap-surface[data-surface="library"] .lb-invitation {
  margin: 0;
}
.ap-surface[data-surface="library"] .lb-invitation h2 {
  margin: 0;
  font-family: var(--reading-face);
  font-size: 27px;
  line-height: 1.22;
  font-weight: 500;
  color: var(--ink);
}
.ap-surface[data-surface="library"] .lb-invitation p {
  margin: 10px 0 24px;
  max-width: 52ch;
}
.ap-surface[data-surface="library"] .lb-invitation .lb-empty-secondary {
  margin: 20px 0 0;
  font-size: 15px;
}

/* At a phone's width the row is ONE line that never wraps: the title takes what it
 * needs and the author takes what is left, up to a share of the row, ellipsising rather
 * than breaking. It never breaks inside a name — an orphaned "…and Michael" is the
 * failure this rule exists for — and it never drops to a line of its own, which the
 * inline run it replaced did whenever the title happened to fill the width. A title
 * that wraps to two lines keeps the author on its FIRST baseline, as the wide tier
 * already does, because both are flex items on one baseline. */
@media (max-width: 640px) {
  .ap-surface[data-surface="library"] .lb-row {
    display: flex;
    flex-wrap: nowrap;
    align-items: baseline;
    column-gap: 8px;
  }
  .ap-surface[data-surface="library"] .lb-row .t {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 17px;
  }
  .ap-surface[data-surface="library"] .lb-row .m {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 45%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Continue reading's row is roomy at every width: its three lines stack. */
  .ap-surface[data-surface="library"] .lb-row.lb-open {
    display: block;
  }
  .ap-surface[data-surface="library"] .lb-row.lb-open .t {
    display: block;
    font-size: 22px;
  }
  .ap-surface[data-surface="library"] .lb-row.lb-open .m {
    display: block;
    max-width: none;
    white-space: normal;
  }
}
/* Per-surface stylesheet: Reading (#/read/<id>) — the full Apparatus (#28),
 * classes ported from design/recur-apparatus.html + recur-margin-authoring.html.
 * Three columns per `.ap-row` grid: instrument rail · prose column · commentary
 * margin. The PROSE column is the one content column every surface stands on, and the
 * other two hang in the gutters the viewport leaves beside it: the rail from 880px, the
 * margin from 1180px (#711). Below each width that track comes back into the flow — the
 * `§` above the section's heading, the margin behind a mono disclosure; ≤640px is a
 * supported single column. Since ADR 0009 the
 * fold holds only DISPLAYED entries — the authoring form is the bottom dock's second
 * height at every width — so no rule here maintains authoring parity across tiers.
 * Tokens/keyframes/shared classes stay in apparatus.css; only the surface's own
 * layout, margin, and forms live here.
 *
 * A SECTION ROW IS ONE GRID ROW PER BLOCK since #118, so a margin item can stand level
 * with the block it glosses (ADR 0011). Every element of a section — station, blocks,
 * compare, popovers, margin groups — is a direct child of `.ap-row`, and
 * `surfaces/read.ts › rowStyle` stamps each with the `--ap-row` the wide tier places it
 * at. Two consequences run through the rules below. NOTHING SHARING THE COLUMN'S RHYTHM
 * CARRIES A BOTTOM MARGIN — grid items' margins do not collapse, so an element owning
 * the space on both sides would double every gap it shares; the rhythm is the
 * `--ap-lead` each element owns ABOVE itself, and the section row owns the space below
 * its last block. Two things stand outside that rhythm and do carry one: inside a group,
 * ordinary block flow resumes and the margin's entries stack on their own bottom gutters
 * (only the last one's is cancelled, because it would stretch a grid row for nothing);
 * and the folded tier's `▸ Margin · N` handle, which is chrome that exists only below
 * 1180px and owns its own separation from what it discloses. And a margin ITEM can no
 * longer outgrow the block it glosses: ADR 0011 accepted row-stretch as the price of a
 * placement that needs no measure pass, and ADR 0013 bounds it at the wide tier, where a
 * reader's 3,501-character note had turned that price into 2,157px of empty prose column
 * (#127). Per item, so a block carrying three long notes still spreads its row by three
 * bounded cells — what the bound removes is the term that grew with how much the reader
 * wrote. The rules are at the foot of this file with the rest of the wide tier.
 *
 * SOURCE-ORDER GOTCHA (design system §3, a real bug fixed once): the wide-tier
 * `@media (min-width:1180px)` block MUST come after the base `.ap-row` rules —
 * same specificity, later source order wins. Put it before and the grid stays
 * 2-column and the margin wraps under the rail. The media blocks are at the
 * foot of this file for exactly that reason. */

.ap-surface[data-surface="read"] {
  /* THIS SURFACE IS THE ONE CONTENT COLUMN, like every other (#711). It used to state a
   * SHELL — the measure plus the tracks beside it — which made the reading column narrower
   * than the 680px every other surface stands on, so the prose, the masthead's row and the
   * bottom dock sat on three different axes at the same width. The rail and the commentary
   * margin hang OUTSIDE the column now, in the gutters the viewport leaves, exactly as the
   * prototype does (`design/recur-contents.html`: one 680/32 column for the masthead, the
   * bar, the prose and the dock, with the `§` and the margin absolutely beside it). So
   * there is no `max-width` here at all: `.ap-surface` already says `--measure-page`, and
   * the width dial moves that token, so the dial reaches this surface through the same
   * channel it reaches the Library through.
   *
   * THE APPARATUS GRID'S TRACKS, named once. A row of the text and the reading bar above it
   * stand on the same grid — the readout starts where a paragraph starts and the tools end
   * where one ends — and the tiers restate the tracks here rather than on each element that
   * takes them. `--ap-prose-column` is the column both put their content in, and the two
   * OUTDENTS are what push the other tracks into the gutters: a row is wider than the
   * column it is read in, by exactly the tracks that stand beside it.
   *
   * EACH OUTDENT HAS A TIER BECAUSE EACH NEEDS ROOM. The gutter a track hangs in is
   * `(100vw - --measure-page) / 2 + --page-inset`, so the rail's 106px needs 880px of
   * viewport and the margin's 226px needs 1180px — the width the design system already
   * names for wide-screen margin entries. Below its own width a track comes back into the
   * flow: the `§` stands above the section's heading, and the margin folds. */
  --ap-row-columns: minmax(0, 1fr);
  --ap-prose-column: 1;
  --ap-row-outdent-start: 0px;
  --ap-row-outdent-end: 0px;
  /* NO TOP PADDING OF ITS OWN (#711): the reading bar is the surface's first element and
   * sticks at the masthead's own bottom edge, so it has to start there. The column's
   * clearance below it is the header row's, and the blocked read states its own. */
  padding-top: 0;
  padding-bottom: 140px; /* room for the fixed bottom dock */
  /* WHERE THE VIEWPORT'S USABLE TOP IS — the masthead's measured height (`surfaces/read.ts
   * › measurePosition` publishes `--position-top` on the root) with the literal beside it
   * covering the frame before the first measurement. Both pieces of chrome seated under
   * the bar read this rather than the pair: the position readout at the left corner and
   * the Aa control at the right (#351) are stated to be at the same seat, and four copies
   * of one fallback is how two elements that must agree come to sit 0.9px apart the day
   * the wordmark changes size. */
  --ap-chrome-top: var(--position-top, 52px);
  /* Where the chrome ENDS: the masthead's height plus the reading bar's, published by the
   * same pass and read by the two panels that hang below the whole of it. The fallback
   * beside it covers the frame before the first measurement, and it is the masthead's own
   * fallback plus the bar's designed height rather than a third literal that could come to
   * disagree with the first. */
  --ap-chrome-bottom: var(--chrome-height, calc(var(--ap-chrome-top) + 57px));
  /* The column's LEAD: the space a prose element owns above itself, and the same space a
   * margin group owns — which is the whole mechanism behind "level with its anchor"
   * (ADR 0011). One declaration, because two 18s that must move together is two 18s that
   * can stop agreeing. A heading opens a passage and owns more; its groups take the same
   * larger lead (`surfaces/read.ts › leadOf` names which, this file says how big). */
  --ap-lead: 18px;
  --ap-head-lead: 30px;
  /* A station's, a compare's and a check's clearance from the prose they sit between — a
   * different voice arriving wants more room than the lead, and since #711 that space is
   * the WHOLE separation: the plate fill and the hairline flag that used to draw the
   * boundary are gone, so what is left has to be big enough to be read as a boundary. */
  --ap-station-gap: 40px;
  /* THE MACHINE PROSE'S TYPE (#381). A station, its compare and a comprehension check are
   * MACHINE-AUTHORED PROSE STANDING IN THE READING COLUMN, so the size and spacing dials
   * have to reach them the way they reach `.ap-block` — the literal 16px/1.5 these two
   * tokens replaced could not hear a dial at all, and a plate that grew around type that
   * did not is what that defect looked like to a reader.
   *
   * A STEP OFF THE DIAL, not the dial itself: this prose is set a shade under the column
   * by design, and a flat `var(--reading-size)` would iron out the hierarchy instead of
   * moving it. Every other size in these blocks is a PIXEL offset on this one, so one
   * number carries the whole scale and each step keeps its relation to the others. A pixel
   * offset and not a ratio of the reading size, because a ratio of 19px does not land on
   * the designed value; the offset does, and it stays an ABSENCE here as it is in
   * `apparatus.css`. The lines that are not this size are offsets ON this leading, because
   * a line-height is already a ratio and a ratio of a ratio says nothing a reader of this
   * file can check.
   *
   * #711 RETUNED BOTH, because the dress the numbers served is gone: the blocks were
   * plates a step smaller than the column so a panel would not shout over the prose, and
   * a station is now part of the page. 18.5/1.6 is what the design draws (`design/
   * recur-contents.html › .cmp-row p`), and the question above it steps UP rather than the
   * block stepping down.
   *
   * The leading is a token of its own because this line is tighter than the column's, and
   * because it must not be set on the block ELEMENT: the chrome's own captions inside state
   * no line-height, so a container declaration would silently retune them. What the dials
   * still may not touch is those captions' scale and the reader-voice italic's FACE — the
   * two-voice rule (`apparatus.css › THE READING DIALS`), which is about the face and never
   * about the size. */
  --ap-station-size: calc(var(--reading-size) - 0.5px);
  --ap-station-leading: calc(var(--reading-leading) - 0.12);
  /* HOW MUCH ROOM THE RESPONSE ROW HOLDS while its call is out, so the Notice under it
   * stands in one place from the reveal onward (`dev/e2e-response.mjs ›
   * arrivalDoesNotMoveTheNotice`). It is a reservation and therefore a guess, and the
   * only honest way to size it is against the prose it reserves for: a Response is two
   * or three sentences, which is four lines of the row's own type in this column.
   * It was 6lh, tuned when the row carried a label line above the words and was set a
   * size smaller; at the row's new type that left about three blank lines under a
   * settled Response, which reads as a missing element rather than as a held place. */
  --ap-response-reserve: 4lh;
}
/* THE READER'S VOICE is `apparatus.css › .ap-voice` since #711 — the elements that
 * wear it carry the class, and this file states only their rhythm. It used to be a
 * selector list here, which made the voice a fact about this surface; it is a fact
 * about the app, and the Library, Review and the dock all need it.
 * The selection colour left with it, for the same reason.
 *
 * Title itself is the shared .ap-surface-title; only the column's rhythm here. */
/* THE HEAD IS THE TITLE AND ONE BYLINE LINE (#711, story 43), so the first sentence
 * arrives early. The clearance the surface no longer carries at the top is here, under
 * the bar the title now follows. */
.ap-surface[data-surface="read"] .ap-reading-head {
  padding-top: 26px;
}
.ap-surface[data-surface="read"] .ap-reading-title {
  margin-bottom: 0;
}
.ap-surface[data-surface="read"] .ap-reading-by {
  margin: 8px 0 0;
  font-family: var(--ui-face);
  font-size: 15px;
  color: var(--faint);
}
/* Set once, and shown at the head only once it is set: a reader reopening a finished
 * text can clear it without traversing the text (#349). */
.ap-surface[data-surface="read"] .ap-reading-head .ap-digest {
  margin-top: 10px;
}
.ap-surface[data-surface="read"] .ap-reading-standing {
  color: var(--muted);
  font-size: 18.5px;
}

/* The blocked read (#198, §11) — the full-page takeover for a source whose text
 * is not on this device yet. Only the rhythm here; the title is the shared
 * `.ap-surface-title` and the button is the shared `.ap-btn-accent`.
 *
 * It has no progress BAR, and that is a decision rather than a gap. A bar is a
 * promise about a finish, and the queue cannot make one: the wait usually ends
 * when another device opens the app, which is an event no measurement here can
 * predict. A byte count is a fact, so the reader gets the fact.
 *
 * The source's own title is the one line in the reader's own voice, so it is
 * serif italic like every other piece of the reader's content (§Shared shell
 * chrome, the two-voice rule). Everything around it is the instrument speaking. */
.ap-surface[data-surface="read"] .ap-hydrating {
  padding-top: 40px;
  max-width: 40em;
}
.ap-surface[data-surface="read"] .ap-hydrating-source {
  font-size: 20px;
  color: var(--prose-ink);
  margin-bottom: 18px;
}
/* What is missing, said between two hairlines — the app speaking, in its own sans since
 * #711: it was the instrument's tracked mono, which made the one line a reader has to
 * read here the hardest one on the screen to read. */
.ap-surface[data-surface="read"] .ap-hydrating-state {
  font-family: var(--ui-face);
  font-size: 15px;
  color: var(--ink);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: 14px 0;
}
.ap-surface[data-surface="read"] .ap-hydrating-standing {
  color: var(--muted);
  font-size: 16px;
  margin-top: 18px;
}
.ap-surface[data-surface="read"] .ap-hydrating-acts {
  display: flex;
  align-items: center;
  gap: 18px;
  margin-top: 22px;
}

/* THE QUIET GLYPH ACTION (#711, story 56) — a glyph and its name, no frame. It is the
 * ONE exception the design system's button rule names: every other control the reader
 * presses has a shape, and the reading view's foot actions carry glyphs instead, so that
 * "Mark digested" and "Copy dossier" are there at the end of a text without shouting
 * across the last paragraph. They were boxed tracked-monospace capitals, which is three
 * of the things #711 retires in one control.
 *
 * Only the judgment has a pressed state, and hover must not render like it, or a
 * pointer resting on an undigested source shows the reader their own judgment
 * reflected back: hover moves the ink only, and pressed takes the accent outright. (A
 * touch device sees only the pressed state, which has to carry meaning on its own.) */
.ap-surface[data-surface="read"] .ap-act {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  font-family: var(--ui-face);
  font-size: 15px;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s;
}
.ap-surface[data-surface="read"] .ap-act:hover {
  color: var(--ink);
}
.ap-surface[data-surface="read"] .ap-digest[aria-pressed="true"] {
  color: var(--accent);
}

/* THE FOOT IS ONE ROW OF ACTIONS over one quiet line (#711). Both controls act on the
 * whole text, so they stand together above the line that measures what they copy —
 * `display: contents` twice, because the Dossier's own grouping is about which controls
 * come from which feature and the foot's is about what a reader sees: three actions in a
 * row, then the size. */
.ap-surface[data-surface="read"] .ap-reading-foot-acts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 28px;
  margin-top: 40px;
  padding-top: 6px;
  border-top: 1px solid var(--rule);
}
.ap-surface[data-surface="read"] .ap-dossier,
.ap-surface[data-surface="read"] .ap-dossier-acts {
  display: contents;
}
.ap-surface[data-surface="read"] .ap-dossier-size,
.ap-surface[data-surface="read"] .ap-dossier-note {
  flex: 1 1 100%;
}
.ap-surface[data-surface="read"] .ap-dossier-size,
.ap-surface[data-surface="read"] .ap-dossier-note,
.ap-surface[data-surface="read"] .ap-dossier-feedback {
  font-family: var(--ui-face);
  font-size: 13px;
  color: var(--faint);
  margin: 0;
}

/* The generation controls (#380, #642) — in the About sheet since #711, one row a run,
 * because per-text upkeep lives with the text and not across the reading title.
 *
 * They stay TERTIARY at rest even though Regenerate is destructive, and NO DANGER
 * COLOUR IS INVENTED for it: this palette has none on purpose (see the popover's
 * Delete handle below), and the finality is said in words instead — the warning's
 * own sentence, which names the count and ends "This cannot be undone." The one
 * thing that changes weight is the count itself, stepped to `--ink` inside a
 * `--muted` line so the number a reader is being asked about is the number that
 * reads first.
 *
 * IT STATES NO TYPE OF ITS OWN (#380 meeting #381). #381 made the machine-authored
 * blocks — the station, its compare, the check — take their type from
 * `--ap-station-size`/`--ap-station-leading`, because those blocks are PROSE the reader
 * reads in the column, and the Size and Spacing dials must reach them. This is the
 * other kind of thing: a control at the head of the source, beside `.ap-digest`, and
 * the panel it hands over to is the SHARED cost gate, which is `apparatus.css`'s and
 * is fixed type on all three surfaces that mount it. So its two prose lines wear the
 * gate's own `.ap-cost-note` rather than restating its declarations or deriving from
 * a dial the gate cannot hear — a warning that grew while the gate one element below
 * it stayed put would be one flow disagreeing with itself mid-confirmation. Nothing
 * below sets a size; what is left here is the box, the seat and the emphasis.
 *
 * The head's control is the shared outlined-neutral button since #711 (`.ap-btn-quiet`):
 * it is the alternative in a row whose main action is the Confirm the Cost gate will
 * offer, and "Make one" beside a row's own name is a control a reader must be able to
 * tell from a caption. A SEAT's control is still the tertiary station handle below. */
.ap-surface[data-surface="read"] .ap-gen {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 12px;
}
/* The button is the row's value, so it is tightened to the row's scale — but never below
 * the thumb floor, which is one rule for every control in the app and not this row's to
 * relax (`apparatus.css` § the phone tier). */
.ap-surface[data-surface="read"] .ap-listrow .ap-gen-open {
  padding: 0 14px;
  font-size: 14px;
}
/* A done run, drawn: the check and the word, with the act still beside them. */
.ap-surface[data-surface="read"] .ap-gen-made {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--accent);
}
.ap-surface[data-surface="read"] .ap-gen-made .ap-icon {
  width: 16px;
  height: 16px;
}
/* Why a run that ended unavailable ended: a sentence, where the row's other values are a
 * word and a control, so the value's corner of the row is allowed to wrap and the
 * sentence takes its own line above the act it explains. */
.ap-surface[data-surface="read"] .ap-about-run .ap-listrow-v {
  flex-wrap: wrap;
  justify-content: flex-end;
}
.ap-surface[data-surface="read"] .ap-gen-stopped {
  flex: 1 1 100%;
  text-align: right;
}
/* The warning and the gate mount inside this same group, and each takes the whole
 * width — only the resting buttons stand side by side. */
.ap-surface[data-surface="read"] .ap-gen > :not(.ap-gen-open) {
  flex: 1 1 100%;
}
/* THE WARNING AND THE GATE ARE NOT A PANEL ON THE PAGE (#711). This was a
 * `--rule-strong` box on a `--plate` fill, and it mounts inline at a station's seat, so
 * it was the last framed thing in the reading column. What separates it now is what
 * separates everything else here: a `--rule` hairline above it and room under that. */
.ap-surface[data-surface="read"] .ap-gen-panel {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--rule);
}
/* Colour, size and leading all arrive with `.ap-cost-note`; only the seat is this
 * file's. Its own margin is dropped because the group above already sets the inset. */
.ap-surface[data-surface="read"] .ap-gen-panel-line {
  margin: 0;
}
/* The count the reader is being asked about, stepped out of the `--muted` line it
 * stands in. Weight and colour, never size — the line's scale is the gate's. */
.ap-surface[data-surface="read"] .ap-gen-panel-line b {
  color: var(--ink);
  font-weight: 600;
}
/* The warning's actions row and its dismiss wear the gate's own `.ap-cost-actions` /
 * `.ap-cost-skip` (apparatus.css) — the habit this file already keeps for
 * `.ap-btn-accent`. Copies here would sit one element above the gate they came from,
 * which is the worst place for two definitions to drift apart. */
.ap-surface[data-surface="read"] .ap-gen-status {
  margin: 10px 0 0;
}
/* The same control, seated at a station or a check. A NAMED GLYPH (`.ap-tool`) since
 * #711, where the head's is the outlined-neutral button: at the head it is the
 * alternative in a row whose main action is the Cost gate's Confirm, and here it acts on
 * the block it is standing in, beside the one filled verb that block is asking for. It
 * was a tertiary bare-text handle, which is the one thing this design refuses outright.
 * What is this file's is its place — a station is a flex column, so it stands at the
 * start of its own line rather than stretched across it. */
.ap-surface[data-surface="read"] .ap-gen-seat {
  align-self: flex-start;
  margin-top: 4px;
}
/* The plate's gap already separates it, so it drops the head's margin. */
.ap-surface[data-surface="read"] .ap-gen-seated {
  margin-top: 0;
}
/* The warning's secondary line — "Regenerate all … instead" — is a minor alternative to
 * the Continue beside it, so it wears the outlined neutral and stands on its own line. */
.ap-surface[data-surface="read"] .ap-gen-panel > .ap-gen-all-instead {
  margin-top: 12px;
}

/* —— The apparatus grid ——————————————————————————————————————————————————— */
.ap-surface[data-surface="read"] .ap-row {
  display: grid;
  grid-template-columns: var(--ap-row-columns);
  column-gap: 30px;
  align-items: start;
  margin-inline: var(--ap-row-outdent-start) var(--ap-row-outdent-end);
}
/* A section row owns the space BELOW its last block, which the block used to own
 * itself: prose rhythm is space-above now (see the file header), and without this two
 * sections would sit a paragraph apart instead of a section apart. The header row has
 * no blocks and wants none of it. */
.ap-surface[data-surface="read"] .ap-row[data-section-id] {
  padding-bottom: var(--ap-lead);
}
/* The prose column is the default a row's children fall into; the rail and the margin
 * each name their own track below and win on specificity, never on source order. (A
 * `*:not(.ap-rail)` here would out-specify both — it did, and the margin rendered across
 * the prose until `dev/e2e-mark.mjs` tapped one.) `min-width: 0` is what the removed
 * `.ap-body` wrapper used to carry for the whole column at once — a `1fr` track's
 * minimum is its content, so a code block's `overflow-x` needs each prose element
 * allowed to shrink below it. */
.ap-surface[data-surface="read"] .ap-row > * {
  grid-column: var(--ap-prose-column);
  min-width: 0;
}
/* THE `§` SECTION MARKER is the one job the monospace keeps on this surface, beside a
 * rendered Anchor (#711, story 45): 11px `--faint`, hanging in the left margin here and
 * standing above the section's heading at the phone tier, where there is no margin to hang
 * in. It used to be the loudest thing in the rail — 600 weight, `--muted`, tracked — which
 * made an instrument mark compete with the heading beside it. */
.ap-surface[data-surface="read"] .ap-rail {
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.9;
  letter-spacing: 0;
  color: var(--faint);
  text-align: left;
  font-variant-numeric: tabular-nums;
}
.ap-surface[data-surface="read"] .ap-rail b {
  display: inline;
  font-weight: 400;
  margin-right: 9px;
}

/* IN THE FLOW BELOW 880px, HUNG IN THE GUTTER ABOVE IT. The marker is the instrument's,
 * and the instrument stands beside the text rather than in it — but only where the
 * viewport leaves a gutter wide enough to hold the rail's own 76px measure plus its 30px
 * gap. Below that it comes back into the flow, above the section's heading, which is where
 * `surfaces/read.ts › sectionRow` emits it. */
@media (min-width: 880px) {
  .ap-surface[data-surface="read"] .ap-row > .ap-rail {
    grid-column: 1;
    /* Spans its section's whole grid (`--ap-span`, stamped by `surfaces/read.ts ›
     * sectionRow`) so the rail's own two lines cannot inflate row 1 and push the first
     * block down; `align-items: start` keeps its text at the section's head. */
    grid-row: var(--ap-row, 1) / span var(--ap-span, 1);
    text-align: right;
    padding-top: 8px;
    white-space: nowrap;
    line-height: 1.65;
  }
  .ap-surface[data-surface="read"] .ap-rail b {
    display: block;
    margin-right: 0;
  }
  .ap-surface[data-surface="read"] {
    --ap-row-columns: 76px minmax(0, 1fr);
    --ap-prose-column: 2;
    --ap-row-outdent-start: -106px;
  }
}

/* A bare `§1` directly under the title says nothing a reader wants: in the flow the
 * marker stands above a heading, and the first section of a headingless source has none. */
.ap-surface[data-surface="read"]
  .ap-reading-head
  + .ap-row[data-section-id]:not(:has(.ap-block-heading))
  > .ap-rail:not(:has(.ap-resumed)) {
  display: none;
}
@media (min-width: 880px) {
  .ap-surface[data-surface="read"]
    .ap-reading-head
    + .ap-row[data-section-id]:not(:has(.ap-block-heading))
    > .ap-rail:not(:has(.ap-resumed)) {
    display: block;
  }
}

/* "You were here" (#70) — the rail's own voice: state about your reading, in the
 * accent because it is the one thing on the row that is about the reader rather
 * than the text. Retired by their next scroll, so it never becomes furniture. */
.ap-surface[data-surface="read"] .ap-resumed {
  display: block;
  color: var(--accent);
}

/* —— One break rule, for the whole surface ——————————————————————————————————
 * On the surface root and inherited, rather than on each element that turns out to hold an
 * address: two elements carried it one at a time and every other text host still panned
 * the document (#705). A code block is exempt without an exemption, since `white-space:
 * pre` leaves nothing to break and it scrolls inside itself instead. */
.ap-surface[data-surface="read"] {
  overflow-wrap: anywhere;
}

/* —— Prose column ————————————————————————————————————————————————————————— */
/* Each element owns the space ABOVE it and none below (see the file header): every
 * block is its own grid row now, and grid items' margins do not collapse. */
.ap-surface[data-surface="read"] .ap-block {
  margin: var(--ap-lead) 0 0;
  /* The reading dials (#351), named here rather than inherited from `body` because
   * this is the prose the dials are ABOUT: the two literals these replaced were a
   * second copy of the same 19px/1.72, and the whole point of the dials is that the
   * designed values have one home (`apparatus.css › :root`). */
  font-size: var(--reading-size);
  line-height: var(--reading-leading);
  color: var(--prose-ink);
  text-wrap: pretty;
}
/* Web-link provenance (#45) — an app-voice MARGIN item since #118, standing level with
 * the block it was captured from (ADR 0011): a gloss that is not level with what it
 * glosses is a footnote wearing a margin's clothes. Everything about how it looks is
 * #45's and only the column changed — which is why it now takes the rule-strong flag and
 * the gutter of §The commentary margin instead of declaring its own, and
 * inherits the group's mono voice. What is left here is what is genuinely the links'
 * own: a wrapping row, at their own smaller size. */
.ap-surface[data-surface="read"] .ap-block-links {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  font-size: 12px;
}
/* Sentence case in the chrome's sans since #711: it named a category in the same tracked
 * capitals a control wore, which is what made neither of them legible. */
.ap-surface[data-surface="read"] .ap-block-links-label {
  color: var(--faint);
  font-family: var(--ui-face);
}
.ap-surface[data-surface="read"] .ap-block-link {
  color: var(--accent);
  text-decoration: none;
}
.ap-surface[data-surface="read"] .ap-block-link:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
/* THE AUTHOR'S HEADINGS ARE THE AUTHOR'S (#711, story 44). They were set in the chrome's
 * tracked monospace capitals, which is how a reader scrolls past a section break without
 * noticing it: a heading looked like a label about the text rather than a line the author
 * wrote. So the reading Face, the page's ink, weight 400 — the size alone carries the
 * level, which is `reading/headings.ts`'s to decide and the element's to say.
 *
 * THE SIZES ARE THE DESIGN SYSTEM'S, WITH A FLOOR ON THE READING SIZE. 27/1.22 and
 * 21.5/1.3 are the designed steps at the designed 19px prose, and they are what these
 * resolve to there. A reader who dials the type up moves the prose and not a literal, so
 * each step keeps its distance above the prose instead of sinking into it — the spec's
 * "no smaller than the prose size", said as the arithmetic that keeps it true. */
.ap-surface[data-surface="read"] .ap-block-heading {
  margin: var(--ap-head-lead) 0 0;
  font-family: var(--reading-face);
  font-weight: 400;
  letter-spacing: -0.008em;
  color: var(--ink);
  text-wrap: balance;
}
.ap-surface[data-surface="read"] h2.ap-block-heading {
  font-size: max(27px, calc(var(--reading-size) + 8px));
  line-height: 1.22;
}
.ap-surface[data-surface="read"] h3.ap-block-heading {
  font-size: max(21.5px, calc(var(--reading-size) + 2.5px));
  line-height: 1.3;
}
.ap-surface[data-surface="read"] blockquote.ap-block {
  margin: var(--ap-lead) 0 0 12px;
  padding-left: 14px;
  border-left: 2px solid var(--rule-strong);
  color: var(--muted);
}
/* Code block (Markdown, spec 03) — verbatim whitespace in the mono; overflows scroll
 * rather than break the reading measure.
 *
 * NOT A PANEL IN THE PROSE (#711): this was a hairline box on a `--plate` fill, and a
 * code block is the AUTHOR'S CONTENT, which this design never frames — a frame is what
 * tells a reader something is pressable. The mono is the one thing that says this text is
 * verbatim, and it is admitted here for the reason the design system admits it for the
 * Sync code and the recovery phrase: a reader reads it character by character. What says
 * where it begins and ends is one `--rule` hairline on its leading edge, which is what a
 * reader needs while it scrolls sideways, and the prose lead above it. */
.ap-surface[data-surface="read"] pre.ap-block-code {
  margin: var(--ap-lead) 0 0;
  padding: 2px 0 2px 16px;
  border-left: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--ink);
  white-space: pre;
  overflow-x: auto;
}
/* EPUB image (#32, spec 03) — constrained to the column measure, never wider;
 * a hairline frame so alt-text fallback (broken/evicted blob) reads as a missing
 * figure, not stray prose. THE FRAME IS THE PICTURE'S OWN EDGE and survives #711's
 * retirement of the box: it is not a panel drawn around content but the rectangle
 * standing in for one. The alt text inside it is the app reporting, so it is the
 * app's sans and no longer the instrument's mono. */
.ap-surface[data-surface="read"] img.ap-block-image {
  display: block;
  max-width: 100%;
  height: auto;
  background: var(--plate);
  border: 1px solid var(--rule);
  color: var(--muted);
  font-family: var(--ui-face);
  font-size: 13.5px;
}
/* The image BLOCK (ADR 0029) is the `<figure>` holding the picture and, below it, the mark
 * an entry anchored here leaves. It declares nothing of its own: it carries `.ap-block`,
 * whose `margin` shorthand supplies the lead and takes the figure's UA margin with it.
 *
 * The picture as a tap target: the only way to author about an image, since there is
 * nothing in one to select. A real button, unlike the inline `<mark>` above — a
 * block-level control fragments nothing — so the keyboard, the focus ring and the ARIA are
 * the platform's (ADR 0003) and only the UA's own chrome is taken back off. */
.ap-surface[data-surface="read"] .ap-imgtap {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
  /* A UA suppresses selection inside a button, and the alt-text placeholder this one wraps
   * IS prose on the page — selectable, quotable and cardable before it was wrapped, and a
   * regression if it stopped being. `captureImage` is what keeps the two gestures apart:
   * a drag that ends inside this block is a selection, not a tap. */
  -webkit-user-select: text;
  user-select: text;
}
/* Missing or intentionally unresolved web/Markdown images (#57) keep their
 * alt text in-flow without manufacturing a broken image request. */
.ap-surface[data-surface="read"] .ap-block-image-placeholder {
  display: block;
  padding: 14px 16px;
  background: var(--plate);
  border: 1px solid var(--rule);
  color: var(--muted);
  font-family: var(--ui-face);
  font-size: 13.5px;
}
/* Marks — the persistent trace an anchored entry leaves in the prose (ADR 0009).
 * `<mark>`'s own UA fill is dropped on the base class, so each kind adds exactly the
 * one property it owns: a card FILLS, a selection-born note UNDERLINES. Text covered
 * by both wears both classes and shows both, which is why the pair was chosen — fill
 * and underline do not fight, so there is no precedence rule anywhere in this file. */
.ap-surface[data-surface="read"] mark.ap-mark {
  background: none;
  color: inherit;
}
/* One rule, because it is one decision: a card FILLS, at either scale it is painted at
 * (ADR 0009, ADR 0029). The note's half cannot join it — an empty strip has no text to
 * decorate, so it wears the same accent as a hairline edge instead — and the two stay
 * apart below rather than pretending to be one. */
.ap-surface[data-surface="read"] mark.ap-hl,
.ap-surface[data-surface="read"] .ap-blockmark.ap-hl {
  background: var(--accent-wash);
}
/* DOTTED, AND CLEAR OF THE WORDS (#711, story 49): a solid coloured underline on the web
 * means a link, and the one thing a Mark must never be mistaken for is somewhere to go.
 * 2px at 6px of offset is far enough below the baseline to read as an annotation of the
 * line rather than as part of it. */
.ap-surface[data-surface="read"] mark.ap-ul {
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
}
/* THE CARD GLYPH AFTER A WASHED PASSAGE (#711, story 64) — what says a card rather than a
 * note is behind those words, and the only thing a card leaves below 1180px besides the
 * wash. The drawing is 16px and the TARGET is the thumb's: an absolutely positioned
 * overlay rather than padding, so the glyph stays tucked against the words it follows and
 * the line box it sits in does not grow. `position: relative` on the button is what that
 * overlay is measured against, and it is inside a `<p>`, not an ancestor of the margin's
 * groups — the placement pass's own reference frame is untouched. */
.ap-surface[data-surface="read"] .ap-cardmark {
  position: relative;
  /* `inline`, and not the `inline-flex` every other glyph control on this surface
   * takes: this one stands INSIDE a paragraph, and an inline-level box that
   * establishes its own formatting context makes `innerText` break the line at it —
   * which puts a newline in the middle of the author's sentence for anything that
   * reads the rendered text, a reader's own copy-paste included. */
  display: inline;
  margin: 0 0 0 3px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--accent);
  line-height: 1;
  cursor: pointer;
}
.ap-surface[data-surface="read"] .ap-cardmark .ap-icon {
  display: inline-block;
  width: 16px;
  height: 16px;
  vertical-align: -3px;
}
/* The target drawn out around the glyph to a thumb's size. It reaches 14px past the
 * drawing on every side, so in a paragraph carrying two entries it lies over the first
 * characters of the Mark that follows — and THE MARK WINS that overlap, because an inline
 * box later in the paragraph paints over an earlier one's pseudo-element and hit testing
 * follows paint. Words the reader is reading keep their own taps, which is the property
 * `dev/e2e-mark.mjs › aMarkNextToAGlyphKeepsItsOwnTaps` measures on the page: anything
 * here that lifted the glyph out of that order — a `z-index`, an `absolute` on the button
 * itself — would take the first characters of the next Mark with it. */
.ap-surface[data-surface="read"] .ap-cardmark::after {
  content: "";
  position: absolute;
  inset: calc((var(--touch-target) - 16px) / -2);
  /* Behind the paragraph's own inline content, which is what puts the words on top of it
   * in paint order and so in hit order. Without it the target is a positioned box later
   * than the text it covers, and it takes the first characters of the Mark that follows. */
  z-index: -1;
}
/* A mark with entries behind it is also the CONTROL that reads them (ADR 0010). It
 * stays a `<mark>` carrying `role="button"` — a real `<button>` is an atomic inline box
 * and would lift a marked run out of the reading measure onto its own lines — so it
 * gets a pointer and a focus ring here rather than inheriting a button's. Nothing else
 * about its appearance changes: the fill and the underline are ADR 0009's decision. */
.ap-surface[data-surface="read"] mark.ap-mark[role="button"] {
  cursor: pointer;
}
.ap-surface[data-surface="read"] mark.ap-mark[role="button"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* A mark at BLOCK scale (ADR 0029) — what an entry anchored to an image leaves, since
 * there is no text in one to find a run inside. Full width at the block's bottom edge and
 * in flow BELOW the picture, so it covers no pixel of it, the way an underline sits below
 * text rather than over it. The paint is ADR 0009's vocabulary in the one form an empty
 * element can wear it: the card's fill is the same wash — one rule with the run's, above —
 * and the note's underline is a hairline in the same accent along the same edge, because
 * `text-decoration` has no text to decorate here. A block carrying both wears both, and
 * neither is ranked. Focus is the global `:focus-visible` ring, as every real button on
 * this surface takes it.
 *
 * The height is a tap target rather than a hairline: a strip is a stripe of the reading
 * measure, so the finger is served in width and this is what it needs in depth. */
.ap-surface[data-surface="read"] .ap-blockmark {
  display: block;
  width: 100%;
  height: 14px;
  margin: 0;
  padding: 0;
  border: 0;
  /* The one chromeless control on this surface that still declares a corner, because the
   * card's fill below lands on a real button and a UA rounds a filled one (§Design tokens
   * only — square corners). Its bare sibling `.ap-imgtap` paints nothing and needs none. */
  border-radius: 0;
  background: none;
}
.ap-surface[data-surface="read"] .ap-blockmark.ap-ul {
  border-bottom: 1px solid var(--accent);
}
/* Tappable only where there is an entry behind it — the open draft's preview paints and
 * opens nothing, exactly as a previewed run does. */
.ap-surface[data-surface="read"] button.ap-blockmark {
  cursor: pointer;
}

/* —— Predict stations, their compare, and the Comprehension check (#34, #711) ——
 *
 * NO BOX AND NO PLATE FILL (#711, story 53). All three were a plate behind a hairline
 * flag — the dress this issue retires, because a panel on the page is exactly what makes
 * a reading app read as an admin console. What separates them from the prose now is
 * space, and their own voice: an accent dot and a name, a question a size above the
 * column, the reader's italic, and the sans behind a sparkle when a model speaks.
 *
 * The station opens its section, owning no space above it and its full clearance below.
 * Space BELOW is safe here where space above would not be: a bottom margin grows ROW 1,
 * so the block in row 2 and the margin group level with it both start lower TOGETHER and
 * stay in line. The next row's own lead is part of the clearance, hence the subtraction —
 * the gap is one number, not one number and a remainder. */
.ap-surface[data-surface="read"] .ap-predict,
.ap-surface[data-surface="read"] .ap-compare {
  display: flex;
  flex-direction: column;
  gap: 14px;
  animation: ap-rise 0.3s ease both;
  font-size: var(--ap-station-size); /* the base every offset inside it is taken from (#381) */
}
.ap-surface[data-surface="read"] .ap-predict {
  margin: 0 0 calc(var(--ap-station-gap) - var(--ap-lead));
}
.ap-surface[data-surface="read"] .ap-compare {
  /* Clearance above as well, and for the reason it always had one: without it a revealed
   * compare sits flush against the NEXT section's station and the two fuse. The section
   * row's own padding is part of that gap, so it is subtracted here as the station's is. */
  margin: var(--ap-station-gap) 0 calc(var(--ap-station-gap) - var(--ap-lead));
}

/* THE NAME OF A BLOCK IN THIS COLUMN — the accent dot and its name at a station, the
 * sparkle and its name at a check, the name alone on the Prompt-craft screen. Sentence
 * case in the chrome's sans, because it names a category and a category is the app
 * speaking; the difference between the marks is the whole of what each says. The dot is
 * drawn rather than lettered — this instrument's shapes are its own, and a bullet
 * character is a different one in every font.
 *
 * ONE SELECTOR LIST AND NOT THREE COPIES: three blocks naming themselves in three places
 * that drift is what the tracked capitals looked like before they were retired. */
.ap-surface[data-surface="read"] .ap-predict-name,
.ap-surface[data-surface="read"] .ap-comprehension-name,
.ap-surface[data-surface="read"] .ap-craft-eye {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-family: var(--ui-face);
  font-size: 15.5px;
  font-weight: 600;
  color: var(--accent);
}
.ap-surface[data-surface="read"] .ap-predict-name::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}
.ap-surface[data-surface="read"] .ap-comprehension-name .ap-icon {
  width: 18px;
  height: 18px;
}
/* A skipped station is the name alone, and the name steps back to the quiet register:
 * nothing is being asked, so nothing is accented. */
.ap-surface[data-surface="read"] .ap-predict.skipped .ap-predict-name {
  color: var(--muted);
}
.ap-surface[data-surface="read"] .ap-predict.skipped .ap-predict-name::before {
  background: var(--faint);
}

/* THE QUESTION IS THE STATION'S HEADLINE, so it steps UP off the machine size rather than
 * the block stepping down: it is the one thing on the screen the reader has to answer. */
.ap-surface[data-surface="read"] .ap-predict-q {
  margin: 0;
  color: var(--prose-ink);
  font-size: calc(var(--ap-station-size) + 2.5px);
  line-height: calc(var(--ap-station-leading) - 0.2);
  text-wrap: pretty;
}
/* The field is `apparatus.css`'s `.ap-field` since #711 — an underline and nothing else.
 * What is left here is the type: the shared field is fixed at reading size on every other
 * surface, and in the reading column it has to follow the Size and Spacing dials like the
 * prose around it. */
.ap-surface[data-surface="read"] .ap-predict-input {
  font-size: var(--ap-station-size);
  line-height: var(--ap-station-leading);
}
.ap-surface[data-surface="read"] .ap-predict-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}
/* —— THE THREE SPEAKERS (#711, stories 67-69) ————————————————————————————————
 * One row shape for the reader, the model and the text: the glyph in a gutter of its own
 * so a line that wraps keeps its second line under the first, and the voice beside it.
 * The same row carries the reader's committed prediction at the worked station, because
 * it is the same sentence said in the same voice in two places.
 *
 * Each row's NAME is its `aria-label` and nothing is printed: the three glyphs are what a
 * reader reads, and the labels that used to head each row were the app narrating what the
 * voices already say. A screen reader is given the three names in words instead. */
.ap-surface[data-surface="read"] .ap-cmp-row {
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  align-items: start;
  font-size: var(--ap-station-size);
  line-height: var(--ap-station-leading);
  color: var(--prose-ink);
}
/* `start` and not the glyph's own `center` (`apparatus.css › .ap-icon`): a speaker's sign
 * belongs beside the first line it speaks, and these rows are several lines tall — the
 * Response's is taller still, because it holds the room its words will need. */
.ap-surface[data-surface="read"] .ap-cmp-row .ap-icon {
  align-self: start;
  width: 20px;
  height: 20px;
  margin-top: 5px;
}
/* The two signs that are not a model's are quiet marks beside prose. The model's takes
 * the accent, and takes it from `.ap-machine`, which is the rule that owns what a model
 * speaking looks like — so this one steps out of its way rather than repeating it. */
.ap-surface[data-surface="read"] .ap-cmp-row:not(.ap-machine) .ap-icon {
  color: var(--faint);
}
.ap-surface[data-surface="read"] .ap-cmp-row > span {
  text-wrap: pretty;
}
/* The reader's own words: their voice, one step quieter than the text's, because in the
 * compare it is what they are checking and not what they came to read. */
.ap-surface[data-surface="read"] .ap-predict-you {
  color: var(--muted);
}
/* A MODEL SPEAKING IS THE SHARED MACHINE LINE (`apparatus.css › .ap-machine`), worn and
 * not copied: the sans, the accent sparkle and the pulse that says the model is reading
 * all arrive with it, so the Card check's sign and the Response's are one sign and cannot
 * drift into two. This file restates two things and nothing else — the gutter, which is
 * the compare's 30px and not the machine line's 24px because the Response stands level
 * with two rows that already use it, and the type, which has to follow the reading dials
 * here where a one-line report does not. Stated on the ROW rather than on the words
 * inside it, so the reserve below can be said in the row's own lines.
 *
 * The ink is the third and is the prototype's (`recur-contents.html › .cmp-row.model p`):
 * a Response is prose the reader reads, where a machine line is a remark at its `--muted`
 * beside a field. */
.ap-surface[data-surface="read"] .ap-cmp-model {
  grid-template-columns: 30px minmax(0, 1fr);
  font-size: calc(var(--ap-station-size) - 2.5px);
  line-height: var(--ap-station-leading);
  color: var(--ink);
}
/* THE ROW HOLDS ITS HEIGHT FROM THE MOMENT THE CALL LEAVES, so the Notice under it does
 * not jump when the words arrive or when the call fails (`dev/e2e-response.mjs`). */
.ap-surface[data-surface="read"] .ap-predict-response,
.ap-surface[data-surface="read"] .ap-predict-response-failed {
  min-height: var(--ap-response-reserve);
}
.ap-surface[data-surface="read"] .ap-predict-response.pending {
  color: var(--faint);
}
.ap-surface[data-surface="read"] .ap-cmp-failed {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  align-content: flex-start;
  gap: 10px 16px;
  color: var(--muted);
}
/* The reveal is the compare's one control and the only thing in the block before it is
 * pressed, so it is an alternative rather than a main action: outlined accent, at the
 * start of its own line rather than stretched across it. */
.ap-surface[data-surface="read"] .ap-predict-reveal {
  align-self: flex-start;
}

/* —— The Comprehension check ——————————————————————————————————————————————————
 * Dressed by the station's rules: no box, a name in the sans behind the sparkle, the
 * questions in the reading Face, and one hairline between questions — the separation this
 * design draws, where the block used to draw a frame around all five. */
.ap-surface[data-surface="read"] .ap-comprehension {
  min-width: 0;
  font-size: var(--ap-station-size); /* the station's base, for the station's reason (#381) */
  margin: var(--ap-station-gap) 0 calc(var(--ap-station-gap) - var(--ap-lead));
  display: flex;
  flex-direction: column;
  gap: 14px;
  animation: ap-rise 0.3s ease both;
}
.ap-surface[data-surface="read"] .ap-comprehension.unavailable {
  gap: 8px;
}
.ap-surface[data-surface="read"] .ap-comprehension-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}
/* The score is a figure, not a verdict: the chrome's sans with tabular numerals, so five
 * answers in a row do not make the line shuffle. */
.ap-surface[data-surface="read"] .ap-comprehension-head b {
  font-family: var(--ui-face);
  font-size: 13.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.ap-surface[data-surface="read"] .ap-comprehension-note,
.ap-surface[data-surface="read"] .ap-comprehension-complete {
  margin: 0;
  color: var(--faint);
  font-family: var(--ui-face);
  font-size: 13.5px;
  line-height: 1.5;
}
.ap-surface[data-surface="read"] .ap-comprehension-questions {
  display: flex;
  flex-direction: column;
  gap: 22px;
}
.ap-surface[data-surface="read"] .ap-comprehension-question {
  border: 0;
  border-top: 1px solid var(--rule);
  padding: 18px 0 0;
  margin: 0;
  min-width: 0;
}
/* A MODEL WROTE EVERY WORD OF THIS BLOCK, so none of it is set in the author's face —
 * the second of the three rules, and the reason the sparkle above stands where it does.
 * The check used to take the reading Face on the argument that its answers are
 * machine-authored ANSWER text rather than the reader's, which decided the wrong
 * question: what a voice marks is who is speaking, and it is the model in all five
 * questions, their twenty answers and every explanation. The one machine-authored line
 * in this column that keeps the reading Face is the Compare block's Notice, which speaks
 * for the TEXT and says so with the open book; a station's question keeps it because the
 * spec names it (story 53).
 *
 * The SIZE and the SPACING dials still reach it, and must: this is prose the reader reads
 * in the reading column, which is #381's whole argument and is about neither face nor
 * speaker. Only the Font dial stops here. */
.ap-surface[data-surface="read"] .ap-comprehension-question legend {
  padding: 0;
  font-family: var(--ui-face);
  color: var(--prose-ink);
  line-height: var(--ap-station-leading);
  text-wrap: pretty;
}
/* ONE COLUMN AT EVERY WIDTH. The two-column grid was a phone's worst case — two stacks of
 * answer prose sharing 353px — and nothing was gained by it on a wide screen either: four
 * answers to one question are a list, and a list reads down. */
.ap-surface[data-surface="read"] .ap-comprehension-options {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 14px;
}
/* An option is an alternative among equals, so all four wear the outlined neutral button
 * and the chosen one takes the accent — two equal choices take the same kind, and here
 * there are four. What is this file's is the shape of the label: an answer is prose of
 * more than one line, so the button aligns to its leading edge and lets its text wrap,
 * where the shared rule centres one line, and it takes the reading column's own size
 * rather than the button's fixed 15.5 — an answer is read, not scanned. The FACE is the
 * button's own, which is the model's, for the reason stated at the question above. */
.ap-surface[data-surface="read"] .ap-comprehension-option {
  justify-content: flex-start;
  gap: 12px;
  padding: 11px 14px;
  text-align: left;
  font-size: calc(var(--ap-station-size) - 3px);
  font-weight: 400;
  line-height: calc(var(--ap-station-leading) - 0.15);
}
.ap-surface[data-surface="read"] .ap-comprehension-option[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-wash);
  color: var(--accent-ink);
}
/* THE INDEX COLUMN, which is also where the CHOICE is drawn: the A/B/C/D that names an
 * answer becomes the `check` glyph on the one the reader picked, so the chosen option is
 * told by a mark and not by its ground alone — a reader who cannot separate the accent
 * from the paper can still see which of the four they answered. One column either way,
 * so nothing shifts when the mark arrives, and the answers' first words stay in line. */
.ap-surface[data-surface="read"] .ap-comprehension-option > span {
  flex: none;
  align-self: flex-start;
  display: inline-flex;
  justify-content: center;
  width: 1.1em;
  font-family: var(--ui-face);
  font-size: 13px;
  font-weight: 600;
  color: var(--faint);
}
.ap-surface[data-surface="read"] .ap-comprehension-option[aria-pressed="true"] > span {
  color: var(--accent);
}
.ap-surface[data-surface="read"] .ap-comprehension-option > span > .ap-icon {
  width: 1.15em;
  height: 1.15em;
}
/* WHETHER THE ANSWER WAS RIGHT IS DRAWN, in the row the three speakers already use
 * (`.ap-cmp-row` above, worn rather than rebuilt): a glyph in its own gutter, tinted by
 * the result, where a flag down the block's left edge used to say it in a colour alone.
 * What is this element's is its size, a step under the question like the answers, and
 * the model's face — the words after the glyph are the model's explanation. */
.ap-surface[data-surface="read"] .ap-comprehension-feedback {
  margin: 14px 0 0;
  font-family: var(--ui-face);
  font-size: calc(var(--ap-station-size) - 3px);
  line-height: var(--ap-station-leading);
}
.ap-surface[data-surface="read"] .ap-comprehension-feedback.correct .ap-icon {
  color: var(--accent);
}
.ap-surface[data-surface="read"] .ap-comprehension-feedback.incorrect .ap-icon {
  color: var(--muted);
}
.ap-surface[data-surface="read"] .ap-comprehension.unavailable p {
  margin: 0;
  color: var(--muted);
}

/* —— The commentary margin ————————————————————————————————————————————————
 * `.ap-margin` is ONE GROUP — the items sharing an anchor — not the section's cell:
 * since ADR 0011 a section's margin is as many groups as it has anchors, each placed
 * level with what it glosses (see the wide tier at the foot of this file).
 *
 * THE MARGIN IS THE READER'S OWN COLUMN (#711, stories 50 and 51). From 1180px it hangs
 * in the right gutter as a critical edition's does: a Card showing its prompt and when it
 * next comes Due, a Note in the handwriting it was written in with its date. Below that
 * the groups are in the prose column's flow, directly under the Block they gloss — the
 * `▸ Margin · N` disclosure that used to gather them at the section's foot is gone, and
 * with it the one place the reader's own writing was hidden behind a control. What does
 * not survive the fold is the CARD: its Mark and its glyph say it is there, and the
 * prompt is a tap away in the dock, where a phone can give it the room it needs. */
.ap-surface[data-surface="read"] .ap-margin {
  display: grid;
  gap: var(--ap-lead);
}

/* The app voice's flag and gutter — rule-strong where the reader's own entries take the
 * accent. A block's captured links are the one thing left wearing it: the source's own
 * provenance moved into the About sheet with #711, where it is rows rather than a margin
 * item behind a disclosure. */
.ap-surface[data-surface="read"] .ap-block-links {
  padding-left: 11px;
  border-left: 2px solid var(--rule-strong);
  font-family: var(--ui-face);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}

/* THE READER'S OWN ENTRY, AND THE CONTROL THAT OPENS IT (ADR 0046). One rule for both
 * kinds, because a card and a note differ in their voice and their glyph and in nothing
 * about the box: a 24px gutter for the glyph, the body beside it, the quiet line under
 * it. A real `<button>`, so the keyboard contract and the focus ring are the platform's;
 * it has no frame of its own, which is the one place this design lets a control go
 * without one — the entry IS the shape, and a rectangle around every marginal note would
 * put the boxes back that #711 took out. */
.ap-surface[data-surface="read"] .ap-stamp,
.ap-surface[data-surface="read"] .ap-mnote {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  align-items: start;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  animation: ap-rise 0.4s ease both;
}
.ap-surface[data-surface="read"] .ap-stamp > .ap-icon,
.ap-surface[data-surface="read"] .ap-mnote > .ap-icon {
  width: 16px;
  height: 16px;
  margin-top: 3px;
  color: var(--accent);
}
.ap-surface[data-surface="read"] .ap-entrybody {
  display: block;
  font-size: 14.5px;
  line-height: 1.45;
  color: var(--ink);
  letter-spacing: 0;
}
/* The note's own face is `apparatus.css › .ap-voice-hand`, which the element carries;
 * only its scale in the margin is this surface's. The handwriting sets small, so it is
 * given the size the prototype gives it. */
.ap-surface[data-surface="read"] .ap-mnote .ap-entrybody {
  font-size: 17px;
}
/* When it next comes Due, or when a note was written — the quiet line under the body,
 * in the chrome's own sans because it is the app speaking about the reader's entry
 * rather than the entry itself. */
.ap-surface[data-surface="read"] .ap-entrywhen {
  grid-column: 2;
  display: block;
  margin-top: 3px;
  font-family: var(--ui-face);
  font-size: 12px;
  color: var(--faint);
}
.ap-surface[data-surface="read"] .ap-stamp:hover .ap-entrybody,
.ap-surface[data-surface="read"] .ap-mnote:hover .ap-entrybody {
  color: var(--accent-ink);
}

/* —— What an opened entry shows in the dock (ADR 0010, ADR 0046) ————————————————
 * The entry as the reader reads it: whose it is and when, what they wrote, where it is
 * anchored, and the two things that can be done to it. Never a card's answer — the
 * refusal is kept by the template having none, and the editor's half stands behind its
 * own disclosure. Bodies are UNCLIPPED here: the bound belongs to the margin, where a
 * group shares a grid row with the block it glosses (ADR 0013), and this is the mount
 * that IS the rest of it. */
.ap-surface[data-surface="read"] .ap-dock-entries {
  display: flex;
  flex-direction: column;
  gap: 22px;
}
/* THE EYEBROW EVERY ARM OF THE DOCK WEARS — whose entry this is and when it was written,
 * in the app's own quiet sans because it is the app naming the reader's work rather than
 * the work itself. One rule for the three consumers, so the voice cannot drift between an
 * entry, its editor and a collapsed draft; each says only the room it takes. */
.ap-surface[data-surface="read"] .ap-dock-entry > b,
.ap-surface[data-surface="read"] .ap-entryedit > b,
.ap-surface[data-surface="read"] .ap-dock-peek > b {
  display: block;
  font-family: var(--ui-face);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
}
.ap-surface[data-surface="read"] .ap-dock-entry > b {
  margin-bottom: 6px;
}
.ap-surface[data-surface="read"] .ap-dock-entry .ap-entrybody {
  font-size: 19px;
  line-height: 1.45;
}
.ap-surface[data-surface="read"] .ap-dock-entry .ap-entrybody.ap-voice-hand {
  font-size: 21px;
}
.ap-surface[data-surface="read"] .ap-dock-entry .ap-entrywhen {
  margin-top: 6px;
}

/* Edit and Delete, and Delete at the FAR END (#711, story 63) — the distance is the
 * point: the two are not a pair, and a thumb reaching for one must not be able to land
 * on the other. Both are outlined; there is no danger colour in this palette on purpose,
 * so the ARMED state says its finality in words (`Delete — final`) with the ink stepped
 * up. Shared with the editor's row, which keeps the same arrangement. */
.ap-surface[data-surface="read"] .ap-entryacts,
.ap-surface[data-surface="read"] .ap-entryedit .row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
}
.ap-surface[data-surface="read"] .ap-entryacts .del,
.ap-surface[data-surface="read"] .ap-entryedit .del {
  margin-left: auto;
}
.ap-surface[data-surface="read"] .ap-entryacts .del[data-armed],
.ap-surface[data-surface="read"] .ap-entryedit .del[data-armed] {
  border-color: var(--ink);
  font-weight: 700;
}
/* The answer disclosure stands on its own line under the prompt field, with room above
 * it: it is a step the reader takes, not one of the editor's two verbs. */
.ap-surface[data-surface="read"] .ap-revealanswer {
  display: inline-flex;
  margin-top: 16px;
}
.ap-surface[data-surface="read"] .ap-entryedit > b {
  margin-bottom: 10px;
}
.ap-surface[data-surface="read"] .ap-entryedit .ap-field-label {
  margin-top: 16px;
}

/* —— The shared card/note form — the dock's expanded height ————————————————————
 *
 * It renders inside the bottom dock and nowhere else (ADR 0009) — the margin no longer
 * houses it — so its whole box lives here, including the height cap: measured room
 * rather than a viewport fraction, so a keyboard eating half the screen still leaves a
 * usable form in what is left. It scrolls inside the dock, and the prose behind never
 * moves.
 *
 * NO FLAG, NO FRAME, NO BOX (#711). The fields ARE the form: `apparatus.css`'s shared
 * underlined field with its name above it, which persists once the field is filled. What
 * stays here is the rhythm between them and the row of verbs at the foot, shared with
 * the entry editor (`.ap-entryedit`) so the two cannot drift. —————————————————————— */
.ap-surface[data-surface="read"] .ap-form {
  display: flex;
  flex-direction: column;
  margin: 0;
  max-height: calc(var(--dock-room, 100vh) * 0.66);
  overflow-y: auto;
}
/* The captured passage, quoted back at reading size and CLAMPED TO TWO LINES (#711,
 * story 57): the whole Excerpt is what the entry keeps, and the dock is a bar at the
 * bottom edge rather than a second reading surface. The anchor line under it is the
 * record of the range, so the clamp costs the reader nothing. */
.ap-surface[data-surface="read"] .ap-excerpt {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin: 0;
  font-family: var(--reading-face);
  font-size: 16px;
  line-height: 1.5;
  color: var(--muted);
}
/* The `Anchored · §n · ¶n` line — one class, used by the form, the peek and an opened
 * entry. The one job the monospace keeps on this surface beside the `§` marker, and it
 * is drawn at the marker's own 11px UNTRACKED: the design system fixes the instrument
 * mark at one size and one tracking, so an Anchor with 0.02em of its own was a second
 * version of a mark that only means anything because there is one of it. */
.ap-surface[data-surface="read"] .ap-anchor {
  display: block;
  margin-top: 8px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--faint);
}
.ap-surface[data-surface="read"] .ap-form .ap-field-label,
.ap-surface[data-surface="read"] .ap-form .ap-field + .ap-field-label {
  margin-top: 18px;
}
/* The reading measure's own size for what the reader is writing — the two textareas are
 * the one place on this surface a reader types prose, and they are set like prose. 16px
 * is the floor at every tier, below which iOS zooms the page on focus; the shared field
 * is already above it. */
.ap-surface[data-surface="read"] .ap-form textarea,
.ap-surface[data-surface="read"] .ap-entryedit textarea {
  font-size: 18px;
}
.ap-surface[data-surface="read"] .ap-form.note textarea,
.ap-surface[data-surface="read"] .ap-entryedit.note textarea {
  font-size: 20px;
  line-height: 1.45;
}
/* THE VERBS STAY REACHABLE WHILE THE FIELDS SCROLL. The form scrolls inside the dock —
 * it has to, because a soft keyboard can leave it a third of the screen — and a row at
 * the end of a scrolling box is a row a reader has to go looking for with the keyboard
 * up. Sticky to the form's own bottom edge, on the dock's own ground so the fields pass
 * under it rather than through it. */
.ap-surface[data-surface="read"] .ap-form .row {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: 20px;
  padding-top: 12px;
  background: var(--paper);
}
/* The Prompt field's own name, and beside it the reference screen that is about writing
 * one (#711). The control wears the shared named-glyph `.ap-tool`, which is a minor
 * action rather than a verb — it was in the row below, where an outlined button read as a
 * third verb and wrapped under the two at 393px. */
.ap-surface[data-surface="read"] .ap-form-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.ap-surface[data-surface="read"] .ap-form-label .ap-field-label {
  margin-bottom: 0;
}
/* A named glyph stacks its label under it and is narrower than that label; against the
 * column's trailing edge the words would overhang it, so the control keeps its whole
 * width inside the form and gives the label room rather than the gutter. */
.ap-surface[data-surface="read"] .ap-form .craft {
  flex: none;
  padding: 0;
}
/* The Card check's one line (#700), under the Prompt it is about (#711, story 59). It is
 * `apparatus.css › .ap-machine` in full — the sparkle, the app's own face, the pulse
 * while it reads — and what is here is only the room it takes and the dismiss control at
 * its end. */
.ap-surface[data-surface="read"] .ap-card-check {
  /* The machine line's two columns plus the dismiss at its end, so the question and the
   * way to wave it away share one row. */
  grid-template-columns: 24px minmax(0, 1fr) auto;
  align-items: center;
  margin: 12px 0 0;
}
.ap-surface[data-surface="read"] .ap-card-check-dismiss {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  margin: -2px 0 0 8px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--faint);
  cursor: pointer;
}
.ap-surface[data-surface="read"] .ap-card-check-dismiss .ap-icon {
  width: 14px;
  height: 14px;
}
.ap-surface[data-surface="read"] .ap-card-check-dismiss:hover {
  color: var(--ink);
}

/* —— The bottom dock — ONE instrument along the screen's bottom edge (spec 04,
 * ADR 0009, ADR 0046): the peek, the form, a Mark's entries, an entry's editor, and the
 * About sheet. Identical in every tier, which is what makes authoring parity structural
 * rather than maintained (spec 06 §Responsive tiers).
 *
 * THE ONE EDGE A FLOATING SURFACE NEEDS (#711): a top hairline, and nothing else. No
 * shadow — this design has none, and `dev/e2e-reading-style.mjs` enforces that over the
 * whole reading surface.
 *
 * `bottom` is the soft keyboard's business: on iPad the keyboard overlays the
 * viewport, so a dock at `bottom: 0` is typed into from under it. `--dock-inset`
 * (how much the keyboard covers) and `--dock-room` (what is left) are measured from
 * `visualViewport` and published by read.ts; the fallbacks here are what render
 * before the first measurement. ———————————————————————————————————————————————— */
/* THE COLUMN KEEPS ROOM FOR THE DOCK AT EVERY WIDTH, and whether or not one is up. The
 * dock is fixed to the bottom edge, so the foot of the text — the two quiet actions and
 * the dossier line — stands under it otherwise; the phone tier has reserved this since
 * ADR 0009 and the dock is the same object at every width.
 *
 * RESERVED ALWAYS, and that is the load-bearing half. Reserving it only while the dock
 * is up (`:has()`) reads better and is wrong: the room goes away when the dock closes,
 * the document shrinks, and a reader who was near the end of a text is scrolled by the
 * browser's own clamp — a save moving the reader, which is the one thing this whole
 * instrument exists not to do. */
.ap-surface[data-surface="read"] {
  padding-bottom: 248px;
}
.ap-surface[data-surface="read"] .ap-seldock {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--dock-inset, 0px);
  z-index: 60;
  padding: 0 var(--page-inset) calc(18px + env(safe-area-inset-bottom));
  background: var(--paper);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  align-items: center;
  max-height: var(--dock-room, 100vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  animation: ap-rise 0.22s ease-out both;
}
/* The BAR spans the screen, as a surface floating at an edge must, and what it holds
 * stands on the content column — the same edges the masthead and the prose take. */
.ap-surface[data-surface="read"] .ap-seldock > * {
  width: 100%;
  max-width: calc(var(--measure-page) - 2 * var(--page-inset));
}
/* The instrument's own strip: the grip where there is a draft to drag, and the one close
 * control every arm wears, at the trailing end. The close is pulled into the gutter by
 * its own optical margin, so its GLYPH lines up with the column's edge rather than its
 * 44px target doing. */
.ap-surface[data-surface="read"] .ap-dock-bar {
  display: flex;
  align-items: center;
  margin-right: -10px;
}
.ap-surface[data-surface="read"] .ap-dock-close {
  display: grid;
  place-items: center;
  width: var(--touch-target);
  height: var(--touch-target);
  margin-left: auto;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.ap-surface[data-surface="read"] .ap-dock-close:hover {
  color: var(--ink);
}

/* —— The About sheet (#711, stories 54 and 55) ————————————————————————————————
 * The source's own upkeep, in the dock's surface: where it came from, when it arrived,
 * and one row a generation run. Rows are the shared `.ap-listrow`, so this file states
 * only the sheet's inset, its head, and what a run's row does when it grows a Cost gate
 * beneath it. The last row drops its hairline: a list's final rule with nothing under it
 * is a box's bottom edge by another name. */
.ap-surface[data-surface="read"] .ap-about-head {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 4px;
}
.ap-surface[data-surface="read"] .ap-about-head b {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--ui-face);
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
}
.ap-surface[data-surface="read"] .ap-about-head .ap-icon {
  width: 16px;
  height: 16px;
  color: var(--accent);
}
/* A run's row and the flow it opens are one thing, so the hairline belongs to the pair
 * and the row inside it draws none of its own. */
.ap-surface[data-surface="read"] .ap-about-run {
  border-bottom: 1px solid var(--rule);
}
.ap-surface[data-surface="read"] .ap-about-run .ap-listrow {
  border-bottom: 0;
}
.ap-surface[data-surface="read"] .ap-about-run .ap-gen {
  margin: 0 0 14px;
}
.ap-surface[data-surface="read"] .ap-about .ap-listrows > :last-child {
  border-bottom: 0;
}

.ap-surface[data-surface="read"] .ap-save-failed {
  margin: 12px var(--page-inset);
  padding: 10px 12px;
  background: var(--accent-wash);
  border-left: 2px solid var(--accent);
}
.ap-surface[data-surface="read"] .ap-save-failed > b,
.ap-surface[data-surface="read"] .ap-save-failed > p {
  display: block;
  margin: 0 0 6px;
}
/* WHAT THE READER WROTE IS IN A FIELD, so it is read and copied out of the one shape the
 * app uses for text (#711): the shared `.ap-field` under its persistent `.ap-field-label`,
 * where it was a boxed textarea under a tracked-monospace caption. Read-only, and short
 * enough to scroll rather than to grow — this is a rescue, not the form. */
.ap-surface[data-surface="read"] .ap-save-failed .ap-field-label {
  margin-top: 10px;
}
.ap-surface[data-surface="read"] .ap-save-failed .ap-field {
  margin-top: 4px;
}
.ap-surface[data-surface="read"] .ap-save-failed textarea {
  max-height: 72px;
  overflow: auto;
  font-size: 16px;
  line-height: 1.4;
}
.ap-surface[data-surface="read"] .ap-save-failed button {
  margin-top: 14px;
}

/* THE PEEK (#711, story 57): the Excerpt over the two verbs. They are side by side, the
 * same width and a thumb tall, with the Card filled and the Note outlined — the one main
 * action of a context is findable without reading, and a passage more often becomes a
 * Card. `1fr 1fr` rather than a wrapping flex row: the two verbs wrapped onto two rows at
 * 393px before #711, which is the complaint this peek was re-drawn for. */
.ap-surface[data-surface="read"] .ap-dock-peek {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 4px;
}
.ap-surface[data-surface="read"] .ap-dock-verbs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-top: 4px;
}
.ap-surface[data-surface="read"] .ap-seldock .first {
  text-align: center;
}
/* A collapsed draft's peek still says what it holds, in one line: the eyebrow, the
 * reader's opening words, the anchor it keeps. It is the proof the draft is alive, so it
 * elides rather than wrapping — the whole of it comes back on expand. */

.ap-surface[data-surface="read"] .ap-seldock .gist {
  font-family: var(--reading-face);
  font-size: 16px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.ap-surface[data-surface="read"] .ap-dock-peek.note .gist {
  font-size: 18px;
}
/* The drag handle between the two heights: a hit strip carrying one hairline chevron,
 * tall enough to be grabbed on a tablet without a stylus. It takes the bar's width
 * beside the close control. */
.ap-surface[data-surface="read"] .ap-dock-grip {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  gap: 10px;
  /* The only control that changes the dock's height, so the strip — not the chevron
   * inside it — is the tap target, and it is deliberately thumb-sized. */
  min-height: var(--touch-target);
  margin: 0;
  padding: 8px 0;
  background: none;
  border: none;
  border-radius: 0;
  cursor: grab;
  touch-action: none; /* the gesture is ours; the page must not pan under the finger */
}
.ap-surface[data-surface="read"] .ap-dock-grip .bar {
  display: block;
  width: 10px;
  height: 10px;
  border-top: 2px solid var(--faint);
  border-left: 2px solid var(--faint);
  transform: rotate(45deg);
}
.ap-surface[data-surface="read"] .ap-dock-grip[aria-expanded="false"] .bar {
  transform: rotate(225deg);
}
.ap-surface[data-surface="read"] .ap-dock-label {
  font-family: var(--ui-face);
  font-size: 12.5px;
  color: var(--muted);
}
.ap-surface[data-surface="read"] .ap-dock-grip:hover .bar {
  border-color: var(--accent);
}

/* —— The Prompt-craft reference screen — a static overlay, opened from the
 * card form's tertiary control (spec 04 §Prompt craft). —————————————————————— */
.ap-surface[data-surface="read"] .ap-craft {
  position: fixed;
  inset: 0;
  z-index: 70;
  overflow: auto;
  background: color-mix(in srgb, var(--paper) 97%, transparent);
  padding: 34px 20px 60px;
}
/* A CENTRED DIALOG, so it takes the one edge a floating surface gets: a 1px `--faint`
 * border, on the page's own ground rather than a plate fill (#711, §Frames). */
.ap-surface[data-surface="read"] .ap-craft-panel {
  /* The reading measure, named rather than repeated: this was a second literal 638px,
   * which #351 makes untenable in both directions — the designed value has one home
   * now, and a reader who has narrowed their column should not meet one panel that
   * ignored them. Identical to what shipped at the designed measure. */
  max-width: var(--measure-reading);
  margin: 0 auto;
  border: 1px solid var(--faint);
  background: var(--paper);
  padding: 24px 28px 26px;
}
.ap-surface[data-surface="read"] .ap-craft-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}
/* The dialog closes by a control with a shape, like every other control in the app. */
.ap-surface[data-surface="read"] .ap-craft-close {
  padding: 0 14px;
  min-height: 44px;
  font-size: 14px;
}
.ap-surface[data-surface="read"] .ap-craft-intro {
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--muted);
  font-style: italic;
  margin-bottom: 18px;
}
.ap-surface[data-surface="read"] .ap-craft-h {
  font-family: var(--ui-face);
  font-size: 13px;
  font-weight: 600;
  color: var(--faint);
  margin: 24px 0 2px;
}
.ap-surface[data-surface="read"] .ap-craft-row {
  display: grid;
  grid-template-columns: 130px 1fr;
  gap: 12px;
  padding: 9px 0;
  border-top: 1px solid var(--rule);
  font-size: 14.5px;
  line-height: 1.55;
}
.ap-surface[data-surface="read"] .ap-craft-row .t {
  font-family: var(--ui-face);
  font-size: 14px;
  font-weight: 600;
  color: var(--accent);
  padding-top: 1px;
}
.ap-surface[data-surface="read"] .ap-craft-src {
  font-family: var(--ui-face);
  font-size: 13px;
  line-height: 1.9;
  color: var(--faint);
  margin-top: 16px;
}

/* —— THE READING BAR (#711, story 46) ————————————————————————————————————————
 * What a reader reaches for mid-text, on the content column's own axis, joined to the
 * masthead as one piece of sticky chrome: the position readout at the leading end, and
 * About, Contents and Type as named glyphs at the trailing one.
 *
 * STICKY, NOT FIXED, and the first thing in the surface, which is why the surface drops
 * its own top padding above: a sticky bar whose natural place is the masthead's own bottom
 * edge is stuck from the first pixel of scroll, so the transform below always has the
 * whole bar to move and there is no width of scroll where the two halves of the chrome
 * disagree. It also keeps the bar in flow, so no rule here reserves its height.
 *
 * It replaced two FIXED CORNERS (#351, #478) — the readout at the top left, the Aa control
 * at the top right — which floated over the prose above 640px and stood down into the flow
 * below it, where they scrolled away with the header and left a reader mid-book with
 * neither. One bar at every width is also what lets the receding rule cover the phone
 * tier: there is one thing to take away and bring back, and it is the thing the controls
 * are in.
 *
 * `--position-top` is the masthead's MEASURED height, published to the document element by
 * `surfaces/read.ts › measurePosition` (§A measured layout fact reaches the stylesheet as a
 * CSS custom property). It rides the root rather than this rule because that is the channel
 * every measured viewport fact on this surface uses — the same one `--dock-inset` and
 * `--dock-room` take — and it is read here through `--ap-chrome-top`, which carries the
 * pre-measurement literal for this rule and for the two popovers seated under it. */
.ap-surface[data-surface="read"] .ap-reading-bar {
  position: sticky;
  top: var(--ap-chrome-top);
  z-index: 30; /* the reading bar — see the ladder in apparatus.css */
  padding: 2px 0 10px;
  background: var(--paper);
  transition: transform var(--ap-chrome-slide);
}
/* IT STANDS ON THE APPARATUS GRID, taking the same tracks a row of the text takes
 * (`--ap-row-columns`), so the readout starts where a paragraph starts and the tools end
 * where one ends. It does not WEAR `.ap-row`: a row of that set is a row of the text, and
 * several drivers reach a section by its index among them. */
.ap-surface[data-surface="read"] .ap-reading-bar {
  display: grid;
  grid-template-columns: var(--ap-row-columns);
  column-gap: 30px;
  /* The same outdent a row takes, so the bar's paper reaches across both gutters and the
   * `§` markers and margin entries scrolling under it pass behind rather than through. */
  margin-inline: var(--ap-row-outdent-start) var(--ap-row-outdent-end);
}
.ap-surface[data-surface="read"] .ap-reading-bar > .ap-reading-bar-in {
  grid-column: var(--ap-prose-column);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 44px;
  /* The hairline is the column's, as the foot's is: the bar's own paper reaches across
   * the rail and the margin so the prose passes behind it, and the rule under it is drawn
   * where the prose it separates actually starts. */
  border-bottom: 1px solid var(--rule);
}
/* THE BAR LEAVES WITH THE MASTHEAD (#653). When `apparatus.css ›
 * .ap-masthead[data-receded]` takes the masthead off screen, the bar rises by its own seat
 * and its own height, on the masthead's own `--ap-chrome-slide`, so the two move as one
 * piece rather than leaving a bar floating where the masthead was. The sibling combinator
 * reaches it because `surfaces.ts › shell` renders the masthead immediately before the
 * surface. The masthead's keyboard-focus hold (`apparatus.css`) holds the bar too, both
 * ways: focus in the masthead keeps the bar, and focus in the bar keeps the whole chrome.
 * The global reduced-motion rule stills the slide. */
.ap-masthead[data-receded]:not(:has(:focus-visible))
  ~ .ap-surface[data-surface="read"]:not(:has(.ap-reading-bar :focus-visible))
  .ap-reading-bar {
  transform: translateY(calc(-100% - var(--ap-chrome-top)));
}

.ap-surface[data-surface="read"] .ap-reading-tools {
  display: flex;
  align-items: stretch;
  gap: 10px;
}

/* THE POSITION READOUT (wayfinding, #72, ADR 0036) — the place the reader is in and
 * `§n / N`, saying exactly what it has always said and re-dressed in the chrome's own sans
 * (#711). It is DISPLAY-ONLY again: it carried the Contents while there was no bar to put
 * a named control in, and one element with two jobs was the price of that seat.
 *
 * Its text is written by the scroll-frame painter, never rendered, and so is
 * `data-readout` — the painter's word for whether the readout has anything to say, which
 * the rule below turns into the readout's silence while the header row is still saying it.
 * The readout goes quiet; the bar does not move, because the three tools stand in it
 * whatever the readout has to say. */
.ap-surface[data-surface="read"] .ap-position {
  min-width: 0;
  margin: 0;
  font-family: var(--ui-face);
  font-size: 13px;
  line-height: 1.4;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ap-surface[data-surface="read"] .ap-position[data-readout="silent"] {
  visibility: hidden;
}
.ap-surface[data-surface="read"] .ap-position b {
  color: var(--muted);
  font-weight: 400;
}
/* The separator belongs to the name, so a bare `§n / N` (a headingless chunk of
 * a chapterless source) shows no leading dot. */
.ap-surface[data-surface="read"] .ap-position b:empty {
  display: none;
}
.ap-surface[data-surface="read"] .ap-position b::after {
  content: " · ";
  color: var(--faint);
}

/* —— The seat both chrome popovers take ——————————————————————————————————————
 * One rule, because it is one decision: a popover hung off a control that is itself
 * pinned in the chrome needs no measured placement at all, so the seat is inset values
 * and the surface's one geometry pass is never asked a
 * question about it. `position: fixed` because the top layer's containing block is the
 * viewport; `inset: auto` and `margin: 0` undo the UA's centring.
 * The top and the bottom clearance are measured DOWN FROM THE WHOLE CHROME
 * (`--ap-chrome-bottom`, the local alias at the head of this file for the measured
 * `--chrome-height`) and not from the screen's edge, or a popover in the top layer
 * would paint over the bar its own control stands in —
 * and they live here rather than in each rule because the chip's height is one fact.
 * Each popover states only what is its own below: which edge it hangs from, how wide
 * it is, and its padding. */
.ap-surface[data-surface="read"] .ap-contents,
.ap-surface[data-surface="read"] .ap-tune-pop {
  position: fixed;
  inset: auto;
  margin: 0;
  top: calc(var(--ap-chrome-bottom) + 6px);
  max-height: calc(100vh - var(--ap-chrome-bottom) - 32px);
  overflow-y: auto;
  /* A flick at the end of the list must not scroll the book underneath — on a phone
   * that is the place the reader is trying to keep. */
  overscroll-behavior: contain;
  background: var(--plate);
  border: 1px solid var(--faint);
}

/* —— The Contents (#478, ADR 0036) ————————————————————————————————————————————
 * The list of the source's sections. Its seat is the shared rule above; what is its
 * own is the LEFT edge — the corner its control sits in — and a width that leaves a
 * long chapter title room to be read. It is an `<ol>`, so it also drops the UA's
 * marker and padding. */
.ap-surface[data-surface="read"] .ap-contents {
  left: var(--page-inset);
  width: min(360px, calc(100vw - 2 * var(--page-inset)));
  list-style: none;
  padding: 8px;
}
/* A ROW, not a bare button: #479's Pin is set and cleared here and nowhere else
 * (ADR 0036), and its control stands BESIDE the jump rather than inside it — a
 * control nested in a control is not a thing that can be operated. */
/* THE ROW IS AS TALL AS THE TALLER OF ITS TWO CONTROLS, which since #711 is the Pin: a
 * named glyph stacks its drawing over its name, and a one-line row cropped both. It is a
 * list of things a reader presses, so the height it needs is the one a press already
 * asks for. */
.ap-surface[data-surface="read"] .ap-contents-row {
  display: flex;
  align-items: stretch;
  gap: 4px;
  min-height: var(--touch-target);
}
/* The row's TWO controls share one bare-button reset, the way the popover seat above is
 * one rule for two popovers: they sit in one row and read as one control pair, so a
 * border or a radius appearing on one of them would be visible and nobody's fault. What
 * differs is below — the jump takes the room, the Pin takes a fixed column. */
.ap-surface[data-surface="read"] .ap-contents-b,
.ap-surface[data-surface="read"] .ap-contents-pin {
  background: none;
  border: 0;
  border-radius: 0;
  cursor: pointer;
}
.ap-surface[data-surface="read"] .ap-contents-b {
  display: flex;
  flex: 1;
  min-width: 0;
  align-items: baseline;
  gap: 10px;
  text-align: left;
  padding: 7px 6px;
}
.ap-surface[data-surface="read"] .ap-contents-b:hover {
  background: var(--paper);
}
.ap-surface[data-surface="read"] .ap-contents-b[aria-current="location"] {
  background: var(--accent-wash);
  border-left: 2px solid var(--accent);
}
/* The §n, in the instrument voice the rail and the readout already speak. */
.ap-surface[data-surface="read"] .ap-contents-n {
  flex: none;
  font-family: var(--mono);
  /* The rail's own 11px untracked, because it IS the rail's mark: the design system fixes
   * the `§` marker at one size and one tracking, and a Contents drawing it at 12px with
   * 0.1em of letter-spacing was drawing a second version of the same mark. */
  font-size: 11px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}
/* TWO REGISTERS, and the difference is the whole of rule 3 (ADR 0036): a real title is
 * ink, the first words of a headingless section are muted, so a first line is never
 * read as a heading the source does not have. */
.ap-surface[data-surface="read"] .ap-contents-name,
.ap-surface[data-surface="read"] .ap-contents-opening {
  min-width: 0;
  font-size: 13px;
  line-height: 1.5;
}
.ap-surface[data-surface="read"] .ap-contents-name {
  color: var(--ink);
}
.ap-surface[data-surface="read"] .ap-contents-opening {
  color: var(--muted);
}
/* THE PIN (#479, ADR 0036) IS A NAMED GLYPH (`.ap-tool`), like every other tool in this
 * design. It was a bare word — "Pin", "Pinned" — in tracked mono, which is two of the
 * things #711 retires in one control: a control a reader cannot tell from a caption, and
 * the instrument voice on something that is not an instrument mark. The glyph is
 * `bookmark-simple`, the same sign the Library uses for where a reader stopped, because
 * a Pin is the reader putting that mark down by hand.
 *
 * HELD AND NOT HELD DIFFER BY THEIR NAME AS WELL AS BY COLOUR — "Pin" against "Pinned",
 * which a named glyph shows without being asked — so the state survives a reader who
 * cannot tell the accent from the rule, which is what the old border-and-fill square was
 * for. A second bookmark at the regular weight would say it a third way and would mean
 * adding a glyph this issue's registry does not carry. `aria-pressed` stays the one place
 * the state is written (CONVENTIONS § A control's selected state is an ARIA attribute).
 *
 * The hairline it stands behind is its whole boundary, the way a segment's is: the pair
 * is one control in one row, and a second border inside that row would read as a second
 * row. */
.ap-surface[data-surface="read"] .ap-contents-pin {
  flex: none;
  min-width: 58px;
  border-left: 1px solid var(--faint);
}
.ap-surface[data-surface="read"] .ap-contents-pin[aria-pressed="true"] {
  color: var(--accent);
}

/* —— The Type control's popover (#351) ————————————————————————————————————————
 * Where the reader sets this device's reading style. The control itself is one of the
 * reading bar's three named glyphs and states nothing of its own beyond the bar's rule;
 * what is left here is the panel behind it. It was a bare `Aa` chip pinned to the
 * viewport's top-right corner, which is the seat #711 replaced with the bar.
 *
 * The popover hangs under the control, at the control's own edge. Its ground and its
 * edge are the shared popover seat's above — a second copy of them here said the same
 * thing twice and was the kind of pair that drifts. */
.ap-surface[data-surface="read"] .ap-tune-pop {
  right: var(--page-inset);
  width: min(320px, calc(100vw - 2 * var(--page-inset)));
  padding: 16px;
}

/* —— How a chrome popover ARRIVES ——————————————————————————————————————————
 * The two top-layer popovers the reading bar hangs — the Type panel and the Contents — fade in
 * over 4px of rise. `@starting-style` is what lets a transition run from
 * `display: none`: it names the values the element has on the frame it enters the
 * top layer, and the `:popover-open` rule below is where it settles. `display` and
 * `overlay` are in the transition list with `allow-discrete` so the element is
 * kept painted for the fade OUT as well — which is why this is a transition and not
 * the shared `ap-rise` keyframes: a keyframe runs once on entry and cannot follow the
 * element back out of the top layer. The distance is `ap-rise`'s own 6px, inverted
 * because these hang from above; the reduced-motion rule in apparatus.css stills all
 * of it. One rule for the two, because it is one decision: an instrument's panel
 * opens the same way wherever it is hung. */
.ap-surface[data-surface="read"] .ap-tune-pop,
.ap-surface[data-surface="read"] .ap-contents {
  transition:
    opacity 0.16s ease-out,
    transform 0.16s ease-out,
    overlay 0.16s allow-discrete,
    display 0.16s allow-discrete;
}
.ap-surface[data-surface="read"] .ap-tune-pop:popover-open,
.ap-surface[data-surface="read"] .ap-contents:popover-open {
  opacity: 1;
  transform: none;
}
@starting-style {
  .ap-surface[data-surface="read"] .ap-tune-pop:popover-open,
  .ap-surface[data-surface="read"] .ap-contents:popover-open {
    opacity: 0;
    transform: translateY(-6px);
  }
}
.ap-surface[data-surface="read"] .ap-tune-pop:not(:popover-open),
.ap-surface[data-surface="read"] .ap-contents:not(:popover-open) {
  opacity: 0;
  transform: translateY(-6px);
}


.ap-surface[data-surface="read"] .ap-tune-pop .ap-field-label {
  font-size: 12px;
  line-height: 1.45;
}

/* A row is its mono caption and the controls that answer it. One rule for all five,
 * because they are one control repeated — a scheme, a face, and three dials all read
 * as "here is the thing, here are your choices". */
.ap-surface[data-surface="read"] .ap-tune-row {
  margin-bottom: 14px;
}
/* The caption is `.ap-field-label` and nothing else — it wants no difference from the
 * shared recipe, so it states none. */
/* Every row of controls WEARS the shared segmented control (`apparatus.css › .ap-seg`),
 * the steppers included, so `− · Default · +` reads as one instrument with a readout in
 * its middle rather than as two loose buttons around a label. The row's one difference
 * from the shared recipe is that it fills the panel's width. */
.ap-surface[data-surface="read"] .ap-tune-opts {
  display: flex;
}

/* A ONE-OF-N ROW IS A GRID OF THREE (#371) — the Theme row's three sit in three equal
 * columns. The Font row is the exception, below. */
.ap-surface[data-surface="read"] .ap-tune-choices {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}
/* THE FONT ROW IS A LIST, one face per line, each option SET IN THE FACE IT NAMES.
 * Seven faces (nine on WebKit, where Charter and New York are offered) do not fill a
 * grid of two or three without an orphan, and "Atkinson Hyperlegible" does not fit a
 * third of 288px without wrapping. A list has neither problem, and it gives the
 * reader the one thing a name cannot: what the face looks like. That preview is the
 * Aa control's own argument ("set in the face it sets", above) applied to each
 * option, which is why these buttons alone leave the mono instrument voice — a
 * serif's name in tracked mono caps says nothing about the serif. The face per
 * option is keyed off `data-option`, the value the popover writes, and the tokens
 * named here are the same ones `appearance.ts › FACE_TOKEN` maps. */
.ap-surface[data-surface="read"] .ap-tune-row[aria-label="Font"] .ap-tune-choices {
  grid-template-columns: 1fr;
}
.ap-surface[data-surface="read"] .ap-tune-row[aria-label="Font"] .ap-tune-b {
  position: relative;
  text-align: left;
  letter-spacing: 0;
  font-size: 15px;
  line-height: 1.3;
  padding: 7px 36px 7px 12px;
  border-right: 0;
  border-bottom: 1px solid var(--faint);
}
.ap-surface[data-surface="read"] .ap-tune-row[aria-label="Font"] .ap-tune-b:last-child {
  border-bottom: 0;
}
/* The tick beside the chosen face is DRAWN — two borders of a rotated box — for the
 * reason the Contents' pin gives: a glyph would set in whichever face its row is in,
 * and a border is the same shape in every one of them. */
.ap-surface[data-surface="read"] .ap-tune-row[aria-label="Font"] .ap-tune-b[aria-pressed="true"]::after {
  content: "";
  position: absolute;
  right: 15px;
  top: 50%;
  width: 5px;
  height: 10px;
  margin-top: -7px;
  border-right: 1.5px solid var(--accent);
  border-bottom: 1.5px solid var(--accent);
  transform: rotate(45deg);
}
.ap-surface[data-surface="read"] .ap-tune-b[data-option="serif"] { font-family: var(--serif); }
.ap-surface[data-surface="read"] .ap-tune-b[data-option="sans"] { font-family: var(--sans); }
.ap-surface[data-surface="read"] .ap-tune-b[data-option="os"] { font-family: var(--os-face); }
.ap-surface[data-surface="read"] .ap-tune-b[data-option="literata"] { font-family: var(--literata); }
.ap-surface[data-surface="read"] .ap-tune-b[data-option="atkinson"] { font-family: var(--atkinson); }
.ap-surface[data-surface="read"] .ap-tune-b[data-option="opendyslexic"] { font-family: var(--opendyslexic); }
.ap-surface[data-surface="read"] .ap-tune-b[data-option="lexend"] { font-family: var(--lexend); }
.ap-surface[data-surface="read"] .ap-tune-b[data-option="charter"] { font-family: var(--charter); }
.ap-surface[data-surface="read"] .ap-tune-b[data-option="newyork"] { font-family: var(--newyork); }

/* Every button in the popover is a member of the shared segmented control
 * (`apparatus.css › .ap-seg-b`), which owns its frame, its hover and its selected
 * state; what is stated here is the member's rhythm in a 320px panel — its share of
 * the row and its inset. (The Font row's members take the face they name instead, above.)
 * The tracked capitals the Theme row and the steppers used to speak in went with #711:
 * a segment names a setting, and a name is sentence case here like every other. */
.ap-surface[data-surface="read"] .ap-tune-b {
  flex: 1;
  min-width: 0;
  padding: 7px 4px;
}

/* Where a dial stands, between its two steppers. Tabular figures and a fixed share of
 * the row so the readout cannot resize the buttons either side of it as the reader
 * steps past ±10 — a stepper that moves under the finger holding it is the one defect
 * a stepped control can have. */
.ap-surface[data-surface="read"] .ap-tune-at {
  flex: 1.4;
  align-self: center;
  padding: 7px 0;

  text-align: center;
  /* A figure, not an instrument mark: the chrome's sans with tabular numerals, the same
   * reading the Cost gate's estimate takes (apparatus.css). */
  font-family: var(--ui-face);
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The foot: the way back, and the one thing the popover says in prose. */
.ap-surface[data-surface="read"] .ap-tune-foot {
  border-top: 1px solid var(--rule);
  padding-top: 10px;
}
.ap-surface[data-surface="read"] .ap-tune-reset {
  padding: 4px 0;
}
.ap-surface[data-surface="read"] .ap-tune-note {
  margin: 4px 0 0;
  font-family: var(--ui-face);
  font-size: 12px;
  line-height: 1.5;
  color: var(--faint);
}

/* —— Wide tier ≥1180px: the rail and the margin both hang in the viewport's gutters,
 * PLACED BY ANCHOR. MUST follow the base `.ap-row` rules (same specificity, later source
 * order wins). ————————————————————————————————————————————————————————————————————— */
@media (min-width: 1180px) {
  .ap-surface[data-surface="read"] {
    /* Both tracks hang, so the row is the column plus 106px of rail and 226px of margin.
     * `1fr` and not `--measure-reading`: the prose track IS the column's own content box,
     * which the width dial already moves. */
    --ap-row-columns: 76px minmax(0, 1fr) 196px;
    --ap-prose-column: 2;
    --ap-row-outdent-start: -106px;
    --ap-row-outdent-end: -226px;
  }
  /* WHICH TIER THIS IS, published for the one pass that has to know (ADR 0046). The
   * collision pass moves a margin group only where the margin is a hanging column; below
   * this width the groups are in the prose column's flow and a push would tear the
   * reader's text. The breakpoint keeps ONE home by being asked of the stylesheet rather
   * than restated as a `matchMedia` literal in `surfaces/read.ts`. */
  .ap-surface[data-surface="read"] .ap-reading {
    --margin-column: 1;
  }
  /* The whole of ADR 0011's placement, in one declaration: each element stands in the
   * grid row `surfaces/read.ts › rowStyle` stamped it with — one row per block — so a
   * margin group is level with the block it glosses. Explicit indices and not `subgrid`,
   * which would have bought the same alignment for a far smaller diff; the ADR owns that
   * rejection and its reasons, and this file should not re-argue them. Below this tier
   * the stamps are simply not read, and everything auto-places into the single prose
   * column. */
  .ap-surface[data-surface="read"] .ap-row > * {
    grid-row: var(--ap-row, auto);
  }
  /* THE MARGIN NO LONGER SIZES THE PROSE (#711, ADR 0046). A group used to be an ordinary
   * grid item, so the row it shared with its anchor grew to hold it and the prose column
   * was torn open to make room for what the reader wrote beside it — the tear ADR 0013
   * bounded rather than removed, and the reason two entries could never meet.
   *
   * Absolutely positioned INSIDE ITS GRID AREA, which is what keeps ADR 0011's placement
   * exactly as it was: an out-of-flow grid item with a definite placement is laid out
   * against the area it was placed in, so the group still stands level with its anchor
   * and the renderer still decides nothing it did not decide before. `left`/`right` fill
   * that area, where an auto inset would shrink the group to fit its own words.
   *
   * `.ap-row` becomes the containing block, and that is deliberate rather than incidental:
   * nothing else on this surface is positioned against the document any more. */
  .ap-surface[data-surface="read"] .ap-row {
    position: relative;
  }
  .ap-surface[data-surface="read"] .ap-row > .ap-margin {
    position: absolute;
    grid-column: 3;
    left: 0;
    right: 0;
    /* Level with its block's first line: both own the same lead, which is all "standing
     * level with an anchor" amounts to — and is why the group takes its anchor's lead
     * rather than the column's, a heading owning a larger one. */
    margin-top: var(--ap-lead);
    /* ADR 0046's push, applied — what keeps two groups apart now that the row does not.
     * A TRANSFORM and not a `top`, so the pass can read each group's NATURAL top back by
     * subtracting the push in force. `surfaces/read.ts › stackMarginGroups` measures and
     * publishes it on the measured-layout channel `--position-top` established (#72). */
    transform: translateY(var(--margin-push, 0px));
  }
  .ap-surface[data-surface="read"] .ap-row > .ap-margin[data-lead="heading"] {
    margin-top: var(--ap-head-lead);
  }

  /* ADR 0013's BOUND, the other half of standing level with an anchor: an item's body may
   * not be taller than the block it glosses, so one item can no longer stretch the row it
   * shares with it — the price ADR 0011 accepted in one clause, and a reader set at 3,501
   * characters (#127). `--margin-budget` is the anchor's RENDERED height floored at four
   * measured per group by `surfaces/read.ts › boundMarginItems` and arriving on the
   * measured-layout channel `--position-top` established. THE FOUR-LINE FLOOR IS HERE, not
   * there: `lh` is the unit for "one line" and resolves against each body's own type, so
   * the floor needs no number in JS and no assumption that the two entry templates share a
   * type rule. A group with no anchor to measure — a section's foot — gets no budget at
   * all, and the floor is simply what the fallback answers.
   *
   * Before the pass runs there is no budget and the floor alone holds, which is also the
   * whole shape below 1180px, where these rules do not apply: the narrow tier stands each
   * group under its own block, tears nothing, and is untouched here (ADR 0013).
   *
   * The fade is on the CLIPPED state alone. The mask fades the last 26px of the element's
   * own box, so on a body that fits it would gray out its final line for nothing. Which
   * bodies are clipped is the layout's own answer, stamped back as `data-clipped` — and
   * the rest of a clipped body is read by tapping the entry, which opens it unclipped in
   * the dock (ADR 0046), so there is one fact here and no length threshold anywhere.
   * `currentColor` rather than a colour: a mask reads alpha, so the gradient needs an
   * opaque end and not a palette entry. */
  .ap-surface[data-surface="read"] .ap-margin .ap-entrybody {
    /* Two declarations, and the second is the rule. The first is what an engine without
     * `lh` or `max()` is left holding: the anchor's height alone, bound but unfloored,
     * which is #127 fixed without the four-line courtesy rather than #127 returning. */
    max-height: var(--margin-budget, none);
    max-height: max(var(--margin-budget, 0px), 4lh);
    overflow: hidden;
  }
  .ap-surface[data-surface="read"] .ap-margin .ap-entrybody[data-clipped] {
    -webkit-mask-image: linear-gradient(to bottom, currentColor calc(100% - 26px), transparent 100%);
    mask-image: linear-gradient(to bottom, currentColor calc(100% - 26px), transparent 100%);
  }
}

/* —— Narrow tier <1180px: the margin stands in the prose column's own flow, each group
 * directly under the Block it glosses (#711, stories 50 and 51). The `▸ Margin · N`
 * disclosure that used to gather a whole section's groups at its foot is gone: a reader's
 * own writing is the last thing to hide behind a control, and on a phone it is the thing
 * they scrolled past that they want to see.
 *
 * WHAT DOES NOT COME WITH IT IS THE CARD. A prompt and its next review under every
 * washed passage would be the reader reading their own deck instead of the text; the
 * wash and the glyph say a card is there, and a tap opens it in the dock, which is where
 * a phone has the room. ————————————————————————————————————————————————————————— */
@media (max-width: 1179px) {
  .ap-surface[data-surface="read"] .ap-margin {
    margin-top: 14px;
    margin-bottom: 4px;
  }
  .ap-surface[data-surface="read"] .ap-margin .ap-stamp {
    display: none;
  }
}

/* —— Supported phone ≤640px: single column; the rail flattens into an instrument
 * eyebrow while retaining explicit separators between its metadata fields. ———————— */
@media (max-width: 640px) {
  /* One column, so every explicit placement has to stand down: column 2 would mint a
   * phantom track, and a spanning rail would push the whole section below itself. The
   * blocks' own lead is what separates them here. */
  .ap-surface[data-surface="read"] .ap-craft-row {
    grid-template-columns: 1fr;
    gap: 3px;
  }
  /* THE BAR ITSELF NEEDS NO PHONE RULE (#711). Both controls used to stand down into the
   * flow here — a fixed chip over a full-width column covers the prose scrolling under it
   * — and standing down is what took them away from a reader mid-book. They are in the
   * sticky bar at every width now, and the receding rule is what keeps them off the prose:
   * reading forward takes the whole chrome away, and a scroll back or a tap brings it.
   *
   * THE READOUT DOES NOT COME TO THE PHONE. It has never been shown at this width, the
   * prototype's bar carries none at any width, and whether `§7 / 32` is a progress readout
   * is an open refusal the re-dress does not settle (#711 § Reading view) — so the bar
   * arrives here without the app gaining a ratio it did not have. The three named glyphs
   * take the row, which is also what keeps a 393px bar from overflowing. The element stays
   * in the DOM: the scroll painter writes to it either way, and what it says is the tier's
   * business rather than the painter's (ADR 0036). */
  .ap-surface[data-surface="read"] .ap-position {
    display: none;
  }
  /* Both popovers span the page's own insets here — one rule, as their seat is. */
  .ap-surface[data-surface="read"] .ap-contents,
  .ap-surface[data-surface="read"] .ap-tune-pop {
    left: var(--page-inset);
    right: var(--page-inset);
    width: auto;
  }
  .ap-surface[data-surface="read"] .ap-contents-b {
    align-items: center;
  }
  .ap-surface[data-surface="read"] .ap-contents-pin {
    min-width: var(--touch-target);
  }
}
/* Per-surface stylesheet: Review (#/review) — the defend path (#26), built out
 * to the decided UX (#31: the deal and the deck-clear ledger) and re-dressed by
 * #711. The shared shell, the button and the row live in apparatus.css; only the
 * Review rhythm lives here — the column is `.ap-surface`'s own, so this file
 * states no width.
 *
 * The shape of the surface is one sentence: the prompt IS the page, and every
 * press of the session is in a bar at the bottom edge so a thumb never hunts for
 * one. Nothing here is framed — the card the reader reads has no box at all, and
 * the bar takes the one edge a surface floating over the page gets, a top
 * hairline. */

/* THE DECK — done, this one, still to come, at most nine
 * (`schedule/deal.ts › deckDots`). The design's one round shape, the accent dot,
 * counted out: a reader sees their place, and never a backlog number. */
.ap-surface[data-surface="review"] .rv-deck {
  display: flex;
  gap: 7px;
  margin: 4px 0 44px;
}
.ap-surface[data-surface="review"] .rv-deck i {
  width: 8px;
  height: 8px;
  border: 1.5px solid var(--faint);
  border-radius: 50%;
}
.ap-surface[data-surface="review"] .rv-deck i[data-dot="done"] {
  background: var(--faint);
}
.ap-surface[data-surface="review"] .rv-deck i[data-dot="current"] {
  border-color: var(--accent);
  background: var(--accent);
}

/* THE CARD. The Source names a text, but here it is the chrome's line above the
 * thing the reader came for, so it speaks in the app's voice and stays out of
 * the way; the prompt is the author's-register reading Face at a size that makes
 * it the page. */
.ap-surface[data-surface="review"] .rv-src {
  margin: 0 0 12px;
  font-family: var(--ui-face);
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--faint);
}
/* A tap anywhere on it reveals (`surfaces/review.ts`), so a pointer says so —
 * and stops saying so once there is nothing left to reveal. */
.ap-surface[data-surface="review"] .rv-card {
  cursor: pointer;
}
.ap-surface[data-surface="review"] .rv-card[data-revealed] {
  cursor: auto;
}
.ap-surface[data-surface="review"] .rv-q {
  margin: 0;
  font-size: clamp(23px, 3.4vw, 28px);
  line-height: 1.3;
  letter-spacing: -0.006em;
  text-wrap: pretty;
}
/* Once the answer is out it is what the reader came to check, so it takes the
 * ink and the prompt steps back to muted (#711 story 76). */
.ap-surface[data-surface="review"] .rv-card[data-revealed] .rv-q {
  color: var(--muted);
}
.ap-surface[data-surface="review"] .rv-answer {
  margin: 30px 0 0;
  font-size: clamp(21px, 3vw, 24px);
  line-height: 1.42;
  color: var(--ink);
  text-wrap: pretty;
}

/* THE BAR — every press of the session, in one rectangle at the viewport's
 * bottom edge. The Grades arrive exactly where the Reveal button stood, which is
 * the whole point: the thumb does not move between the two halves of a card.
 *
 * Fixed, and on the reading dock's rung of the stacking ladder documented in
 * `apparatus.css › .ap-masthead` — the same role at the same edge, on a surface
 * where the dock never appears. The inner row stands on the content column, so
 * the bar's ends reach the window while its buttons line up with the card. */
.ap-surface[data-surface="review"] .rv-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 60;
  background: var(--paper);
  border-top: 1px solid var(--rule);
  padding-bottom: env(safe-area-inset-bottom);
}
.ap-surface[data-surface="review"] .rv-bar-in {
  display: grid;
  gap: 12px;
  max-width: var(--measure-page);
  margin: 0 auto;
  padding: 14px var(--page-inset);
}
/* TWO EQUAL COLUMNS, and they have to be made equal rather than asked to be. A
 * grid child's `min-width` is `auto`, so a plain `1fr 1fr` lets the longer Grade
 * claim the width its label needs and hands the shorter one what is left — 153px
 * against 115px at 320px, measured. Nothing is clipped and nothing pans, so no
 * existing rule catches it, but the two choices stop being equal, which is the
 * one thing this pair must never be (#711 story 5). `minmax(0, 1fr)` makes the
 * halves halves; the Grade's own padding and its glyph gap give way instead, so
 * the label still stands on one line on the narrowest phone in common use. */
.ap-surface[data-surface="review"] .rv-bar-two {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ap-surface[data-surface="review"] .rv-bar-two > [data-grade] {
  padding-inline: 6px;
}
.ap-surface[data-surface="review"] .rv-bar-two > [data-grade] > :first-child {
  min-width: 0;
  gap: 6px;
}
/* Its rhythm only; the dress is the shared `.ap-hint`. */
.ap-surface[data-surface="review"] .rv-hint {
  margin: 0 0 -2px;
  text-align: center;
}
/* The bar is fixed, so the page has to end above it rather than under it — and
 * above the inset the bar itself pads for, or the two drift the moment a phone
 * has one. A measured height would be exact and would cost an inline style
 * binding, which `test/csp.test.ts` refuses; this number is the bar at its
 * tallest plus room. */
.ap-surface[data-surface="review"]:has(.rv-bar) {
  padding-bottom: calc(150px + env(safe-area-inset-bottom));
}

/* Zero-due / opening standing note — the reading-measure line, no ledger. */
.ap-surface[data-surface="review"] .ap-review-title {
  margin-bottom: 14px;
}
.ap-surface[data-surface="review"] .ap-review-standing {
  color: var(--muted);
  font-size: 18.5px;
}
/* The one quiet way onward from an empty deck. A link wearing `.ap-btn-quiet`,
 * so it is a plain choice beside the standing note and never a call to action —
 * and the shared button gives it its box in every tier, so all this owes it is
 * the space above it. */
.ap-surface[data-surface="review"] .ap-review-standing-link {
  margin-top: 8px;
}

/* Entry animation, matched to the reading stamp; stilled under reduced-motion
 * by the global rule in apparatus.css. */
.ap-surface[data-surface="review"] .ap-fade {
  animation: ap-rise 0.4s ease both;
}

/* DECK CLEAR: the one ceremonial state, fired when grading empties the pile —
 * the only home of the two retention numbers (#31). It leads with the check
 * glyph and the date the schedule next holds until: "nothing more is due" is a
 * state, and a state is drawn rather than written. */
.ap-surface[data-surface="review"] .rv-done {
  display: grid;
  justify-items: start;
  gap: 14px;
}
.ap-surface[data-surface="review"] .rv-done-glyph {
  display: inline-flex;
  color: var(--good);
}
.ap-surface[data-surface="review"] .rv-done-glyph .ap-icon {
  width: 44px;
  height: 44px;
}
.ap-surface[data-surface="review"] .rv-stamp {
  margin: 0;
  font-family: var(--ui-face);
  font-size: 15.5px;
  font-weight: 600;
  color: var(--good);
}
.ap-surface[data-surface="review"] .rv-next {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-family: var(--ui-face);
  font-size: 15.5px;
  color: var(--muted);
}

/* The two metrics as rows, like every other list in the app. The group's name
 * wears the caption voice; the numbers ride the row's own value slot with
 * tabular figures so they align down the trailing edge. */
.ap-surface[data-surface="review"] .rv-ledger {
  width: 100%;
  margin-top: 16px;
}
.ap-surface[data-surface="review"] .rv-ledger .ap-listrow {
  font-variant-numeric: tabular-nums;
}
/* Demonstrated retention is the second metric — a firmer rule sets it apart. */
.ap-surface[data-surface="review"] .rv-lretention {
  border-top: 1px solid var(--rule-strong);
}
.ap-surface[data-surface="review"] .rv-ledger .u {
  color: var(--faint);
}
/* Its rhythm only; the dress and the measure are the shared `.ap-hint`. */
.ap-surface[data-surface="review"] .rv-note {
  margin: 0;
}
/* The Import dialog (#711). NOT a per-surface stylesheet any more: Import stopped
 * being a surface, so there is no `data-surface` to scope under, and the prefix is the
 * dialog's own class. `.ap-import` is one element in the top layer, reachable from the
 * Library, and every rule here carries it for exactly the reason the surface rule
 * exists — nothing in this file may paint anything else.
 *
 * Only the dialog's rhythm is here. The button, the field, the row and the voices are
 * `apparatus.css`'s, and a restatement of one of them would be a copy that drifts. */

/* A surface that floats over the page gets the one edge a surface needs: a 1px
 * `--faint` border, which is what a control's boundary is made of here, and no shadow.
 * `popover="auto"` brings the top layer, Escape, the tap outside and the focus return
 * with it; the UA's own border, padding and colours are what these first lines undo. */
.ap-import {
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(560px, calc(100vw - 40px));
  max-width: 100%;
  max-height: min(84vh, 760px);
  overflow-y: auto;
  padding: 20px var(--page-inset) 30px;
  border: 1px solid var(--faint);
  border-radius: 0;
  background: var(--paper);
  color: var(--ink);
  overscroll-behavior: contain;
}
/* The scrim is not elevation — it is what makes the tap outside a visible offer. */
.ap-import::backdrop {
  background: color-mix(in srgb, var(--ink) 38%, transparent);
}

.ap-import-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}
.ap-import-title {
  margin: 0;
  font-family: var(--ui-face);
  font-size: 15.5px;
  font-weight: 600;
  color: var(--ink);
}
.ap-import .ap-url {
  margin: 0;
}
/* The field's leading glyph is the instrument's, not a control: `--faint` at rest and
 * accent while the field has focus, which `.ap-field`'s own `color` already carries. */
.ap-import .ap-field > .ap-icon {
  width: 20px;
  height: 20px;
}
/* THE DIALOG'S TWO GLYPH-ONLY BUTTONS — the send control on the field's own line, so
 * it is visible from the first frame (disabled, at 35% opacity, until there is a link
 * to send), and the close control beside the title. Both keep the shared button's 46px
 * height, which is what a form's button is and what the phone tier's thumb floor asks;
 * only the width is theirs, because neither carries a word to be as wide as. */
.ap-import-close,
.ap-url-submit {
  width: 46px;
  padding: 0;
}
/* The cost line WEARS the shared hint (`apparatus.css › .ap-hint`) — it is a line that
 * teaches the method above the control it belongs to, which is what that class is. Only
 * its rhythm is stated here. */
.ap-import-cost {
  margin: 10px 0 0;
}
.ap-import-rows {
  margin-top: 26px;
  border-top: 1px solid var(--rule);
}
.ap-import .ap-md {
  margin: 22px 0 0;
}
.ap-md-submit {
  margin-top: 16px;
}

/* THE TWO PICKERS THAT ARE STILL FIELDS. The EPUB row needs nothing here: it IS the
 * picker, a `.ap-listrow` label around a `.ap-visually-hidden` input, so it wears the
 * shared row and the shared disabled look. The Markdown file and the video's two files
 * stay ordinary fields inside their panes — each sits beside a paste box it is the
 * alternative to, and a row cannot say "or". One selector list rather than two
 * byte-identical blocks. */
.ap-import .ap-md input[type="file"],
.ap-video-paste input[type="file"] {
  display: block;
  width: 100%;
  margin-bottom: 4px;
  font-family: var(--ui-face);
  font-size: 13px;
  color: var(--muted);
}
.ap-import .ap-md input[type="file"]::file-selector-button,
.ap-video-paste input[type="file"]::file-selector-button {
  margin-right: 12px;
  padding: 7px 12px;
  border: 1px solid var(--faint);
  border-radius: 0;
  background: transparent;
  font-family: var(--ui-face);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}

.ap-import-error {
  margin: 10px 0 0;
  font-family: var(--ui-face);
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--accent-ink);
}
/* The link to a fix that lives on another surface (#319). `failUrl` focuses it
 * PROGRAMMATICALLY, and the shared `:focus-visible` ring only paints when the last
 * interaction was the keyboard — so a reader who submitted with the mouse would be
 * given a focused control with nothing to see. This rule is that ring, on plain
 * `:focus`, because here the focus was moved FOR the reader rather than by them.
 * The VALUES are the shared ring's (apparatus.css § `:focus-visible`) and are
 * restated only because the selector cannot be: keep the two in step. */
.ap-import-error a:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.ap-video-paste {
  margin: 22px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
}
.ap-video-paste-title {
  margin: 0;
  font-size: 18px;
  line-height: 1.4;
}
.ap-video-paste-channel,
.ap-video-paste-hint {
  font-family: var(--ui-face);
  color: var(--muted);
  font-size: 13.5px;
  line-height: 1.55;
}
.ap-video-paste-channel {
  margin: 2px 0 16px;
}
.ap-video-paste-hint {
  margin: 0 0 8px;
}
.ap-video-paste .ap-field-label {
  margin-top: 18px;
}
.ap-video-paste-command,
.ap-video-paste code {
  font-family: var(--mono);
  font-size: 13px;
}
.ap-video-paste-hint code {
  white-space: nowrap;
}
/* One click selects the whole command. It wraps so the video's URL at its end is in
 * sight, only between arguments so no flag reads as split, and a soft wrap is not
 * copied, so it still pastes as one line. The URL alone may break anywhere: on a
 * phone it is wider than the column.
 *
 * NO PLATE AND NO BOX (#711), the same decision the Sync code took: a string a reader
 * copies character by character keeps the mono, and one hairline is what sets it apart.
 * A frame around it said it was pressable, which is the one thing it is not. */
.ap-video-paste-command {
  margin: 0 0 8px;
  padding: 2px 0 2px 14px;
  border-left: 1px solid var(--rule);
  line-height: 1.55;
  color: var(--ink);
  white-space: pre-wrap;
  -webkit-user-select: all;
  user-select: all;
}
.ap-video-paste-command span {
  white-space: nowrap;
}
.ap-video-paste-command span:last-child {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.ap-video-paste-automatic {
  display: flex;
  gap: 10px;
  align-items: center;
  margin: 12px 0 4px;
  font-family: var(--ui-face);
  font-size: 14px;
  line-height: 1.5;
}
.ap-video-paste-submit {
  margin-top: 16px;
}

.ap-import-generation {
  margin: 6px 0 22px;
}
.ap-import-saved {
  margin: 0 0 10px;
  color: var(--ink);
  font-size: 16px;
  line-height: 1.5;
}
.ap-import-generation .ap-cost-actions .ap-btn-accent:only-child {
  color: var(--on-accent);
  background-color: var(--accent);
}

/* On a phone the dialog is a sheet from the bottom edge: it is where the thumb is,
 * and the one edge it keeps is the hairline the page passes under. */
@media (max-width: 640px) {
  .ap-import {
    inset: auto 0 0 0;
    margin: 0;
    width: 100%;
    max-height: 88vh;
    border-width: 1px 0 0;
    padding-bottom: calc(30px + env(safe-area-inset-bottom));
  }
}
/* Per-surface stylesheet: Settings (#/settings) — the device-local values the app
 * runs on (#319) plus the Sync block and the Library group. Since #711 the surface is
 * a SETTINGS LIST and not a form: five groups, each a name over `.ap-listrows` of the
 * shared row, with one quiet sentence under it. The row, the field, the button and the
 * hint are apparatus.css's; only the settings rhythm lives here — the gaps between
 * groups, and how a control sits in the trailing edge of a row. */

.ap-surface[data-surface="settings"] .ap-settings-group {
  margin: 0 0 40px;
}

/* The group's name wears the shared `.ap-group-name`; its gap to the first row is the
 * only thing that is this surface's. */
.ap-surface[data-surface="settings"] .ap-settings-heading {
  margin: 0 0 4px;
}

/* Every explanation on this surface wears `.ap-hint`, which carries the voice AND the
 * measure since #711 slice 8, so only the rhythm around each one is here: a group's own
 * sentence, and a hint that belongs to one row rather than to its group. */
.ap-surface[data-surface="settings"] .ap-settings-aside {
  margin: 12px 0 0;
}
/* A row hint is not a row: it has no hairline, no 52px floor and no value, because it
 * is the sentence the row above it could not hold. It sits under that row, indented to
 * the leading edge of nothing, and is always visible — a fact behind a hover is a fact
 * the reader will not have when they need it. */
.ap-surface[data-surface="settings"] .ap-settings-rowhint {
  padding: 8px 0 14px;
}
/* The device-local line leads the groups it is about rather than sitting between two
 * of them, so it takes the gap below and not above. */
.ap-surface[data-surface="settings"] .ap-settings-note {
  margin: 0 0 20px;
}

/* The two lines that say the app has just done something — a value saved on this
 * device, an archive merged in. Both wear `.ap-hint`, so all that is left is the one
 * thing they do not share with a hint: the tone that says it went well. The first was
 * a mono-capital chip on a plate with an accent flag, a plate and a voice #711 retired
 * both of. */
.ap-surface[data-surface="settings"] .ap-settings-saved,
.ap-surface[data-surface="settings"] .archive-imported {
  color: var(--good);
}
/* The save confirmation stands at the trailing edge, where the values it is about are. */
.ap-surface[data-surface="settings"] .ap-settings-saved {
  width: fit-content;
  margin: 0 0 12px auto;
}

/* —— A control at the trailing edge of a row ————————————————————————————————
 * THE ROW IS THE TOUCH TARGET. The name and a select's value stretch to the row's full
 * height, so a thumb anywhere along the row lands on the name (which focuses the
 * control it is `for`) or on the control itself.
 *
 * A FIELD IS THE ONE THAT DOES NOT STRETCH. Its underline is its own, and stretching it
 * put that `--faint` line on top of the row's `--rule` one, where the row's hairline
 * simply changed colour under the value and read as a mistake. It stands centred with
 * its own short line about ten pixels above the row's, which is where the prototype
 * draws it (`design/recur-contents.html › .r input`). The field's own 44px floor at the
 * phone tier is what carries the thumb there. */
.ap-surface[data-surface="settings"] .ap-listrow > label,
.ap-surface[data-surface="settings"] .ap-listrow > .ap-listrow-v {
  align-self: stretch;
  display: flex;
  align-items: center;
  min-width: 0;
}
.ap-surface[data-surface="settings"] .ap-listrow > .ap-field {
  min-width: 0;
}
.ap-surface[data-surface="settings"] .ap-listrow > label {
  flex: none;
  cursor: pointer;
}
.ap-surface[data-surface="settings"] .ap-listrow select,
.ap-surface[data-surface="settings"] .ap-listrow > .ap-field > input {
  align-self: stretch;
}

/* A `<select>` that is still a `<select>`: the platform's own picker, its keyboard and
 * its screen-reader behaviour, drawn as its current value at the row's trailing edge
 * with the `caret-down` glyph beside it. `appearance: none` drops the UA's own arrow,
 * which would be a second visual language beside the glyph; nothing else about the
 * control changes. */
.ap-surface[data-surface="settings"] .ap-listrow select {
  min-width: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: right;
  text-align-last: right;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
}
.ap-surface[data-surface="settings"] .ap-listrow-v > .ap-icon {
  width: 14px;
  height: 14px;
  flex: none;
}

/* A field in a row speaks in the row's own voice rather than at reading size: it holds
 * a token, a number or a key, none of which is prose. The width is the value's — a
 * number is short, and a key or an address is given as much of the row as a phone can
 * spare.
 *
 * ITS UNDERLINE IS ITS OWN, a short line under the value and clear of the row's
 * hairline below it. The 9px the field stands above its line by in a form is 4px here,
 * because a row is half a form's height; `:focus-within` takes the doubled accent
 * line's extra pixel out of that padding, as the shared field does, so nothing under it
 * moves while the reader types. */
.ap-surface[data-surface="settings"] .ap-listrow > .ap-field {
  gap: 0;
  padding-bottom: 4px;
}
.ap-surface[data-surface="settings"] .ap-listrow > .ap-field:focus-within {
  padding-bottom: 3px;
}
.ap-surface[data-surface="settings"] .ap-listrow > .ap-field > input {
  width: min(24ch, 46vw);
  font: inherit;
  text-align: right;
}
.ap-surface[data-surface="settings"] .ap-listrow > .ap-field > input[type="number"] {
  width: 10ch;
}

/* The Sync group's row IS the block's summary (#711 story 81): the masthead control's
 * glyph and tone at the leading edge, the constant word Sync beside it, and where this
 * Device stands at the trailing edge. The marker goes because the row's own press is
 * the offer; the standing clamps to one line, because a failure's sentence is as long
 * as the failure and the panel below is where it is read in full. */
.ap-surface[data-surface="settings"] .ap-settings-sync > summary {
  list-style: none;
  cursor: pointer;
}
.ap-surface[data-surface="settings"] .ap-settings-sync > summary::-webkit-details-marker {
  display: none;
}
.ap-surface[data-surface="settings"] .ap-settings-sync > summary > .ap-listrow-v {
  overflow: hidden;
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The Sync panel (`surfaces/settings-sync.ts`), which opens under that row. Its titles
 * and leads step down to this surface's voices; the actions are the shared buttons. */
.ap-surface[data-surface="settings"] .sync-panel {
  padding-top: 8px;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-title {
  margin: 20px 0 10px;
  font-family: var(--reading-face);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
}

/* The panel's own body voice: what it says to the reader in full sentences, and the
 * sentence beside a confirmation checkbox. One recipe, at the row's size, because a
 * screen that set its prose two sizes is a screen with two registers. */
.ap-surface[data-surface="settings"] .sync-panel .sync-lead,
.ap-surface[data-surface="settings"] .sync-panel .sync-confirm {
  font-family: var(--ui-face);
  font-size: 15.5px;
  line-height: 1.5;
  color: var(--muted);
}
.ap-surface[data-surface="settings"] .sync-panel .sync-lead {
  margin: 0;
}

/* A panel note is the group sentence's voice one seat deeper, so it WEARS `.ap-hint`
 * (`surfaces/settings-sync.ts`) and keeps only its margin here; its measure is the
 * explanation measure at the head of this file. */
.ap-surface[data-surface="settings"] .sync-panel .sync-note {
  margin: 0.75rem 0 0;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 2rem;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-actions-tertiary {
  margin-top: 1rem;
  gap: 1.5rem;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-recover {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: 0.75rem;
  margin-top: 3rem;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-recover > label,
.ap-surface[data-surface="settings"] .sync-panel [data-sync-pending-reset-form] > label {
  display: grid;
  gap: 0.5rem;
}

/* The two code forms. Their labels wore mono capitals, which #711 retired as the
 * chrome's voice; they WEAR the shared caption and the shared field now
 * (`surfaces/settings-sync.ts › codeField`), so the underline, its doubling on focus
 * and the padding compensation are `apparatus.css`'s. What is left here is the label's
 * own gap — the grid's, not the caption's — and the one thing this field does not
 * share: the mono face, because a Sync code is 64 characters a reader reads one at a
 * time and copies back. */
.ap-surface[data-surface="settings"] .sync-panel label > .ap-field-label {
  margin-bottom: 0;
}
.ap-surface[data-surface="settings"] .sync-panel .sync-codefield > input {
  font-family: var(--mono);
  font-size: 1rem;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-reset-local {
  margin-top: 3rem;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-reset-local .ap-btn-quiet {
  margin-top: 0.5rem;
}

/* The code itself: no plate and no box (#711), one hairline under it. Mono and tracked
 * on purpose — this is the one string in the app a reader has to read character by
 * character and type somewhere else. */
.ap-surface[data-surface="settings"] .sync-panel .sync-code {
  overflow-wrap: anywhere;
  margin: 2rem 0 1.5rem;
  padding: 0 0 1rem;
  border-bottom: 1px solid var(--rule);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 1rem;
  letter-spacing: 0.08em;
  line-height: 1.8;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-qr {
  display: block;
  width: min(328px, 100%);
  height: auto;
  margin: 1.5rem 0 2rem;
  background: var(--paper);
  image-rendering: pixelated;
}

.ap-surface[data-surface="settings"] .sync-panel .sync-confirm {
  display: flex;
  gap: 0.65rem;
  align-items: flex-start;
  margin: 1.5rem 0;
}

.ap-surface[data-surface="settings"] .sync-panel > .ap-btn-accent {
  margin-top: 2rem;
}

/* —— The Library group (#711) ——————————————————————————————————————————————
 * The Export archive and the storage readout, which left the Library so that the
 * surface a reader opens to read holds nothing but texts. Its three actions are action
 * rows now — a glyph, then the verb — rather than three quiet buttons in a strip, so
 * upkeep reads as the list it is. */

/* The import control is a <label> wrapping a visually hidden file input, so the ROW is
 * the device's file picker. The dimming is the shared row's (35% since #711); what is
 * left here is the half a `<label>` cannot get from `:disabled` — refusing the press.
 *
 * AND THE RING, because the input the keyboard actually focuses is one clipped pixel:
 * `:focus-visible` paints on a control nobody can see, so a reader tabbing through this
 * group would lose their place at exactly this row. The row wears the ring on the
 * input's behalf. `:focus-within` and not `:has(:focus-visible)`: the only way to focus
 * a clipped input is the keyboard, so the two cannot differ here. */
.ap-surface[data-surface="settings"] .archive-import[data-disabled] {
  pointer-events: none;
}
.ap-surface[data-surface="settings"] .archive-import:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.ap-surface[data-surface="settings"] .archive-imported {
  margin: 16px 0 0;
}
.ap-surface[data-surface="settings"] .archive-error {
  font-family: var(--ui-face);
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--accent-ink);
  margin: 16px 0 0;
}
/* An archive that named blobs it could not carry (#200). The count sits in the same
 * line as the merged totals, in the warning ink, because a thin backup that restores in
 * silence is indistinguishable from a complete one. */
.ap-surface[data-surface="settings"] .archive-omitted {
  color: var(--accent-ink);
}
/* The recovery-phrase field an encrypted archive asks for (#200) WEARS the shared field
 * (`apparatus.css › .ap-field`), which is what carries the underline, its doubling on
 * focus and the padding compensation that keeps the row from growing a pixel. What is
 * left here is the one thing this field does not share: the mono face at 16px, because
 * 24 words are read back against paper and a proportional font makes that harder than
 * it needs to be — and below 16px iOS zooms the page on focus, which is the last thing
 * to do to a reader copying 24 words. */
.ap-surface[data-surface="settings"] .archive-phrase {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  margin: 16px 0 24px;
}
.ap-surface[data-surface="settings"] .archive-phrase .ap-field-label {
  display: flex;
  align-items: center;
  gap: 8px;
}
.ap-surface[data-surface="settings"] .archive-phrase .ap-field-label .ap-icon {
  width: 20px;
  height: 20px;
}
.ap-surface[data-surface="settings"] .archive-phrase .ap-field > textarea {
  font-family: var(--mono);
  font-size: 16px;
  line-height: 1.6;
  resize: vertical;
}
.ap-surface[data-surface="settings"] .archive-phrase .ap-btn-accent {
  align-self: flex-start;
}

/* Storage health — the persisted() / estimate() readout ([#3]) — as the row every
 * other value on this surface is. */
.ap-surface[data-surface="settings"] .storage-row {
  font-variant-numeric: tabular-nums;
}
.ap-surface[data-surface="settings"] .storage-v .u {
  color: var(--faint);
}

/* The shell version (#324): the one read-only fact here, so it stands below the last
 * group with the space that says it belongs to none of them. */
.ap-surface[data-surface="settings"] .ap-settings-version {
  display: block;
  margin-top: 48px;
}
.ap-surface[data-surface="settings"] .ap-settings-version-line {
  margin: 0;
}
.ap-surface[data-surface="settings"] .ap-settings-version-hash {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink);
}

@media (max-width: 640px) {
  .ap-surface[data-surface="settings"] .sync-panel .sync-actions,
  .ap-surface[data-surface="settings"] .sync-panel .sync-recover {
    grid-template-columns: 1fr;
  }

  .ap-surface[data-surface="settings"] .sync-panel .sync-actions {
    display: grid;
    align-items: stretch;
  }

  .ap-surface[data-surface="settings"] .sync-panel .sync-actions .ap-btn-accent,
  .ap-surface[data-surface="settings"] .sync-panel .sync-recover .ap-btn-accent {
    width: 100%;
  }

  .ap-surface[data-surface="settings"] .sync-panel .sync-actions-tertiary {
    justify-items: start;
  }
}
