/* ==== PORTED VERBATIM from wonderleaf-app/tools/explainer/static/reader.css (book mode, frozen baseline). Selector rewrites only: body.* -> #reader.* ; :root -> #reader.mode-book. ==== */
/* Into production from mockups/book-alive-2026-09-22/v9/field/app/bookview.css (#629 Phase 2,
   2026-09-26). Two changes, marked `#629` where they are:
   1. THE TYPE. The prototype linked `static/fonts/fonts-core.css` out of the wonderleaf-app tree
      for Lora / Inter / Caveat. The field ships NO webfonts at all — app/style.css sets --serif
      and --sans to system stacks with the language-aware --cjk chain appended — so mirroring
      hundreds of woff2 files into this bundle to serve one view is not this issue's call. The
      named faces stay FIRST in every chain (a host that does serve them still gets them), and
      what follows is now the field's own stack instead of Noto names nothing here can load. The
      book therefore reads in the same faces as the scroll view, and a CJK book is never tofu.
   2. THE FINIS MARK is a background-image here rather than an <img src> in bookview.js, so
      release/build_rc.py's graph walker can see the file and copy it into the RC.
   Nothing else is touched: no rule is retimed, recoloured or removed. */

 #reader.mode-book {
  /* Body + UI: chains fall through CJK fallbacks per glyph so the same
     stylesheet works for Latin, Korean, Japanese, and Simplified Chinese. */
  /* #629: the named faces first, then app/style.css's own stacks (var(--cjk) is retargeted per
     <html lang> there, which is how the field keeps Japanese kanji off Korean faces). */
  --serif: 'Lora', 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, 'Liberation Serif', 'DejaVu Serif', Times, var(--cjk), serif;
  --sans:  'Inter', ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', 'Liberation Sans', Arial, var(--cjk), sans-serif;
  --text-primary: #1a1a1a;
  --text-secondary: #6b6b6b;
  --text-tertiary: #9a9a9a;
  --bg: #fafaf7;
  --border: #ececec;
}

/* ── Reader 2026 (#501) design tokens ──────────────────────────────────────
   ADDITIVE ONLY. Every token is `--r26-*` because the names above and the
   `.flipbook`-scoped palette below are load-bearing elsewhere:
     · `--bg`/`--text-primary` are used by `_reader_critical.css`, which is
       BAKED into all ~2,340 published book <head>s — rebinding them desyncs
       every baked head with no re-render escape.
     · `--serif`/`--sans` carry the per-glyph CJK fallback chains. Rebinding
       either strips the chain and renders tofu on the 905 language variants.
   So: never repoint an existing token; only add. The look comes from weight,
   size and tracking over the faces the reader already loads AND gates
   (Lora / Inter / Caveat) — no new font family is introduced, which also
   keeps test_fonts_selfhosted_contract green. */
 #reader.mode-book {
  /* paper world */
  --r26-surface:    #fffdf3;   /* page surface */
  --r26-paper:      #fbf6e9;   /* app/desk cream */
  --r26-paper-deep: #f4eedb;   /* desk, card grounds */
  --r26-ink:        #1a1d24;
  --r26-body:       #4c4336;   /* body copy brown */
  --r26-muted:      #6a6d74;
  --r26-wood:       #5a3825;   /* kickers, folios, rules */
  --r26-wood-light: #7a5238;
  --r26-leaf:       #2f4a26;
  --r26-leaf-dark:  #24391d;
  --r26-gold-soft:  #edc87c;
  --r26-terra:      #a62d14;   /* fx-shout */
  --r26-hairline:   rgba(90,56,37,0.22);
  --r26-page-edge:  #e2d5ae;

  /* cloth cover world — foil text over the existing cloth and gold tokens */
  --r26-foil-text:  #f2e3c2;
  --r26-foil-dim:   rgba(242,227,194,0.75);

  /* type roles — gated faces only (Cormorant Garamond is CUT: it lives in
     fonts-chat.css, not fonts-core.css, so naming it renders a system
     fallback AND turns the self-hosted font contract red). */
  --r26-display:    var(--serif);            /* headings, finis, folios */
  --r26-prose:      var(--serif);            /* book prose */
  --r26-ui:         var(--sans);             /* buttons, kickers, meta */
  --r26-hand:       'Caveat', var(--serif);  /* cover title, fx voices */

  /* per-scene aura tints — 7 themes (schema.py COLOR_THEMES) + fallback.
     A NEW property name on purpose: each book bakes an inline <style> AFTER
     reader.css setting --accent/--theme-bg, so an equal-specificity rule on
     either of those loses. Nothing bakes --r26-tint. */
  --r26-tint-default:     rgba(226,203,160,0.34);   /* = warm_beige */
  --r26-tint-warm_yellow: rgba(237,200,124,0.34);
  --r26-tint-soft_blue:   rgba(148,177,200,0.28);
  --r26-tint-warm_beige:  rgba(226,203,160,0.34);
  --r26-tint-ocean_teal:  rgba(122,168,166,0.28);
  --r26-tint-lavender:    rgba(183,168,205,0.26);
  --r26-tint-sky_blue:    rgba(158,193,214,0.28);
  --r26-tint-warm_brown:  rgba(214,178,132,0.30);
}
/* === BOOK MODE — two-page spread flipbook (Storybook.html design) ======== */
/* #363 (founder 2026-07-02): double-tapping a page/arrow was triggering the
   browser's double-tap ZOOM mid-read on iPad. `manipulation` keeps pan +
   pinch but drops the double-tap-zoom gesture on the reading surfaces.
   (Pinch-zoom itself is additionally locked inside the native app only —
   reader.js rewrites the viewport meta under the WonderleafApp UA.) */
#reader.mode-book, .flipbook, .nav-zone, .view-toggle, .cover-jump {
  touch-action: manipulation;
}
/* Decorative palette for the printed-book aesthetic. Per-scene themes
   (.bm-image-page[data-theme=...] / .bm-prose-page[data-theme=...]) override
   --accent / --theme-bg / --bubble-bg via _theme_css() below. */
.flipbook {
  --paper:        #f5ecd1;
  --paper-warm:   #f0e3b9;
  --paper-edge:   #d9c891;
  --ink:          #281c10;
  --ink-soft:     #5a4327;
  --ink-faint:    #927853;
  --gold:         #b78a3a;
  --gold-2:       #e3c178;
  --cloth:        #1c2540;
  --cloth-2:      #283557;
  --cloth-3:      #14192d;
}

/* Top-left corner of the iframe. The parent chat shell pins its
   .book-toolbar (share / fav / delete / close) to TOP-right and its
   help button to BOTTOM-right — both were obscuring this toggle in
   earlier positions. Top-left is empty in the chat shell. */
.view-toggle {
  position: fixed;
  /* safe-area offsets: in the edge-to-edge native app (viewport-fit=cover,
     #363) the status bar / notch and home indicator overlap the physical
     edges; env() is 0 everywhere else, so web behavior is unchanged. */
  top: calc(14px + env(safe-area-inset-top, 0px));
  left: calc(14px + env(safe-area-inset-left, 0px));
  z-index: 100;
  background: rgba(255,255,255,0.92);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 14px;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,0.06);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  user-select: none;
}
.view-toggle:hover { color: var(--text-primary); }
/* When in book mode, give the toggle a parchment-warm look so it doesn't
   clash with the dark tabletop backdrop. */
#reader.mode-book .view-toggle {
  background: rgba(245, 236, 209, 0.92);
  color: #3b2c14;
  border-color: rgba(80,55,20,0.25);
}

/* Cover-jump button — sits next to the view-toggle in the top-left of
   the iframe. Click jumps straight back to the cover (state 0) without
   manually un-flipping every spread. */
.cover-jump {
  position: fixed;
  top: calc(14px + env(safe-area-inset-top, 0px));
  left: calc(134px + env(safe-area-inset-left, 0px));     /* directly right of .view-toggle (which is ~110px wide + 10px gap) */
  z-index: 100;
  background: rgba(255,255,255,0.92);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 14px;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,0.06);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  user-select: none;
  display: none;        /* only show in book mode (book-open or book-end) */
}
.cover-jump:hover { color: var(--text-primary); }
#reader.mode-book.book-open .cover-jump,
#reader.mode-book.book-end .cover-jump {
  display: block;
  background: rgba(245, 236, 209, 0.92);
  color: #3b2c14;
  border-color: rgba(80,55,20,0.25);
}
@media (max-width: 760px), (max-height: 500px), (orientation: portrait) {
  /* cover-jump is a flipbook-only control; hide it wherever the flipbook is
     forced to scroll — this list MUST match the force-scroll media query below
     (narrow, short-landscape, or portrait), else it floats over scroll content. */
  .cover-jump { display: none !important; }
}
#reader[data-preview="1"] .cover-jump { display: none !important; }

.scroll-top {
  position: fixed;
  right: calc(14px + env(safe-area-inset-right,0px));
  bottom: calc(16px + env(safe-area-inset-bottom,0px));
  z-index: 90;
  width: 44px;
  height: 44px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: rgba(255,255,255,0.92);
  color: var(--text-secondary);
  font-family: var(--sans);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,0.06);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
  transition: opacity 0.18s, transform 0.18s;
  user-select: none;
}
.scroll-top:hover { color: var(--text-primary); }
.scroll-top.visible {
  opacity: 1;
  pointer-events: auto;
  transform: none;
}
@media (max-width: 1000px) {
  /* #199: invited-tester / random-anon claim panels are parent-shell overlays
     pinned to the bottom edge; lift the iframe-owned back-to-top button above
     them so it isn't occluded.
     #235: scope the lift to when that panel is actually present — the parent
     passes ?claim=1 (read by reader.js -> #reader.has-tester-claim). Normal readers
     keep the bottom-right corner from the base rule above. */
  #reader.has-tester-claim .scroll-top { bottom: calc(152px + env(safe-area-inset-bottom,0px)); }
}

/* #92 read-aloud control. INJECTED by reader.js (not baked into renderer.py) so
   every already-published book gets it without a re-render. Bottom-left, mirrors
   the .view-toggle look and the .scroll-top safe-area treatment. A single stable
   button node (data-state drives the icon/spinner); progress + upsell are siblings.
   Read-aloud is a Plus/Family feature: free/anon see the pill as an UPGRADE
   affordance (PLUS badge + upsell), never a dead button. */
.wl-ra {
  position: fixed;
  left: calc(14px + env(safe-area-inset-left,0px));
  bottom: calc(16px + env(safe-area-inset-bottom,0px));
  z-index: 100;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: rgba(255,255,255,0.92);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 14px;
  min-height: 29px;
  box-sizing: border-box;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,0.06);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  user-select: none;
}
.wl-ra:hover { color: var(--text-primary); }
.wl-ra[hidden] { display: none !important; }
#reader.mode-book .wl-ra {
  background: rgba(245,236,209,0.92);
  color: #3b2c14;
  border-color: rgba(80,55,20,0.25);
}
.wl-ra .wl-ra-ico { font-size: 13px; line-height: 1; display: inline-flex; }
.wl-ra .wl-ra-spin {
  width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid rgba(120,90,40,0.25);
  border-top-color: rgba(120,90,40,0.85);
  animation: wlRaSpin 0.8s linear infinite;
  display: none;
}
.wl-ra[data-state="loading"] .wl-ra-spin { display: inline-block; }
.wl-ra[data-state="loading"] .wl-ra-ico { display: none; }
@keyframes wlRaSpin { to { transform: rotate(360deg); } }
.wl-ra-plus {
  font-size: 10px; font-weight: 600; color: #78561c;
  background: rgba(120,86,28,0.14);
  border-radius: 999px; padding: 1px 6px; margin-left: 1px;
}
.wl-ra-prog {
  position: fixed;
  left: calc(14px + env(safe-area-inset-left,0px));
  bottom: calc(51px + env(safe-area-inset-bottom,0px));
  z-index: 100;
  width: 160px;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  color: var(--r26-wood);
  pointer-events: none;
}
/* In book mode the fixed nav zones sit UNDER this readout — at 1920x1080 the
   whole progress display overlaps navPrev. With pointer-events:none the tap
   passes through to the zone and turns the page, so the NO_FLIP guard never
   even sees the event: the control activates the book underneath it, which
   is the exact defect class #161 exists for, on a LOCKED paid feature.
   Absorbing input here makes the guard reachable; the readout itself is not
   interactive, so the tap correctly does nothing. Scroll mode keeps
   pointer-events:none so the readout never blocks the page. */
#reader.mode-book .wl-ra-prog { pointer-events: auto; }
#reader.mode-book .wl-ra-prog { color: rgba(245,236,209,0.92); }
.wl-ra-prog[hidden] { display: none !important; }
.wl-ra-prog .bar {
  height: 3px; border-radius: 999px;
  background: rgba(150,130,90,0.28); margin-top: 4px; overflow: hidden;
}
#reader.mode-book .wl-ra-prog .bar { background: rgba(245,236,209,0.22); }
.wl-ra-prog .bar i {
  display: block; height: 100%; border-radius: 999px;
  /* --gold is scoped to .flipbook; this control is appended to <body>, so
     outside book mode the variable was undefined, the declaration invalid,
     and the progress FILL painted transparent — a paid feature with an
     invisible progress bar in scroll view. */
  background: var(--r26-wood-light); transition: width 0.3s ease;
}
#reader.mode-book .wl-ra-prog .bar i { background: rgba(245,236,209,0.85); }
/* Locked read-aloud upsell popover. Fixed-position, so it never shifts page
   layout (zero-defects CLS gate). BASE = the original legacy/LW popover, kept
   BYTE-UNCHANGED; #450's sample redesign is scoped to `.wl-ra-has-sample`
   (standard books only), so the LW popover renders exactly as before. */
.wl-ra-upsell {
  position: fixed;
  left: calc(14px + env(safe-area-inset-left,0px));
  bottom: calc(53px + env(safe-area-inset-bottom,0px));
  z-index: 102;
  width: 236px;
  background: #fffdf6;
  border: 1px solid rgba(80,55,20,0.18);
  border-radius: 14px;
  box-shadow: 0 12px 34px rgba(0,0,0,0.28);
  padding: 14px;
  font-family: var(--sans);
  font-size: 12.5px;
  line-height: 1.45;
  color: #3b2c14;
}
@media (max-height: 500px) {
  /* Short landscape phones: the popover is ~189px of real content anchored
     53px off the bottom, so on a 320px-tall viewport it reaches y=78 and the
     shared-home ✕ (z-index 200) lands ON TOP of its header — the control
     collision class #161 exists for. Bound it to the space that actually
     exists below the top chrome and let it scroll. */
  .wl-ra-upsell {
    max-height: calc(100vh - 160px - env(safe-area-inset-bottom, 0px));
    overflow-y: auto;
  }
}
.wl-ra-upsell[hidden] { display: none !important; }
.wl-ra-upsell b { font-weight: 650; font-size: 13px; display: block; margin-bottom: 3px; }
.wl-ra-upsell .wl-ra-sub { color: #6b5836; margin-bottom: 11px; }
.wl-ra-upsell .wl-ra-go {
  display: block; text-align: center;
  background: #2f6b4f; color: #fff; font-weight: 600;
  border: none; width: 100%;
  border-radius: 999px; padding: 9px 0; font-size: 12.5px;
  text-decoration: none; cursor: pointer;
}
.wl-ra-upsell .wl-ra-go:hover { background: #285c43; }

/* #450 sample redesign — STANDARD books only (mockup:
   2026-07-17-full-audit/read-aloud-sample). */
.wl-ra-upsell.wl-ra-has-sample {
  width: 266px; border-radius: 16px;
  box-shadow: 0 14px 38px rgba(0,0,0,0.30); padding: 15px;
}
.wl-ra-has-sample .am-eyebrow {
  display: flex; align-items: center; gap: 6px;
  font: 600 9.5px/1 var(--sans); letter-spacing: .13em;
  text-transform: uppercase; color: #78561c; margin: 0 0 7px;
}
.wl-ra-has-sample .am-eyebrow svg { width: 12px; height: 12px; display: block; }
.wl-ra-has-sample .am-head {
  font-family: var(--serif); font-size: 16px; font-weight: 600;
  line-height: 1.18; color: #2f230f; margin: 0 0 12px;
}
.wl-ra-has-sample .am-sample {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  cursor: pointer; padding: 9px 12px 9px 9px; margin: 0 0 12px;
  border-radius: 14px; border: 1px solid rgba(120,86,28,.34);
  background: linear-gradient(180deg, #fae8bb 0%, #edcb89 100%);
  box-shadow: 0 2px 9px rgba(120,86,28,.18), inset 0 1px 0 rgba(255,255,255,.55);
  -webkit-appearance: none;
}
.wl-ra-has-sample .am-disc {
  flex: 0 0 auto; position: relative; width: 38px; height: 38px; border-radius: 50%;
  background: #2f6b4f; display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 7px rgba(31,74,52,.42);
}
.wl-ra-has-sample .am-ring { position: absolute; inset: -4px; width: 46px; height: 46px; }
/* Loading spinner on the disc BEFORE playback actually begins (a slow uncached
   cut can take a moment) — reuses the pill's wlRaSpin keyframes. */
.wl-ra-has-sample .am-load {
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.4); border-top-color: #fff;
  animation: wlRaSpin 0.8s linear infinite;
}
.wl-ra-has-sample .am-txt { display: flex; flex-direction: column; min-width: 0; }
.wl-ra-has-sample .am-t1 { font: 650 13.5px/1.15 var(--sans); color: #342610; white-space: nowrap; }
.wl-ra-has-sample .am-t2 { font: 500 11px/1.25 var(--sans); color: #6d4e19; margin-top: 2px; white-space: nowrap; }
.wl-ra-has-sample .am-pitch { font: 400 12.5px/1.45 var(--sans); color: #5f4a2c; margin: 0 0 12px; }
.wl-ra-has-sample .wl-ra-go { font-family: var(--sans); }
/* "Now reading" highlight on the live scene-1 node while the sample plays. */
.am-glow {
  box-shadow: 0 0 0 3px rgba(224,170,74,.95), 0 0 26px 7px rgba(224,170,74,.48) !important;
  border-radius: 12px !important;
}
.am-chip {
  position: absolute; top: 10px; left: 10px; z-index: 6;
  display: flex; align-items: center; gap: 6px;
  font: 600 11px/1 var(--sans); color: #3b2c14; background: rgba(255,253,246,.95);
  border: 1px solid rgba(120,86,28,.32); border-radius: 999px; padding: 6px 11px 6px 9px;
  box-shadow: 0 3px 11px rgba(0,0,0,.20);
}
.am-chip .bars { display: flex; align-items: flex-end; gap: 2px; height: 11px; }
.am-chip .bars i { width: 2.4px; background: #2f6b4f; border-radius: 2px; }
.am-chip .bars i:nth-child(1) { height: 5px; }
.am-chip .bars i:nth-child(2) { height: 11px; }
.am-chip .bars i:nth-child(3) { height: 7px; }
.am-chip .bars i:nth-child(4) { height: 9px; }
.wl-ra-sr {
  position: absolute !important; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (min-width: 761px) and (min-height: 501px) and (orientation: landscape) {
  /* scroll-top is hidden only where the flipbook is actually ACTIVE — the exact
     inverse of the force-scroll query (wide AND tall AND landscape). Portrait
     tablets and short-landscape phones now scroll, so they KEEP back-to-top. */
  #reader.mode-book .scroll-top { display: none !important; }
  /* Already-baked books froze `#reader.mode-book{height:100vh}` in their inline
     critical CSS. On iOS/iPadOS Safari 100vh is the LARGE viewport (the toolbar
     never retracts here — book view is overflow:hidden), so the centered book
     sat below the visible midpoint ("slightly lower", founder 2026-07-17 on a
     Safari share link). This mutable-asset override reaches every published book
     without a re-render; dvh = the visible viewport. Scoped to the flipbook-
     active query so scroll mode (which sets height:auto) is untouched. Browsers
     without dvh drop this and keep the inline 100vh fallback. */
  #reader.mode-book { height: 100dvh; }
}

.page-indicator {
  position: fixed;
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  /* #629, 2026-09-26: OPAQUE, and no backdrop-filter. This is the one element of the ported
     stylesheet that the Field actually paints (.view-toggle, .cover-jump, .scroll-top, .wl-ra and
     the report/home-bar controls all belong to the chat shell and are never built here), so it is
     the one place the standing founder rule — no backdrop-filter, no large blurs, they stall the
     iPad GPU — had something to bite on. It sits on the reader's own flat paper ground, so the
     blur had nothing to show through it; the colour is the same rgba composited onto that ground,
     and the pill is pixel-identical. `winner/tools/check_clips.mjs` fails if any element under
     #reader ever gets a backdrop-filter back. */
  background: #f3e9cf;
  border-radius: 999px;
  padding: 5px 14px;
  font-family: 'Caveat', 'Nanum Pen Script', 'Yomogi', 'Liu Jian Mao Cao', var(--serif);   /* #629: style.css's fx-handnote chain */
  font-size: 17px;
  font-weight: 600;
  color: #5d4022;
  letter-spacing: 0.02em;
  box-shadow: 0 2px 6px rgba(0,0,0,0.18);
  display: none;
}
#reader.mode-book .page-indicator { display: block; }

.nav-zone {
  position: fixed;
  top: 0; bottom: 0;
  /* Reader 2026: the wide book reaches much further out than the old
     min(96vw,1100px) portrait one, so a fixed 22% zone would sit ON TOP of
     the book's own outer edges — a click 18-30% across the book box would
     hit navPrev (a no-op at the cover) instead of the page. Claim exactly
     the gutter beside the book, no more and NO LESS.
     --r26-book-w is published by sizeBook() because the width has THREE
     caps (94vw, 1300px, and a height-derived one); deriving it from the
     first two alone under-claims badly — at 1366x768 the real gutter is
     ~159px and a 94vw/1300px guess claims only 41px, leaving ~118px of
     untappable strip on each side. The fallback keeps a sane zone for the
     one frame before sizeBook runs. */
  width: max(0px, calc((100vw - var(--r26-book-w, min(94vw, 1300px))) / 2));
  z-index: 50;
  cursor: pointer;
  display: none;
  align-items: center;
  background: transparent;
  user-select: none;
}
#reader.mode-book .nav-zone { display: flex; }
/* At the closed states the book is translated across the midline, so the
   zone on the far side would swallow clicks meant for the cover. */
/* ONLY at the cover, and ONLY the backward zone: there is nothing before the
   cover, so that zone has no action. The forward zone at the BACK cover must
   stay live — one more "next" there is the only way to reach the
   recommendations page, and disabling it made that page unreachable by
   clicking (founder: "the recommended section ... is nowhere to be seen in
   book view"). Keyboard still worked, which is why the gate missed it. */
#reader.mode-book.book-closed .nav-zone.prev { pointer-events: none; }
/* ...and the NEAR side must grow to cover the strip the translate vacates.
   book-closed slides the book LEFT by 25% of its width, so a bookWidth/4
   band opens between the cover's right edge and the right-hand zone — ~262px
   at 1366x768. Without this the reader taps beside a closed cover and
   nothing happens. Mirrored at the back cover. */
/* Done with a pseudo-element rather than by widening the zone: the zone lays
   its arrow out with justify-content, so changing its WIDTH moves the arrow,
   and that move lands during the opening turn as a real layout shift (0.062,
   caught by the turn-instantaneity gate). The ::after extends only the hit
   area — the zone's own box, and therefore the arrow, never moves. */
#reader.mode-book.book-closed .nav-zone.next::after,
#reader.mode-book.book-end .nav-zone.prev::after,
#reader.mode-book.book-end .nav-zone.next::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  width: calc(var(--r26-book-w, min(94vw, 1300px)) * 0.25);
}
#reader.mode-book.book-closed .nav-zone.next::after { right: 100%; }
#reader.mode-book.book-end .nav-zone.prev::after { left: 100%; }
/* Push the arrow toward the OUTER edge of the viewport so it doesn't
   overlap the book page content. The click zone is still the full 22%
   so users can click anywhere on the left/right edge of the screen,
   but the visible button sits clear of the spread. */
.nav-zone.prev { left: 0; justify-content: flex-start; padding-left: 14px; }
.nav-zone.next { right: 0; justify-content: flex-end;   padding-right: 14px; }
.nav-zone .arrow {
  width: 44px; height: 44px;
  border-radius: 50%;
  background: rgba(245, 236, 209, 0.85);
  color: #5d4022;
  font-family: var(--sans);
  font-size: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.32);
  opacity: 0;
  transition: opacity 0.2s;
}
.nav-zone:hover .arrow { opacity: 1; }


/* Reader 2026: the desk is warm cream paper, not a dark tabletop. The dark
   backdrop lives in `_reader_critical.css`, which is inlined into every baked
   <head> BEFORE this file's <link> — so this equal-specificity rule wins for
   every already-published book with no re-render. The loading veil above it
   stays dark and untouched: it is founder-locked, and it paints on the first
   frame precisely because it is baked, so recolouring it here would only
   produce a dark→cream flash the moment reader.css lands. */
#reader.mode-book {
  /* #629 (founder, 2026-09-26: "get rid of vertical invisible lines in the
     background"): the desk was two very shallow radial gradients over cream,
     and a shallow gradient across a wide area steps into visible bands on an
     8-bit panel. The desk is now the one flat cream the scroll view stands on
     (app/style.css #reader), so both views share a ground and nothing on the
     desk can band or cost the compositor a large gradient repaint on a turn. */
  background: #fbf6e9;
}
#reader.mode-book::before {
  content: '';
  position: fixed;
  left: 50%; bottom: -10vh;
  width: 90vw; height: 60vh;
  transform: translateX(-50%);
  /* The warm shade pool the book cast on the desk is gone with the gradient
     desk (#629, same reason: a 7 % alpha radial over cream bands too). */
  background: none;
  pointer-events: none;
  z-index: 0;
}
#reader.mode-book .book.scroll-layout { display: none !important; }
#reader:not(.mode-book) .flipbook { display: none !important; }
/* Preview-mode (anon truncated payload) pins to scroll layout so the
   signup paywall stays visible; toggle hidden since there's no book
   payload to switch into. */
#reader[data-preview="1"] .view-toggle { display: none !important; }
#reader[data-preview="1"] .flipbook,
#reader[data-preview="1"] .nav-zone,
#reader[data-preview="1"] .page-indicator { display: none !important; }

@media (max-width: 760px), (max-height: 500px), (orientation: portrait) {
  /* Two-page spread doesn't fit phone-width viewports; founder choice:
     mobile users see the scroll layout while web is paginated. The
     `max-height: 500px` arm also catches LANDSCAPE phones (wide enough to
     dodge max-width:760px but too SHORT — ~390px — to fit the 459px book,
     which clipped top+bottom). Desktops are tall (>=600px) so never match.
     `orientation: portrait` arm (founder 2026-07): the two-page spread is
     cramped and clips text on PORTRAIT tablets (iPad portrait), so portrait
     always uses the scroll layout — the flipbook spread is LANDSCAPE-only.
     Landscape iPad (1024x768) keeps the spread; rotating to portrait swaps
     to scroll automatically. */
  #reader.mode-book .flipbook { display: none !important; }
  #reader.mode-book .book.scroll-layout { display: block !important; }
  #reader.mode-book .nav-zone, #reader.mode-book .page-indicator { display: none !important; }
  #reader.mode-book {
    background: var(--bg);
    display: block;
    height: auto;
    overflow: auto;
  }
  #reader.mode-book::before { display: none; }
  #reader.mode-book .view-toggle { display: none !important; }
  /* #reader.mode-book persists here even though the flipbook is HIDDEN and the
     nav zones are gone, so the book-mode rule that makes the read-aloud
     readout absorb taps has nothing to protect — it would just park a dead
     160px rectangle over scroll content. There is no zone underneath to
     steal the tap, so let it pass through as it does in scroll mode. */
  #reader.mode-book .wl-ra-prog { pointer-events: none; }
}

/* When the rendered HTML is printed or exported to PDF, force scroll
   layout (founder locked: 'PDF export should ONLY do scroll view'). The
   PDF export route additionally appends ?view=scroll to be belt-and-
   braces; this rule covers print stylesheets and user File→Print. */
@media print {
  #reader.mode-book { background: var(--bg); height: auto; overflow: visible; display: block; }
  #reader.mode-book::before { display: none; }
  #reader.mode-book .flipbook,
  #reader.mode-book .view-toggle,
  #reader.mode-book .nav-zone,
  #reader.mode-book .page-indicator { display: none !important; }
  #reader.mode-book .book.scroll-layout { display: block !important; }
}
@media print {
  /* All fixed reader chrome must never print, in EITHER view mode — PDFs render
     in scroll mode where the mode-book rule above doesn't apply (#97 follow-up:
     the Book/Scroll-view toggle was leaking into exported PDFs). */
  .view-toggle, .cover-jump, .scroll-top, .nav-zone, .page-indicator,
  .shared-home-bar, .wl-ra, .wl-ra-prog, .wl-ra-upsell,
  .wl-report-btn, .wl-report-back,
  .reader-recommendations { display: none !important; }  /* #452 endpaper: story-only PDF */
}

/* The flipbook container — two-page-spread book on a table. Closed state
   (book-closed) shifts it left so only the right-half (cover) sits
   centered, and hides the binding + paper-stack edges. Book-end is the
   mirror state for the back cover after the final flip. */
.flipbook {
  position: relative;
  /* Reader 2026: a WIDE landscape spread (2 : 1.2). Height stays the driver —
     prod sets an explicit `height`, and an explicit height beats
     `aspect-ratio`, so declaring aspect-ratio here would be silently ignored.
     Derive the width from it instead and keep the viewport caps. */
  height: var(--book-height, min(72vh, 740px));
  width: min(94vw, 1300px, calc(var(--book-height, min(72vh, 740px)) * 1.666));
  /* Board reveal around a landed page. THE ANATOMY LAW: this margin belongs
     to the opened cover and travels with it — there is no separate boards
     layer. Interior sheets inset by these; cover sheets do not. */
  --padY: clamp(15px, 2.6vh, 24px);
  --padX: clamp(15px, 1.4vw, 24px);
  margin: 0 auto;
  /* A turning sheet bulges toward the viewer, and the bulge scales with
     sheetWidth/perspective. Prod's old 2800px put it ~12px OUTSIDE the
     covers at 1440x900. 14000px (the spec's figure) fixes that, but measured
     across chromium/webkit/firefox it lands at 6.99px of clearance at
     1920x1080 — the widest the book ever gets, and 0.5% of its width. That
     is the correct value sitting exactly on its own tolerance, which is one
     rounding error or one zoom level from a page escaping the covers on the
     highest-bar surface. 18000px is the least visible lever (the projection
     is already near-orthographic, so ~3px less bulge mid-turn is not a look
     change) and buys ~11px everywhere. T-BV-09 asserts >=7px. */
  /* #629 (founder, 2026-09-26, Linux Firefox: seams on the board at rest AND mid-turn): the book is
     2D. No perspective, no preserve-3d, no backface culling; see the note on .bm-sheet. */
  /* #629 (founder, 2026-09-26: lines in the background). The book's shadow
     was `filter: drop-shadow(0 34px 60px ...)` on this whole 3D container: a
     60 px blur at 45 % alpha falls off across the cream in wide steps that
     read as vertical lines beside the book and horizontal ones under it, and
     a filter on a transforming container is the large-blur cost the iPad rule
     forbids. The shadow now belongs to the four BOARD faces (a tight
     box-shadow on the cover sheets' face-inner), so it is exactly the book's
     own footprint in every state and through every turn. */
  /* #629 (founder, 2026-09-26): no 3deg tilt. A tilted 3D container puts every
     straight edge in the book on a subpixel, and renderers draw hairline seams
     where two such layers meet (page edges, the picture box, the gutter).
     Flat, every edge is a whole pixel. */
  transform: translateX(0);
  transition: transform 0.95s cubic-bezier(0.42, 0.04, 0.58, 0.96),
              opacity 0.4s ease;
  z-index: 1;
}
#reader.mode-book.book-closed .flipbook {
  transform: translateX(-25%);
}
#reader.mode-book.book-end .flipbook {
  transform: translateX(25%);
}

/* Reduced motion: the page does not swing, it swaps. Content is identical
   and every state is still reachable — only the 1.1s arc and the .95s book
   slide are removed. Same mechanism prod already uses for `.no-animate`
   (jumpToCover / restoreSpread), so the faces land on their 0/180 rest
   instantly and backface culling shows the right one with no in-between. */
@media (prefers-reduced-motion: reduce) {
  .flipbook,
  .bm-sheet,
  .bm-face,
  .page-stack,
  .bm-binding { transition: none !important; }
}

/* Paper-stack edges — the thin striped band peeking out behind the
   open pages, suggesting many pages stacked under the visible spread. */
.page-stack {
  position: absolute;
  /* Inset to the PAGE block, not the boards — the laminations belong to the
     pages, which now sit inside the cover's margin. */
  top: calc(var(--padY) + 3px); bottom: calc(var(--padY) + 1px);
  width: 7px;
  z-index: -1;
  /* #629: flat page-block edge; the 1 px laminations were lines by construction. */
  background: #e9dcbc;
  box-shadow: none;
  transition: opacity 0.4s ease 0.2s;
}
.page-stack.right { left: calc(50% - 3px); border-radius: 0 2px 2px 0; }
.page-stack.left  { right: calc(50% - 3px); border-radius: 2px 0 0 2px; opacity: 0; }
/* A closed book is exactly ONE object: no leaking page edges at EITHER end.
   book-end previously revealed the left stack, which drew a second component
   beside the closed back board. */
#reader.mode-book.book-closed .page-stack { opacity: 0; transition-delay: 0s; }
#reader.mode-book.book-end .page-stack { opacity: 0; transition-delay: 0s; }

/* Sheets — stacked on the RIGHT half of the flipbook. Each sheet flips
   around its LEFT edge (the binding). After flipping, the sheet visually
   occupies the LEFT half (showing its back face). */
.bm-sheets {
  position: absolute;
  top: 0; bottom: 0;
  left: 50%;
  width: 50%;
  /* #629: its own stacking context, so the sheets' z-index hand-off (1000+ while turning) stays
     BELOW the spine (.bm-binding, z 200). preserve-3d used to create this context; without it
     the turning sheet covered the binding and the gutter went plain white mid-turn. */
  z-index: 0;
  /* The container must not hit-test, or it swallows the back board. In this
     preserve-3d context the container's own box sits at z=0 while the back
     board rests at translateZ(-2px) (the fore-edge separation below), so the
     board is BEHIND the container plane and `elementFromPoint` returns
     `.bm-sheets` instead of the sheet. Its per-sheet click handler then never
     fires, the final forward turn never commits, and the reader dead-ends one
     sheet short of the endpaper (#544; shipped 2026-07-29 in the same commit
     that added the -2px separation).
     Precisely, because the first draft of this comment got the mechanism
     wrong (Concierge r1): Share / Make / recommendations are NOT on a sheet's
     back face — `reader.js` appends that panel to `document.body`
     (`document.body.appendChild(panel)`), outside the flipbook entirely, and
     its own hit-testing was never broken. What #544 breaks is NAVIGATION to
     the `book-end` state that reveals it. Nor is the reader trapped: the nav
     zones (`#navPrev`/`#navNext`) are siblings outside this container, so the
     arrow still turns the page. The founder reproduced exactly that on a real
     iPad — page surface dead, arrow working — which is the signature of a
     container swallowing the hit, not of a dead control.
     The container carries no handler of its own, so making it transparent to
     hits costs nothing and leaves the 3D geometry — and the founder-verified
     fore-edge fix — byte-identical. */
  pointer-events: none;
}
.bm-sheet { pointer-events: auto; }
/* THE ANATOMY LAW (Reader 2026): a book is covers + pages hinged at a spine,
   and no physical part is ever drawn twice. Interior sheets are the smaller
   page block, inset from the boards everywhere EXCEPT at the spine; the two
   cover sheets are the boards themselves and stay full-size. The reveal you
   see around a landed page is therefore the opened cover's own margin. */
/* THE TURN IS 2D (#629, 2026-09-26). The book used to be a true 3D flip (rotateY under
   perspective 18000px, preserve-3d, backface-visibility, translateZ separations). At that
   perspective the turn is optically flat: a sheet at angle θ is drawn at cos θ of its width.
   But every engine draws 3D through its own special path, and each one drew seams: WebKit
   leaked the culled back face mirrored across the spine (hairlines on the left page mid-turn),
   Firefox/WebRender drew the double-mirrored, perspective-scaled board as tiles with visible
   joins 128 px apart (founder, Linux Firefox, at rest and mid-turn). Now the sheet is squashed
   about the spine with scaleX(cos θ) (bookview.js _animateTurn samples the old easing, so the
   motion is the same), the faces swap at edge-on on the same animation clock, and every engine
   draws the book through its ordinary 2D path. z-index (bookview.js) orders the sheets. */
.bm-sheet {
  position: absolute;
  top: var(--padY); bottom: var(--padY);
  left: 0; right: var(--padX);
  transform-origin: 0 50%;
  transform: none;
  cursor: pointer;
}
/* `.cover-sheet` does not exist in prod — the renderer marks PAGES, not
   sheets. `.bm-sheets` contains only sheets, so :first-child/:last-child are
   exact; :has() is the explicit intent where supported. */
.bm-sheet:first-child,
.bm-sheet:last-child,
.bm-sheet:has(.bm-cover),
.bm-sheet:has(.bm-back-cover) {
  top: 0; bottom: 0; right: 0;
}
/* The board's INNER face carries a page, so the inset travels with the cover
   (rather than a separate layer under it): first sheet's back = inside front
   cover, last sheet's front = inside back cover. */
.bm-sheet:first-child .bm-face-back .bm-page,
.bm-sheet:has(.bm-cover) .bm-face-back .bm-page {
  top: var(--padY); bottom: var(--padY);
  left: var(--padX); right: 0;
  box-shadow: 0 1px 6px rgba(10,12,24,0.35);
}
.bm-sheet:last-child .bm-face-front .bm-page,
.bm-sheet:has(.bm-back-cover) .bm-face-front .bm-page {
  top: var(--padY); bottom: var(--padY);
  left: 0; right: var(--padX);
  box-shadow: 0 1px 6px rgba(10,12,24,0.35);
}
.bm-sheet.flipped {
  transform: scaleX(-1);
  /* Flipped sheets are still clickable: clicking the most-recently-flipped
     sheet's back face calls flip(-1), going back one spread. Keep
     cursor: pointer so the affordance is honest. */
}
/* The back board sits a hair BEHIND the page-stack plane. Every sheet rests
   at z=0, so a cover landing at the end of its turn becomes near-coplanar
   with the whole stack and WebKit's plane-split then DROPPED the static back
   board's paint for the last ~3 frames of a close: the navy fore-edge band
   vanished and the book showed bare page cream to its right edge ("the right
   side of the back cover disappears" — founder, real Safari). 2px keeps the
   board out of every tie; at perspective 18000px the offset is invisible
   (0.01% scale). Only the BACK board: the same offset on the FRONT board
   makes its own turning quad dip through z=0 mid-turn and get split —
   frame-verified to reintroduce the vanishing-cover bug. */
.bm-sheet:last-child,
.bm-sheet:has(.bm-back-cover) {
  transform: none;   /* #629: 2D book, no z separation needed (z-index orders the sheets) */
}
/* Same specificity (0,2,0) as `.bm-sheet.flipped` above — this pair must sit
   AFTER the rest-pose back-board rule so a flipped back board keeps both its
   angle and its -2px offset. */
.bm-sheet:last-child.flipped,
.bm-sheet:has(.bm-back-cover).flipped {
  transform: scaleX(-1);
}
/* Used by jumpToCover() and restoreSpread() to snap state changes without
   animating: the sheets jump straight to their 0°/180° rest so
   backface-visibility shows the correct face instantly (no in-between). */
.flipbook.no-animate .bm-sheet,
.flipbook.no-animate .bm-face,
.flipbook.no-animate.flipbook,
.flipbook.no-animate {
  transition: none !important;
}

/* Each face is split into outer (transform + backface-visibility, NO border-
   radius / overflow) and inner (border-radius + overflow:hidden + content).
   Putting border-radius+overflow on the SAME element as backface-visibility
   triggers a long-standing WebKit bug where the back face leaks through. */
.bm-face {
  position: absolute;
  inset: 0;
  background: transparent;
  /* Which face paints is governed SOLELY by backface-visibility (above) —
     angle-based, so it hands off at exactly 90° and stays continuous through
     the whole turn in BOTH directions. A former belt-and-braces `visibility`
     swap (time-gated `transition: visibility 0s linear 0.55s` + .flipped
     visibility:hidden rules) is deliberately gone: its 0.55s delay fired the
     swap AFTER the eased rotation had already passed 90°, so from ~90°→~115°
     the back face was still visibility:hidden while it faced the viewer — a
     blank "dead zone" that dropped the second half of the page-turn (the page
     vanished at the top of its arc and the destination just appeared). The
     back-face leak the swap also guarded is handled by the outer/inner split
     below (border-radius+overflow off the backface-visibility element). */
}
/* translateZ(4px): the two faces of a sheet are otherwise EXACTLY coplanar
   (front at 0deg, back at 180deg, same plane), and during an animated turn
   WebKit's plane-split resolved that tie badly — for sheet angles ~90-150deg
   NEITHER face rasterized and the turning cover vanished outright
   (frame-verified; 0.4px of separation already cured it). The offset is
   applied in the face's own rotated frame, so the back face sits behind the
   sheet when the sheet shows its front and in front of it once the sheet
   passes 90deg — the depth order is numerically correct at every angle. At
   perspective 18000px, 4px of z is invisible (<0.03% scale, <0.5px shift). */
.bm-face-back { transform: scaleX(-1); }   /* #629: mirrored inside a mirrored (flipped) sheet = reads the right way */
/* Canonical card-flip: the FRONT face needs its OWN 3D transform too. WebKit
   only honors backface-visibility on a face that is itself a 3D layer; a
   non-transformed child of the preserve-3d sheet is not back-cullable, so the
   cover's front face was NEVER culled and leaked mirror-flipped onto the left
   page — both DURING the turn (past 90°) and at rest (founder 2026-07-15, real
   iPad; reproduced in WKWebView: the navy endpaper showed the cover title
   reversed). `.bm-face-back` worked only because its rotateY(180deg) made it a
   3D layer. rotateY(0deg) is a visual no-op that makes the front face a 3D
   layer whose backface-visibility:hidden reliably culls it — angle-based, so
   it hides at exactly 90° with NO dead zone (the 96ed126 second-half fix is
   preserved) and NO rest leak. Belt-and-braces: the outer/inner split still
   keeps border-radius/overflow off the backface-visibility element. */
.bm-face-front { transform: none; }
/* #629: which face shows is chosen here, not by backface culling: the front of an unturned
   sheet, the back of a turned one. During a turn bookview.js animates the faces' visibility on
   the SAME Web Animations clock as the sheet's scaleX, swapping them where the sheet is edge-on. */
.bm-sheet:not(.flipped) > .bm-face-back { visibility: hidden; }
.bm-sheet.flipped > .bm-face-front { visibility: hidden; }
/* While it turns, both faces of the sheet are live and bookview.js steps their opacity at edge-on
   on the compositor, in step with the squash. Same specificity as the rest rules, and after them. */
.bm-sheet.bm-turning > .bm-face { visibility: visible; }
.bm-face-inner {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--r26-surface);
}
/* #629 (founder, 2026-09-26): no gutter shade and no page-edge hairline. The
   inset shadow with a negative spread drew a hard vertical edge 22 px from
   the spine on some renderers, and the 1 px inset line IS a line. The spine is
   drawn once, by .bm-binding. */
/* #629 (founder, 2026-09-26, second mid-turn frame): no rounded corners on a
   page face. `overflow: hidden` + `border-radius` on a face that holds a
   composited descendant (the clip <video>) makes WebKit composite the face
   as a masked layer of its own, and a composited child of a hidden back face
   is not culled with it: it is drawn mirrored across the spine and its edge
   shows as a hairline that trails the turning sheet. Square page corners. */
.bm-face-front .bm-face-inner {
  border-radius: 0;
  box-shadow: none;
}
.bm-face-back .bm-face-inner {
  border-radius: 0;
  box-shadow: none;
}
/* The two cover sheets ARE the boards, on BOTH their faces — so the margin
   revealed around an open page is cloth, and it travels with the cover
   because it *is* the cover. Without this the boards' inner faces stayed
   page-cream and the book read as loose sheets on a desk rather than a
   bound case. */
.bm-sheet:first-child .bm-face-inner,
.bm-sheet:last-child .bm-face-inner,
.bm-sheet:has(.bm-cover) .bm-face-inner,
.bm-sheet:has(.bm-back-cover) .bm-face-inner {
  background:
    linear-gradient(135deg, var(--cloth-2) 0%, var(--cloth) 50%, var(--cloth-3) 100%);   /* #629: sheen radial removed */
}
/* Little Wonders binds its own case colour. The rule above paints the BOARD
   (the sheet's face-inner), which LW never overrode because that surface did
   not exist before this reskin — so an LW book showed a sage outer cover with
   a NAVY inner board. Same sage gradient as #reader.lw-edition .bm-cover. */
#reader.lw-edition .bm-sheet:first-child .bm-face-inner,
#reader.lw-edition .bm-sheet:last-child .bm-face-inner,
#reader.lw-edition .bm-sheet:has(.bm-cover) .bm-face-inner,
#reader.lw-edition .bm-sheet:has(.bm-back-cover) .bm-face-inner {
  background:
    linear-gradient(150deg, #376a4d 0%, #2f5f44 55%, #265239 100%);   /* #629: sheen radial removed */
}
/* Boards carry a deeper fore-edge radius than the pages they hold. */
.bm-sheet:first-child .bm-face-front .bm-face-inner,
.bm-sheet:last-child .bm-face-front .bm-face-inner {
  border-radius: 3px 14px 14px 3px;
}
.bm-sheet:first-child .bm-face-back .bm-face-inner,
.bm-sheet:last-child .bm-face-back .bm-face-inner {
  border-radius: 14px 3px 3px 14px;
}
.bm-sheet:first-child .bm-face-front .bm-face-inner,
.bm-sheet:last-child .bm-face-back .bm-face-inner {
  box-shadow:
    0 18px 26px -14px rgba(45,24,16,0.42),
    0 3px 8px rgba(45,24,16,0.14);   /* #629: the two inset bevel lines are gone */
}
/* #629: the book's shadow is the boards' own (founder, 2026-09-26). The outer
   faces above (front cover, back cover) and the inner boards below (the cloth
   margins an open book shows) each cast a tight shadow of their own footprint,
   so closed, open, at the end and mid-turn the shadow is exactly where board
   is. No slab, no filter: nothing can outline itself on the desk. */
.bm-sheet:first-child .bm-face-back .bm-face-inner,
.bm-sheet:last-child .bm-face-front .bm-face-inner {
  box-shadow:
    0 18px 26px -14px rgba(45,24,16,0.42),
    0 3px 8px rgba(45,24,16,0.14);
}

/* Paper grain is drawn by .bm-page's own ::before (below), NOT by a veil over
   .bm-face-inner. A full-size layer stacked above the content of every face
   is one more thing the compositor must blend on each frame of a turn. */
/* The old radial-dot grain is replaced by the tile above. Killing it here
   would also kill the cloth linen weave (a different rule on the same
   pseudo-element sets only background-image, so `display` would cascade
   through), so the covers re-assert it. */
.bm-page::before {
  background-image: url('img/grain.png?v=1');
  background-size: 96px 96px;
  opacity: 0.5;
  mix-blend-mode: normal;
}

/* Spine binding shadow visible when book is open */
.bm-binding {
  position: absolute;
  left: calc(50% - 4px);
  top: 1px; bottom: 1px;
  width: 8px;
  z-index: 200;
  pointer-events: none;
  border-radius: 2px;
  /* Dark cloth hinge: the spine of the case, seen between the two page
     blocks — cool navy rather than neutral black so it reads as the cover
     material continuing through the gutter. */
  background:
    linear-gradient(to right,
      rgba(10,14,30,0) 0%,
      rgba(10,14,30,0.55) 30%,
      rgba(20,25,45,0.75) 50%,
      rgba(10,14,30,0.55) 70%,
      rgba(10,14,30,0) 100%);
  opacity: 1;
  transition: opacity 0.45s ease 0.3s;
}
#reader.mode-book.book-closed .bm-binding { opacity: 0; transition-delay: 0s; }
#reader.mode-book.book-end .bm-binding { opacity: 0; transition-delay: 0s; }

/* Generic page styling shared by image / prose / title / endpaper pages. */
.bm-page {
  position: absolute;
  inset: 0;
  background: var(--paper);
  /* #629 (founder, 2026-09-26): the two shallow radial glows that sat here
     banded into horizontal lines on 8-bit panels. Flat paper; the grain
     tile on ::before is what gives the page its surface. */
  overflow: hidden;
}
/* Subtle paper grain on every page (multiply blend with the page bg). */
.bm-page::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(circle at 17% 23%, rgba(120,80,30,0.05) 0 1px, transparent 1.5px),
    radial-gradient(circle at 73% 41%, rgba(120,80,30,0.04) 0 1px, transparent 1.5px),
    radial-gradient(circle at 41% 78%, rgba(120,80,30,0.05) 0 1px, transparent 1.5px),
    radial-gradient(circle at 89% 86%, rgba(120,80,30,0.04) 0 1px, transparent 1.5px);
  background-size: 31px 29px, 38px 41px, 27px 33px, 44px 37px;
  pointer-events: none;
  mix-blend-mode: multiply;
}

/* === COVER (cloth-bound, gold-foil frame, no image) ===================== */
.bm-cover, .bm-back-cover {
  background:
    linear-gradient(135deg, var(--cloth-2) 0%, var(--cloth) 50%, var(--cloth-3) 100%);   /* #629: sheen radial removed */
  overflow: hidden;
}
.bm-cover::before, .bm-back-cover::before {
  /* Replace the paper grain with a faint linen weave on the cloth. */
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    repeating-linear-gradient(0deg,   rgba(255,255,255,0.025) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg,  rgba(0,0,0,0.06)        0 1px, transparent 1px 3px);
  pointer-events: none;
  opacity: 0.8;
  mix-blend-mode: normal;
}
.bm-cover-spine {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 18px;
  background:
    linear-gradient(to right,
      rgba(0,0,0,0.55) 0%,
      rgba(0,0,0,0.35) 35%,
      rgba(0,0,0,0.10) 75%,
      rgba(0,0,0,0) 100%);
  z-index: 5;
}
.bm-back-cover-spine {
  position: absolute;
  right: 0; top: 0; bottom: 0;
  width: 18px;
  background:
    linear-gradient(to left,
      rgba(0,0,0,0.55) 0%,
      rgba(0,0,0,0.35) 35%,
      rgba(0,0,0,0.10) 75%,
      rgba(0,0,0,0) 100%);
  z-index: 5;
}
.bm-cover-inner {
  position: absolute;
  /* Proportional, not fixed px: the board is now a wide landscape format and
     a 38/44px frame read as a hairline margin on it. */
  inset: 6.5%;
  border: 1px solid rgba(227,193,120,0.55);
  border-radius: 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 40px 32px;
  z-index: 2;
}
.bm-cover-inner::after {
  /* inner double rule */
  content: '';
  position: absolute;
  inset: 6px;
  border: 1px solid rgba(227,193,120,0.30);
  border-radius: 1px;
  pointer-events: none;
}
.cover-eyebrow {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--gold-2);
  opacity: 0.85;
  margin-bottom: 18px;
}
/* Cover/title/finis ornaments — dot · ✦ · dot. Flexbox layout puts the
   star DEAD CENTER between the two dots regardless of the unicode glyph's
   own bounding-box quirks. The bare ✦ character isn't horizontally
   centered in its glyph box, so margin-based layouts always looked
   left-leaning. flex + gap fixes it. */
.cover-ornament,
.title-ornament {
  margin-bottom: 28px;
  user-select: none;
}
.title-ornament { margin-bottom: 0; }
.ornament-row {
  display: inline-flex;
  align-items: center;
  gap: 18px;
  line-height: 1;
}
.ornament-row .dot {
  display: inline-block;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--gold-2);
}
.ornament-row .ornament-star {
  font-family: var(--serif);
  font-style: normal;
  font-size: 22px;
  line-height: 1;
  color: var(--gold-2);
  opacity: 0.9;
}
.ornament-row .ornament-logo {
  width: 22px;
  height: 22px;
  object-fit: contain;
  display: block;
  /* #629: naming the file HERE is what puts it in the RC — build_rc.py walks CSS url() and JS
     imports, and never a src="" written by a script. */
  background: url('img/wonderleaf-logo-64.png') center/contain no-repeat;
}
/* Title page is on warm paper, not cloth — recolor the ornament. */
.bm-title-page .ornament-row .dot { background: var(--ink-faint); }
.bm-title-page .ornament-row .ornament-star { color: var(--ink-faint); opacity: 1; }
.cover-title {
  /* Hand-lettered, foil-bright. Caveat is critical-gated (preloaded AND in
     reader.js's reveal gate), so it is resident before the cover is shown —
     an ungated display face here would FOUT at reveal. The chain falls
     through var(--serif) for CJK, so Korean/Japanese/Chinese covers keep
     their Noto Serif faces instead of rendering tofu. */
  font-family: var(--r26-hand);
  font-weight: 700;
  font-size: clamp(26px, 3.4vw, 44px);
  line-height: 1.08;
  letter-spacing: 0;
  color: var(--r26-foil-text);
  margin: 0 0 14px;
  text-shadow:
    0 1px 0 rgba(0,0,0,0.45),
    0 0 22px rgba(227,193,120,0.18);
  max-width: 90%;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.cover-question {
  font-family: var(--r26-prose);
  font-style: italic;
  font-size: clamp(13px, 1.4vw, 17px);
  letter-spacing: 0.02em;
  color: var(--r26-foil-dim);
  margin: 0;
  max-width: 90%;
  overflow-wrap: anywhere;
}
/* "Wonderleaf Editions" — existing user-facing copy. Restyled to sit inside
   the new gold frame; never hidden. */
.cover-imprint {
  position: absolute;
  bottom: 9%;
  left: 0; right: 0;
  text-align: center;
  font-family: var(--r26-ui);
  font-weight: 600;
  font-size: 9.5px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: rgba(227,193,120,0.8);
  z-index: 2;
}

/* Back cover — minimal device */
.bm-back-cover-inner {
  position: absolute;
  inset: 38px 56px 38px 44px;
  border: 1px solid rgba(227,193,120,0.40);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.back-mark {
  text-align: center;
  line-height: 1.7;
  max-width: 60%;
  /* Container stays neutral so the upright sparkle reads as upright; the
     italic styling applies only to the text. */
}
.back-mark .sparkle {
  display: block;
  font-family: var(--serif);
  font-style: normal;        /* upright ✦ — italic was making it lean left */
  font-size: 18px;
  margin-bottom: 12px;
  letter-spacing: 0;
  color: var(--gold-2);
  /* opacity .7 put this at 4.25:1 against the lightest cloth stop — under
     the locked floor. .85 is 5.51:1 and still reads as a quiet mark. */
  opacity: 0.85;
}
.back-mark .back-mark-text {
  display: block;
  font-family: var(--serif);
  font-style: italic;
  font-size: 14px;
  letter-spacing: 0.04em;
  color: rgba(227,193,120,0.8);
}

/* === ENDPAPER (inside front + inside back) ============================== */
.bm-endpaper {
  position: absolute;
  inset: 0;
  background: #182140;   /* #629: was a navy radial; flat, no banding */
  overflow: hidden;
}
/* Endpaper has NO ::before dots — the Storybook design's constellation
   pattern read as 'dirty' against the smooth navy background once it was
   rendered at production size, so it was removed 2026-05-18. The bm-page
   inherits a paper-grain ::before from .bm-page, but we override that to
   `display: none` so the endpapers stay clean dark navy. */
.bm-endpaper::before { display: none; }
.bm-endpaper-mark {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--serif);
  font-style: italic;
  font-size: 14px;
  color: rgba(227,193,120,0.8);
  letter-spacing: 0.16em;
}

/* === TITLE PAGE ========================================================= */
.bm-title-page .title-block {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 40px 50px;
}
.bm-title-page .title-eyebrow {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  /* --ink-faint (#927853) measures 3.30:1 on the title page and only 4.17:1
     even on pure white — under the LOCKED 4.5:1 floor wherever it lands.
     --r26-wood-light is 6.66:1 and reads as the same quiet kicker. */
  color: var(--r26-wood-light);
  margin-bottom: 32px;
}
.bm-title-page .title-rule {
  width: 56px;
  height: 1px;
  background: var(--ink-faint);
  opacity: 0.5;
  margin: 0 auto 32px;
}
.bm-title-page .title-main {
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(30px, 4.8vw, 52px);
  line-height: 1.05;
  color: var(--ink);
  letter-spacing: -0.01em;
  margin: 0 0 24px;
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.bm-title-page .title-question {
  font-family: var(--serif);
  font-style: italic;
  font-size: 17px;
  color: var(--ink-soft);
  margin: 0 0 48px;
  letter-spacing: 0.02em;
  max-width: 90%;
  overflow-wrap: anywhere;
}
.bm-title-page .title-ornament {
  font-size: 22px;
  color: var(--ink-faint);
  letter-spacing: 0.4em;
  user-select: none;
}
.bm-title-page .title-imprint {
  position: absolute;
  bottom: 56px;
  left: 0; right: 0;
  text-align: center;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 9.5px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  /* Was --ink-faint at .7 = 2.18:1. The opacity multiplier composites before
     the eye sees it, so it counts against the floor. */
  color: var(--r26-wood-light);
}

/* === IMAGE PAGE (LEFT page of a scene spread) =========================== */
/* Pure flexbox layout — NOT absolute positioning. Earlier attempts at
   absolute positioning had a containing-block walk-up bug in some
   browsers (the .illo's `position: absolute` could resolve to the
   .flipbook ancestor instead of .bm-image-page, dragging the image to
   the webpage center instead of the left-page center). Flex layout
   sidesteps all containing-block / 3D-transform quirks. */
/* Per-scene aura tint. Declared on a NEW property (--r26-tint) and prefixed
   with #reader.mode-book: every book bakes an inline <style> AFTER reader.css
   that sets --accent/--theme-bg per data-theme, so an equal-specificity rule
   on either of those would lose. The fallback is declared first so a book
   shipping a color_theme we don't know still renders a sane aura. */
#reader.mode-book .bm-image-page { --r26-tint: var(--r26-tint-default); }
#reader.mode-book .bm-image-page[data-theme="warm_yellow"] { --r26-tint: var(--r26-tint-warm_yellow); }
#reader.mode-book .bm-image-page[data-theme="soft_blue"]   { --r26-tint: var(--r26-tint-soft_blue); }
#reader.mode-book .bm-image-page[data-theme="warm_beige"]  { --r26-tint: var(--r26-tint-warm_beige); }
#reader.mode-book .bm-image-page[data-theme="ocean_teal"]  { --r26-tint: var(--r26-tint-ocean_teal); }
#reader.mode-book .bm-image-page[data-theme="lavender"]    { --r26-tint: var(--r26-tint-lavender); }
#reader.mode-book .bm-image-page[data-theme="sky_blue"]    { --r26-tint: var(--r26-tint-sky_blue); }
#reader.mode-book .bm-image-page[data-theme="warm_brown"]  { --r26-tint: var(--r26-tint-warm_brown); }

.bm-image-page {
  display: flex;
  flex-direction: column;
  padding: 4% 4.5%;
  /* #629: the tint pool behind the plate is gone (it banded, and the founder
     wants nothing drawn behind the painting); the plate sits on flat paper. */
  background: var(--r26-surface);
}
.bm-image-page .scene-num {
  flex: 0 0 auto;
  margin-bottom: 14px;
  font-family: var(--r26-ui);
  font-weight: 600;
  font-size: 9.5px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  /* No opacity multiplier: --r26-wood-light at .7 over the page surface is
     3.36:1, under the LOCKED 4.5:1 floor for content text. Full strength is
     6.66:1 and still reads as quiet book furniture at 9.5px. */
  color: var(--r26-wood-light);
}
.bm-image-page .illo {
  /* flex: 1 1 0 with min-height:0 lets the flex item shrink to the
     real available space (default min-content would hold it open to the
     image's natural height). Image inside is then centered via flex. */
  flex: 1 1 0;
  min-height: 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.bm-image-page .illo .illustration-container {
  /* Container fills .illo. Image inside fits via object-fit:contain so
     the full painting always shows, centered, with letterboxing only
     where the page aspect doesn't match. */
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  /* #629 (founder, 2026-09-26): no rounded clip box. A clip layer around the
     painting (and the clip's <video>) drew its own edges as hairlines on some
     renderers; the painting is centred with object-fit and needs no clipping. */
  border-radius: 0;
  /* Override scroll-mode rules that would clobber book layout: */
  aspect-ratio: auto;        /* scroll mode sets 3/2 */
  background: transparent;   /* scroll mode sets #eee / var(--bg) */
  overflow: visible;
  /* No box-shadow in book mode — founder direction: clean, no shadow. */
}
.bm-image-page .illo .illustration {
  display: block;
  width: 100%;
  height: 100%;
  /* CRITICAL: explicit contain (not cover). The scroll-mode .illustration
     rule sets `object-fit: cover` which would crop/zoom the image to fill
     the container. Without this override the image bleeds across the
     page (founder reported 2026-05-18: "image left edge starts at center,
     cuts off at right edge, repeats from left edge" — that was cover
     scaling the 3:2 painting to fill the taller-than-wide page box). */
  object-fit: contain;
  object-position: center center;
  /* #629: no blend on the plate (founder's no-blend rule; a blend layer is
     one more composited edge). The scroll view has never blended either. */
  mix-blend-mode: normal;
  /* NO filter here. A drop-shadow on this element — which already carries
     mix-blend-mode — sits inside the sheet's animating 3D transform and
     forces a re-rasterisation on every turn. The plate's lift is drawn by
     the container's box-shadow instead, which costs the compositor nothing. */
}
.bm-image-page .illo .illustration-placeholder {
  /* Placeholder during generation — keep simple 3:2 aspect. */
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  max-height: 100%;
}

/* === PROSE PAGE (RIGHT page of a scene spread) ========================== */
/* Pure flex layout — see .bm-image-page above for rationale. */
.bm-prose-page {
  display: flex;
  flex-direction: column;
  padding: 6% 7%;
  background: var(--r26-surface);
}
.bm-prose-page .scene-head {
  flex: 0 0 auto;
  margin-bottom: 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-family: var(--r26-ui);
  font-weight: 600;
  font-size: 9.5px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  /* No opacity multiplier: --r26-wood-light at .7 over the page surface is
     3.36:1, under the LOCKED 4.5:1 floor for content text. Full strength is
     6.66:1 and still reads as quiet book furniture at 9.5px. */
  color: var(--r26-wood-light);
}
.bm-prose-page .scene-head .swatch {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--accent, var(--ink-faint));
  margin-right: 10px;
  vertical-align: middle;
}
.bm-prose-page .prose {
  /* Flex 1 to fill the available space between scene-head and page-foot.
     min-height:0 to allow shrink. justify-content:center vertically
     centers the text within the available column. */
  flex: 1 1 0;
  min-height: 0;
  font-family: var(--r26-prose);
  font-size: clamp(14.5px, 1.42vw, 19px);
  line-height: 1.76;
  color: var(--r26-body);
  /* A book page is a FIXED-HEIGHT box, so a narrower measure makes prose
     taller. `overflow-y: auto` below means an over-long scene scrolls rather
     than losing its last lines — but a book page that scrolls is itself a
     defect on this surface, so T-BV-13 sweeps the corpus asserting
     scrollHeight <= clientHeight at every viewport. */
  max-width: min(38ch, 100%);
  margin-inline: auto;
  width: 100%;
  padding: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(140,100,40,0.35) transparent;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.bm-prose-page .prose::-webkit-scrollbar { width: 4px; }
.bm-prose-page .prose::-webkit-scrollbar-thumb {
  background: rgba(140,100,40,0.35);
  border-radius: 2px;
}
.bm-prose-page .prose p {
  margin: 0 0 1em;
  /* Ragged right. Justifying a 38ch measure opens rivers and forces
     hyphenation mid-word, which reads badly to a child. */
  text-align: left;
  hyphens: manual;
}
.bm-prose-page .prose p:last-child { margin-bottom: 0; }

/* ==== #349 Little Wonders edition skin (#reader.lw-edition) ==================
   Set by the /lw/{slug} render. Founder-approved design (mockups in
   qc-ipad-bookview/LW-BOOK-MOCKUP-*.png): sage picture-book cover with a
   sprout, Fredoka type, warm brown captions with one orange "pop" word.
   The flipbook STRUCTURE (sheets, turns, arrows, cover-jump) is the
   standard reader's — only the skin changes. */

/* --- caption pages: classic picture-book scale, Fredoka, centered ------- */
#reader.lw-edition .bm-prose-page .prose {
  color: #3b2a1e;
}
/* No drop cap on a centered one-line caption — it reads as a typo at
   picture-book scale. */
#reader.lw-edition .bm-prose-page .prose p:first-child::first-letter {
  color: inherit;
}
/* The art reads as a mounted picture card (mockup): rounder corners, a
   soft warm shadow with an ivory keyline. */
#reader.lw-edition .bm-image-page .illustration {
  border-radius: 16px;
  box-shadow:
    0 0 0 1.5px rgba(255, 252, 244, 0.96),
    0 14px 30px -10px rgba(60, 40, 25, 0.30);
}
/* One playful word per caption (server marks it **word** -> fx-pop): warm
   orange. Founder 2026-07-03: fx must fire when the PAGE ARRIVES, not at
   document load (every hidden page animating at once left turned-to pages
   frozen mid-pose and wiggling on one shared clock). At rest the word sits
   in its finished pose; reader.js moves .lw-fx-live to the newly-revealed
   spread after each flip settles, replaying the entrance pop + a finite
   triple wiggle. Standard books keep fx-pop exactly as-is. */
#reader.lw-edition .bm-page .prose .fx-pop {
  color: #d8872e;
}
/* Scroll layout (phones) is linear — the load-time entrance is natural
   there; just keep the wiggle finite and the word orange. */
#reader.lw-edition .scene .prose .fx-pop {
  color: #d8872e;
}
@keyframes lw-popwiggle {
  0%, 88%, 100% { transform: rotate(0) scale(1); }
  92%           { transform: rotate(-6deg) scale(1.12); }
  96%           { transform: rotate(5deg) scale(1.12); }
}
@media (prefers-reduced-motion: reduce) {
}

/* --- sage cover + back cover (mockup LW-BOOK-MOCKUP-COVER) -------------- */
#reader.lw-edition .bm-cover,
#reader.lw-edition .bm-back-cover {
  background:
    linear-gradient(150deg, #376a4d 0%, #2f5f44 55%, #265239 100%);   /* #629: sheen radial removed */
}
#reader.lw-edition .bm-cover::before,
#reader.lw-edition .bm-back-cover::before { opacity: 0.35; }
#reader.lw-edition .bm-cover-inner { border-color: rgba(250,246,232,0.55); }
#reader.lw-edition .bm-cover-inner::after { border-color: rgba(250,246,232,0.28); }
#reader.lw-edition .bm-back-cover-inner { border-color: rgba(250,246,232,0.40); }
#reader.lw-edition .cover-eyebrow { color: rgba(250,246,232,0.92); }
#reader.lw-edition .lw-sprout {
  display: inline-block;
  font-size: 32px;
  line-height: 1;
}
#reader.lw-edition .cover-title {
  color: #faf6e8;
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.08);
}
#reader.lw-edition .cover-question {
  color: rgba(250, 246, 232, 0.88);
}
#reader.lw-edition .cover-imprint { color: rgba(250, 246, 232, 0.85); }
#reader.lw-edition .back-mark .sparkle { color: rgba(250, 246, 232, 0.85); }
#reader.lw-edition .back-mark .back-mark-text {
  color: rgba(250, 246, 232, 0.85);
}

/* --- title page: warm paper stays, type goes Fredoka/sage ---------------- */
#reader.lw-edition .title-main {
  color: #4d7c5c;
}
#reader.lw-edition .bm-title-page .lw-sprout { font-size: 26px; }

/* --- endpapers + finis: keep the book cozy, not formal ------------------- */
#reader.lw-edition .bm-endpaper {
  background: #487a55;   /* #629: flat, no banding */
}
#reader.lw-edition .bm-end .finis {
  color: #4d7c5c;
}
/* Reader 2026: no drop cap. The approved spread sets prose as one even
   block on a 38ch measure — a floated 44px initial pulls the eye to the
   letter instead of the sentence and breaks the first three lines against
   the narrower measure. Same neutralisation the LW edition already uses;
   the letter stays ordinary prose text, so nothing is removed. */
.bm-prose-page .prose p:first-child::first-letter {
  font-family: inherit;
  font-weight: inherit;
  font-size: inherit;
  line-height: inherit;
  float: none;
  padding: 0;
  color: inherit;
}
.bm-prose-page .bubble {
  font-size: 13.5px;
  padding: 8px 14px;
  margin: 12px auto;
}

/* Folio + running title. In flex-layout context (image-page, prose-page)
   this sits at the bottom of the flex column. Image-page reverses the
   inline order so the folio sits on the outer (left) edge of the left
   page; prose-page keeps natural order so its folio sits on the outer
   (right) edge of the right page. */
.bm-image-page .page-foot,
.bm-prose-page .page-foot {
  flex: 0 0 auto;
  margin-top: 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-family: var(--r26-display);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  /* .78 opacity here was 3.98:1 — also under the floor. */
  color: var(--r26-wood-light);
}
.bm-image-page .page-foot { flex-direction: row-reverse; }
.bm-page .page-foot .page-foot-title {
  max-width: 60%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* === FINIS / colophon page (LEFT page of the closing spread) ============ */
.bm-end {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 60px 50px;
}
.bm-end .finis {
  font-family: var(--serif);
  font-style: italic;
  font-size: 30px;
  letter-spacing: 0.18em;
  color: var(--ink-soft);
  margin: 0 0 24px;
}
.bm-end .ornament {
  margin-bottom: 32px;
  user-select: none;
}
/* Finis-page recolor: dots match warm-paper ink, logo sized to match
   the ornament-row height. */
.bm-end .ornament-row .dot { background: var(--ink-faint); }
.bm-end .ornament-row .ornament-logo {
  width: 26px;
  height: 26px;
  opacity: 0.9;
}
.bm-end .colophon {
  font-family: var(--serif);
  font-style: italic;
  font-size: 13px;
  line-height: 1.7;
  /* --ink-faint is ~3.5:1 on this page and under 4.5:1 even on white. */
  color: var(--r26-wood-light);
  max-width: 36ch;
  margin: 0;
}

/* Scroll-mode .illustration-container styles further down still target
   scroll mode only; book-mode versions are scoped under .bm-image-page
   above so they don't conflict. */


.book-footer {
  margin-top: 24px;
  padding: 18px 8px 0;
  border-top: 1px solid var(--border);
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.55;
  color: var(--text-tertiary);
  text-align: center;
}

/* === Inline emphasis effects ============================================= */
/* Locked vocabulary (Rikku 2026-05-05): pop, marker, handnote, shout, stamp,
   rainbow. Static-first so PDF capture works; entry animations resolve TO the
   static state, never away from it.                                          */

.fx { display: inline-block; }

/* Nested markers (`~~But what **is** it?~~`) render as an fx span INSIDE an fx
   span. Every fx-* class carries its own rotate() and em font-size, and CSS
   COMPOUNDS both down a nesting chain: the inner word would come out
   double-tilted (-2deg + -1.5deg) and oversized (1.22em x 1.18em), and in the
   LW edition it would run lw-popwiggle inside an already-rotated parent.
   The inner span keeps its PAINT — colour, weight, highlighter gradient, which
   is the whole point of the nesting — and yields all GEOMETRY and motion to the
   outer one. !important because the LW overrides (#reader.lw-edition .bm-page
   .prose .fx-pop) outrank a plain `.fx .fx` selector.
   Nesting only became reachable when the renderer stopped dropping nested
   markers (2026-07-12); before that this shape could not occur. */
.fx .fx {
  transform: none !important;
  animation: none !important;
  font-size: 1em;
  margin: 0;
}

.fx-pop {
  /* Reader 2026: brand green, not the per-book theme accent — the accent is
     tuned to the scene art and swung this voice across the palette book to
     book, which read as inconsistent rather than emphatic. */
  color: var(--r26-leaf);
  font-weight: 600;
  font-style: italic;
  transform: rotate(-1.5deg);
  text-shadow: 0 1px 0 rgba(0,0,0,0.04);
  padding: 0 1px;
  animation: fx-pop-in 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
}

/* Yellow-highlighter underline — restored 2026-05-19. #94 had pulled
   the background gradient because long phrases couldn't wrap (one
   inline-block span = one unbreakable layout unit, so a long highlight
   either fit on one line or left a giant gap). With the per-word
   fx-span splitting from #120, each highlighted word is now its OWN
   inline-block atom; the background paints per word, the inter-word
   whitespace stays unhighlighted, and the line wraps naturally at
   those spaces. The phrase reads as a row of highlighter strokes —
   one per word — which is the "marker" feel without the broken wrap.
   Text color stays default ink so the yellow reads as overlay, not
   recoloring. */
.fx-marker {
  background-image: linear-gradient(180deg,
    transparent 0%, transparent 58%,
    rgba(254, 240, 138, 0.85) 58%,
    rgba(254, 224, 110, 0.78) 92%,
    transparent 96%);
  padding: 0 2px;
  border-radius: 1px;
  font-weight: 500;
  animation: fx-marker-draw 0.45s ease-out backwards;
}

.fx-handnote {
  /* Latin: Caveat / Korean: Nanum Pen Script / JP: Yomogi / Simplified CN: Liu Jian Mao Cao
     Browser falls through chain per glyph — single class, multilingual handwritten feel. */
  font-family: 'Caveat', 'Nanum Pen Script', 'Yomogi', 'Liu Jian Mao Cao', var(--serif);
  font-weight: 600;
  font-size: 1.18em;
  line-height: 1;
  color: #555;
  margin: 0 1px;
  animation: fx-handnote-in 0.5s ease-out backwards;
}

.fx-shout {
  /* Reader 2026 swaps only the LATIN head of the chain (Bangers -> Caveat):
     the approved look letters this voice by hand. The KR/JP/SC members are
     left exactly as they were, so every non-Latin book keeps the display
     face it already had instead of falling back to tofu — the same reason
     --serif is never rebound. */
  font-family: 'Caveat', 'Black Han Sans', 'Hachi Maru Pop', 'ZCOOL KuaiLe', var(--sans);
  font-weight: 700;
  font-size: 1.2em;
  letter-spacing: 0.01em;
  color: var(--r26-terra);
  transform: rotate(-2deg);
  margin: 0 2px;
  animation: fx-shout-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
}

.fx-stamp {
  /* Latin head swapped to Caveat (see .fx-shout); KR/JP/SC members kept. */
  font-family: 'Caveat', 'Bagel Fat One', 'RocknRoll One', 'Ma Shan Zheng', var(--sans);
  font-weight: 600;
  font-size: 1.12em;
  color: var(--r26-wood);
  transform: rotate(-2.5deg);
  margin: 0 2px;
  animation: fx-stamp-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
}

/* Founder iteration 2026-05-19: fx-rainbow (per-word gradient color)
   was visually dizzying on multi-word phrases like "red, orange,
   yellow, purple" because the per-word fx-span split from #120 made
   every word repeat the full gradient. Neutralized to plain
   inheritable text — keeps the emphasis weight without the
   confusing color cycling. The animation is also disabled. */
.fx-rainbow {
  color: inherit;
  background-image: none;
  -webkit-background-clip: initial;
  background-clip: initial;
  -webkit-text-fill-color: inherit;
  font-weight: 600;
  animation: none;
}

@keyframes fx-pop-in {
  0%   { transform: scale(0.6) rotate(-12deg); opacity: 0; }
  60%  { transform: scale(1.12) rotate(2deg); opacity: 1; }
  100% { transform: scale(1) rotate(-1.5deg); opacity: 1; }
}
@keyframes fx-marker-draw {
  /* Gentle opacity fade-in per word. The highlighter background is back
     (2026-05-19, paired with per-word fx-span splitting from #120) but
     the entry stays a soft fade rather than a sweep so multi-word
     highlights don't feel busy — each word's yellow band fades in at
     the same beat as the rest of the prose. */
  from { opacity: 0.4; }
  to   { opacity: 1; }
}
@keyframes fx-handnote-in {
  0%   { opacity: 0; transform: translateY(-3px) rotate(-3deg); }
  100% { opacity: 1; transform: translateY(0) rotate(0); }
}
@keyframes fx-shout-in {
  0%   { transform: scale(0.5) rotate(8deg); opacity: 0; }
  60%  { transform: scale(1.18) rotate(-4deg); opacity: 1; }
  100% { transform: scale(1) rotate(-2deg); opacity: 1; }
}
@keyframes fx-stamp-in {
  0%   { transform: scale(2) rotate(-15deg); opacity: 0; }
  70%  { transform: scale(0.92) rotate(-1deg); opacity: 1; }
  100% { transform: scale(1) rotate(-2.5deg); opacity: 1; }
}
@keyframes fx-rainbow-in {
  0%   { background-position: 100% 0; opacity: 0; }
  100% { background-position: 0 0; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .fx, .fx-pop, .fx-marker, .fx-handnote, .fx-shout, .fx-stamp, .fx-rainbow {
    animation: none !important;
  }
}

/* Inside dialogue bubbles, accent colors should sit against the bubble bg
   without fighting it. The bubble already provides contrast — let pop/shout
   inherit the bubble's text color rather than the scene accent.            */
.bubble .fx-pop, .bubble .fx-shout { color: inherit; }

/* WS-A (#129/#149): neutral "Preparing next page…" affordance shown only
   while a page-turn decode gate is in flight (rare — scene images are
   eager + small WebP). Centered, soft, non-alarming. Never implies the
   book is broken. */
.page-turn-pending {
  position: absolute;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 40;
  padding: 8px 16px;
  border-radius: 999px;
  background: rgba(40, 28, 12, 0.82);
  color: #fff;
  font-family: "Fredoka", var(--sans);   /* #629: fall through to the field's stack */
  font-size: 13px;
  letter-spacing: 0.02em;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
  pointer-events: none;
  animation: page-turn-pending-pulse 1.3s ease-in-out infinite;
}
.page-turn-pending[hidden] { display: none; }
@keyframes page-turn-pending-pulse {
  0%, 100% { opacity: 0.7; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .page-turn-pending { animation: none; opacity: 0.9; }
}

/* Shared-link "home" ✕ — a CORNER-docked cream chip (#448, founder-approved
   MINIMAL version). It used to sit dead-CENTER at the top and the book text
   scrolled UNDER it, eating a word mid-sentence (every tester hit it). It now
   tucks into the TOP-RIGHT corner, clear of the centred reading column, and is
   ALWAYS visible — a top-right ✕ is the familiar "close" convention and makes it
   obvious how to leave the book. Only shown on a directly-opened shared reader
   (window.top === self, wired in reader.js); in-app the shell's own ✕ is used.
   Cream-paper chip treatment matches the book-mode .view-toggle look. */
.shared-home-bar {
  position: fixed;
  top: calc(12px + env(safe-area-inset-top, 0px));   /* #363 safe-area parity (native direct-open) */
  right: calc(12px + env(safe-area-inset-right, 0px));
  z-index: 200;
}
/* On /read/<id>/ pages the recipient report ⚑ owns the top-right corner
   (.wl-report-btn), so the ✕ docks TOP-LEFT instead (amendment D1). Where the
   .view-toggle is visible (desktop + phone-scroll) the ✕ sits in that same left
   rail directly below the toggle (8px gap); where the toggle is hidden (phone
   book-mode + preview, handled below) the ✕ rises into the toggle's own slot. */
.shared-home-bar.is-read {
  right: auto;
  left: calc(14px + env(safe-area-inset-left, 0px));
  top: calc(51px + env(safe-area-inset-top, 0px));   /* view-toggle bottom (~43px) + 8px gap */
}
@media (max-width: 760px), (max-height: 500px), (orientation: portrait) {
  /* phone book-mode hides .view-toggle (reader.css force-scroll query) → the
     /read/ ✕ takes the toggle's own top-left slot. Mirror that query exactly. */
  #reader.mode-book .shared-home-bar.is-read {
    top: calc(14px + env(safe-area-inset-top, 0px));
  }
}
#reader[data-preview="1"] .shared-home-bar.is-read {
  top: calc(14px + env(safe-area-inset-top, 0px));   /* preview hides .view-toggle too */
}
.shared-home-x {
  width: 44px;
  height: 44px;
  border-radius: 999px;
  border: 1px solid rgba(80, 55, 20, 0.25);
  background: rgba(245, 236, 209, 0.92);             /* product cream paper tone */
  color: #3b2c14;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
}
.shared-home-x:hover { color: var(--text-primary); }

/* #427 recipient "Report this book": fixed top-right circle button (mirrors the
   top-left view toggle; matches the shared-home ✕) + a centered overlay form.
   Fixed / overlay so it never enters the book content flow — book-view and
   scroll-view layouts are untouched. Mounted by reader.js on /read/ pages only. */
.wl-report-btn {
  position: fixed;
  top: calc(12px + env(safe-area-inset-top, 0px));
  right: calc(12px + env(safe-area-inset-right, 0px));
  width: 36px;
  height: 36px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.92);
  color: var(--text-secondary);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
}
.wl-report-btn:hover { color: var(--text-primary); }
.wl-report-back {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(20, 16, 8, 0.30);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
.wl-report-back[hidden] { display: none !important; }
.wl-report-panel {
  width: 100%;
  max-width: 320px;
  box-sizing: border-box;
  text-align: left;
  background: #fbf9f4;
  color: #5f594e;
  border: 1px solid #e2dccf;
  border-radius: 14px;
  padding: 16px 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.28);
  font-family: inherit;
}
.wl-report-panel .wl-report-h { margin: 0 0 0.6rem; font-weight: 600; font-size: 0.95rem; }
.wl-report-panel label { display: block; margin: 0.3rem 0; font-size: 0.85rem; cursor: pointer; }
.wl-report-panel label input { margin-right: 0.4rem; }
.wl-report-note {
  width: 100%;
  box-sizing: border-box;
  margin-top: 0.55rem;
  border: 1px solid #e2dccf;
  border-radius: 8px;
  padding: 0.5rem;
  font-family: inherit;
  font-size: 0.85rem;
  resize: vertical;
}
.wl-report-actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.7rem; }
/* Author display can out-specify the UA [hidden] rule (#161 cascade trap) —
   force-hide the form body + Send button when the success state collapses. */
.wl-report-form[hidden], .wl-report-send[hidden] { display: none !important; }
.wl-report-cancel {
  border: none; background: none; cursor: pointer;
  color: #8a8377; font-family: inherit; font-size: 0.85rem; padding: 0.45rem 0.6rem;
}
.wl-report-send {
  border: none; border-radius: 999px; cursor: pointer;
  background: #5f594e; color: #fff; font-family: inherit; font-size: 0.85rem;
  padding: 0.45rem 1.1rem;
}
.wl-report-done { margin: 0.7rem 0 0; font-size: 0.85rem; color: #5f594e; }

/* ── Font-stall recovery card (2026-07-14 zero-defect book view) ─────────
   Direct-open only: when the explicit font gate fails twice (load error /
   timeout, then one automatic reload also failed), the veil swaps its 📖
   for this controlled card instead of ever revealing fallback typefaces
   (founder: HOLD THE VEIL). System fonts on purpose — the webfonts are
   exactly what failed. Centered inside the full-bleed overlay, so nothing
   can occlude it (#161). */
.book-loading-overlay .book-loading-recovery {
  text-align: center;
  font-family: var(--sans);   /* #629: the field's stack */
  color: rgba(245, 236, 209, 0.92);
  padding: 24px;
}
.book-loading-overlay .book-loading-recovery p {
  font-size: 17px;
  margin: 0 0 18px;
}
.book-loading-overlay .book-loading-recovery button {
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  color: #14131c;
  background: rgba(245, 236, 209, 0.92);
  border: 0;
  border-radius: 999px;
  padding: 10px 26px;
  cursor: pointer;
}
.book-loading-overlay .book-loading-recovery button:hover {
  background: #f5ecd1;
}

/* ── Composited-layer windowing (2026-07-16 GPU-stall fix) ──
   ROOT CAUSE proven: not JS, not any visual effect (stripping backdrop-filter +
   drop-shadow + all mix-blend left the stall at an IDENTICAL ~15.1s — a fixed
   GPU-process crash+relaunch time). The cause is the raw 3D flipbook: EVERY
   spread is a .bm-sheet with two backface-visibility:hidden 3D faces, so a
   10-spread book keeps ~20 composited 3D layers live and rebuilds them all on
   every open — which crashes WebKit's GPU process on the iPad.
   FIX: only paint the sheets within ±2 of the current spread. Every other sheet
   is fully occluded by an opaque in-window paper face (the depth illusion is the
   separate .page-stack element, not the hidden sheets), so `visibility:hidden`
   on them changes NO layout / stacking / transform / backface / 3D-context — it
   only tells the GPU "don't rasterize this occluded sheet." reader.js toggles
   .bm-cold by proximity to `current` on every state change. Applies to ALL
   users (iPad Safari shares the same GPU class as the app; the founder verified
   the app on-device and the full look is unchanged — occluded sheets only).
   No transition on visibility (instant, no dead-zone); no !important needed. */
.bm-sheet.bm-cold {
  visibility: hidden;
}

/* There is deliberately NO painted "case" backdrop behind the sheets (an
   earlier fix drew the cloth on .flipbook::before and faded it in on a 0.44s
   delay to mask a turning cover that vanished mid-air). The vanish itself is
   fixed at the source now — the .bm-face-back / back-board translateZ
   separations above — so the board in the air paints
   continuously and a backdrop would only reintroduce the founder's "cover
   appears on the left while the cover is still turning" (the fade landing
   while the real board was still mid-flight), plus a phantom cloth slab
   beside the closed back board at book-end. */

/* ...except the two boards. The rule above is sound only while every sheet
   is full-size and therefore fully occludes the ones behind it. Under the
   Reader 2026 anatomy the interior sheets are INSET, so the board margin
   around a landed page is painted by the cover sheet itself — window the
   back board out and the right-hand page loses its cloth reveal entirely.
   Cost is bounded: two extra always-live layers, not the whole stack, which
   is what the windowing exists to prevent. */
.bm-sheet:first-child.bm-cold,
.bm-sheet:last-child.bm-cold,
.bm-sheet:has(.bm-cover).bm-cold,
.bm-sheet:has(.bm-back-cover).bm-cold {
  visibility: visible;
}
/* The coplanarity risk this exemption creates (two always-live full-size
   sheets in the z=0 plane of every flipping sheet — the source of the old
   end-of-book jitter) is defused above: the back board rests at
   translateZ(-2px) and every sheet's two faces are separated in z, so no
   landing sheet ever ties a plane with a board. */


/* #507: a reader with JavaScript disabled (or blocked, or a script that
   404s) never gets `#reader.book-loaded`, so the loading veil stays up forever
   and the whole book is unreachable behind it. The veil is declared in
   _reader_critical.css, which is INLINED into every already-published book —
   editing it there would only help books rendered from now on. This override
   lives in reader.css, which is linked and mtime-versioned, so it reaches all
   ~2,340 baked books with no re-render.
   `scripting: none` is the media feature for exactly this and needs no JS to
   evaluate. Engines without it are unaffected: they keep today's behaviour,
   which is what JS-enabled readers already see. */
@media (scripting: none) {
  .book-loading-overlay { display: none !important; }
}
/* ==== Field-reader glue (not from reader.css) ==== */
#reader.mode-book{overflow:hidden}
#reader.mode-book .rd-head,#reader.mode-book .rd-foot{display:none}
#reader.mode-book #reader-scenes{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
/* #629: the book's shadow belongs to the BOARDS (see `.bm-sheet:first-child` /
   `:last-child` face rules): any slab under the book shows its own outline
   while the cover swings and the book slides (founder, 2026-09-26). */
#reader .rd-view{position:fixed;top:calc(var(--safe-top,0px) + 12px);left:calc(var(--safe-left,0px) + 14px);z-index:60;
  font:500 12.5px/1 var(--sans,system-ui,sans-serif);color:#5a3825;background:#fffdf3;border:0;border-radius:999px;padding:9px 13px;cursor:pointer;
  box-shadow:0 6px 16px rgba(45,24,16,.16),inset 0 0 0 1px rgba(90,56,37,.22)}
#reader .rd-view[hidden]{display:none}
#reader.mode-book .dot{z-index:60}
#reader.mode-book .view-toggle{display:none !important}
