:root {
  --ink: #0d2f43;
  --ink-soft: #395363;
  --blue: #0e3d56;
  --blue-light: #d9eced;
  --cream: #f8f0df;
  --paper: #fffdf7;
  --gold: #e9a92b;
  --gold-light: #f7d97d;
  --coral: #eb755b;
  --coral-dark: #d75b45;
  --green: #456f5c;
  /* The pressed/hover half of the green. It arrived on 2026-08-12 as the bare `#2f5244`
     plinth under the drawer's `Create a book` ("Country Selector Mobile.dc.html") and is a
     token now because every filled CTA on the site wears the pair — see the note over
     `.button-coral`. The five stand-alone page templates inline their own scales and each
     needs BOTH halves declared in its own `:root`; a `var()` that resolves to nothing
     computes the background to transparent, which is a button that vanishes on hover. */
  --green-dark: #2f5244;
  --border: #d9d4c7;
  --shadow: 0 22px 55px rgba(10, 42, 59, 0.12);
  --display: Georgia, "Times New Roman", "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", serif;
  --sans: "Avenir Next", Avenir, "Segoe UI", Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;

  /* ── Section rhythm ("Section Rhythm.dc.html", banded mode) ──────────────
     The homepage's sections used to separate themselves with AIR — 90-120px of
     the same cream above and below every heading — which is the design's own
     "loose" mode and the thing it argues against. They separate themselves with
     an EDGE now: each band is its own colour, the seam between two of them is a
     device (a torn wave, a badge straddling the join, the navy band's crest and
     the dip it spills out on), and the padding comes
     down to one shared figure because the band edge is doing the work the air
     used to do. Five things are load-bearing.

     (1) `--band-pad` is ONE number for six sections. The rhythm is the point:
         a section that keeps its own padding reads as a page that changed its
         mind halfway down. Per-section padding survives only where the section
         has to clear something of its own (the straddling badge above
         `.examples-section`, the dip that spills into `.pricing-section`).
     (2) The navy band CRESTS IN and DIPS OUT, and it has no border radius.
         `#forever-gift` used to be arched at the top with `.pricing-section`
         arched below it, which the design calls "an odd floating slab" and
         replaced — do not reintroduce rounded corners on either. It rises into
         `.examples-section` on `.fg-seam` (navy, drawn above its own top edge,
         with the two gold twinkles in the gap it opens) and spills down into
         `.pricing-section` on the mirrored `.pricing-seam`. Both waves are
         navy, so neither section pays an arch debt any more: the extra bottom
         padding and the negative margin that put the right colour in an arch's
         corners are gone from `.examples-section` and `.fg-section` alike.
     (3) A seam device hangs OUTSIDE its section's padding box, so a section
         carrying one may not be `overflow: hidden` — `.how-section` (the wave
         above it), `.examples-section` (the badge above it) and `.fg-section`
         (its crest, and the twinkles above that) are `overflow-x: clip`, which
         clips the axis they need clipped and leaves the other visible. The same
         fix `.pricing-section` and `.faq-section` already carry for the sticky
         intro, for the same reason.
     (4) A CREST's fill is the RECEIVING band's own colour, read from the same
         custom property the section's background is — the seam and the band it
         belongs to cannot drift apart if only one value exists. A crest also
         OVERLAPS its own band by a pixel (`top: 1px` under a full `-100%`
         translate), the `-1px` the hero's own strip used to carry and for
         the same reason: a seam parked exactly on the edge leaves a hairline of
         the wrong colour at some zoom and DPR combinations, and the neighbours
         here are navy-to-sand and cream-to-coral, where a hairline shows.
         A DIP is the other direction and therefore the other fill: it sits
         INSIDE the receiving section and is painted in the band ABOVE it, so
         `.pricing-seam` is `--band-gift` on a sand band. Filled from its own
         section's token it would be sand on sand and paint nothing at all —
         which is what "restoring consistency" here silently deletes. It
         overlaps upwards (`top: -1px`) and the overlap is free, navy on navy.
     (5) `--ft-wave` on the footer has to match whatever lands above it, and the
         coral `.final-cta` is what lands there now. Change the last band before
         the footer and change that token, or a seam runs the width of the page. */
  --band-pad: 74px;
  --band-rule: #dcc99f;
  --band-how: #f4e7cb;
  --band-examples: #fffdf7;
  --band-gift: #0e3d56;
  --band-pricing: #f8edd6;
  --band-faq: #fffdf7;
  --band-final: #eb755b;
}

* { box-sizing: border-box; }
/* The header is sticky (top: 12px), so an anchor jump — #memberships, #pricing,
   #creator, and every /#... link on /shelf — parks the target under it. This is
   the one place that offset lives: it covers the in-page nav, the footer links
   and a fragment arriving on page load, which no per-link JS offset can. */
html { scroll-behavior: smooth; scroll-padding-top: 120px; }
body { margin: 0; overflow-x: hidden; color: var(--ink); background: var(--paper); font-family: var(--sans); -webkit-font-smoothing: antialiased; }
body.modal-open { overflow: hidden; }
button, input, select, textarea { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
a { color: inherit; }
img { display: block; max-width: 100%; }
.section-shell { width: min(1180px, calc(100% - 48px)); margin-inline: auto; }

.site-header { display: flex; align-items: center; justify-content: space-between; gap: 22px; width: min(1280px, calc(100% - 48px)); margin: 22px auto 0; padding: 12px 16px 12px 22px; background: rgba(255, 253, 247, .82); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid #efe4cd; border-radius: 22px; box-shadow: 0 14px 40px -20px rgba(10, 42, 59, .28); position: sticky; top: 12px; z-index: 100; }
.brand { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; color: var(--ink); }
/* Three leaning book spines + a sparkle — the bookshelf.kids mark (matches /shelf). */
.brand-mark { position: relative; display: inline-block; width: 44px; height: 44px; flex: none; }
.brand-mark i { position: absolute; bottom: 4px; width: 10px; border-radius: 3px; transform-origin: bottom; }
.brand-mark i:nth-child(1) { left: 2px; height: 26px; background: var(--coral); transform: rotate(-8deg); }
.brand-mark i:nth-child(2) { left: 15px; height: 34px; background: var(--blue); }
.brand-mark i:nth-child(3) { left: 28px; height: 22px; background: var(--gold); transform: rotate(8deg); }
.brand-mark b { position: absolute; right: 0; top: 0; font-size: 13px; color: var(--gold); line-height: 1; font-weight: 400; }
.brand-word { font-family: var(--display); font-size: 26px; color: inherit; letter-spacing: -0.3px; line-height: 1; font-weight: 700; }
.brand-word em { font-style: normal; color: var(--coral); }
/* the spine mark on the dark login-modal panel: brighter blue spine, larger */
.account-brand-mark { transform: scale(1.35); transform-origin: left bottom; margin-bottom: 6px; }
.account-brand-mark i:nth-child(2) { background: #7fb2d6; }

/* ── The deep-link boot cover ──────────────────────────────────────────────
   Held over the homepage from before first paint until the reader is open, for
   /?book= and /?shared= (see the head script in index.html for why). It is an
   OPAQUE FIXED COVER and deliberately not `visibility: hidden` on an ancestor:
   #storyDialog is opened with showModal() and paints from the top layer, but
   inheritance still follows the DOM tree, so hiding <body> would hide the book
   as well and the reader would open invisible. A cover needs no such care — the
   top-layer dialog paints above it whatever its z-index — which is what makes
   the teardown cosmetic rather than load-bearing. It says what is happening in
   words, and with the brand mark: a bare spinner on a blank screen is the
   "reads as broken" failure #narrationBake exists to answer, and this is the
   screen a family lands on straight after paying. */
.book-boot { display: none; }
html.booting-reader { overflow: hidden; }
html.booting-reader .book-boot { position: fixed; inset: 0; z-index: 9000; display: grid; place-content: center; justify-items: center; background: var(--cream); }
/* `loadPricing`, `checkAIStatus` and `ensureCountryOptions` all run inside the cover's
   window, so a blip in any of them toasts into a covered viewport. The bump is scoped to
   the window rather than raising `.toast`'s own z-index, which would also put it over the
   studio overlay and the gen overlay for every other toast on the site. */
html.booting-reader .toast { z-index: 9001; }
.book-boot-mark { transform: scale(1.7); transform-origin: center bottom; animation: boot-bob 1.5s ease-in-out infinite; }
.book-boot-line { margin: 34px 0 0; font-family: var(--display); font-size: 21px; color: var(--ink); }
@keyframes boot-bob {
  0%, 100% { transform: scale(1.7) translateY(0); }
  50% { transform: scale(1.7) translateY(-5px); }
}
@media (prefers-reduced-motion: reduce) { .book-boot-mark { animation: none; } }
/* Playful nav pills: lift + tilt on hover, each with its own colour swash underline. */
/* Above the drawer breakpoint this wrapper is not a box at all, so `.desktop-nav` and
   `.header-account` remain direct flex items of `.site-header` and the desktop bar is
   exactly what it always was. Below 1100 it becomes the drop panel. */
.header-tools { display: contents; }
/* The hamburger exists only where the drawer does. */
.nav-toggle { display: none; }
.desktop-nav { display: flex; align-items: center; justify-content: center; gap: 2px; flex: 1; }
.desktop-nav a { position: relative; text-decoration: none; font-size: 15px; font-weight: 700; color: #6a5f49; padding: 8px 14px 12px; white-space: nowrap; transition: transform .18s cubic-bezier(.34, 1.56, .64, 1), color .15s; }
.desktop-nav a::after { content: ""; position: absolute; left: 8px; right: 8px; bottom: 2px; height: 5px; border-radius: 6px; opacity: 0; transform: rotate(-1.2deg); transition: opacity .18s; pointer-events: none; }
.desktop-nav a:hover { transform: translateY(-3px) rotate(-2deg); color: var(--ink); }
.desktop-nav a:hover::after { opacity: .35; }
.desktop-nav a:nth-child(1)::after { background: var(--coral); }
.desktop-nav a:nth-child(2)::after { background: var(--gold); }
.desktop-nav a:nth-child(3)::after { background: #456f5c; }
.desktop-nav a:nth-child(4)::after { background: var(--blue); }
.desktop-nav a:nth-child(5)::after { background: #c98bb0; }
.button { min-height: 53px; display: inline-flex; align-items: center; justify-content: center; gap: 28px; padding: 0 24px; border: 0; border-radius: 7px 12px 8px 11px; text-decoration: none; font-size: 14px; font-weight: 800; cursor: pointer; transition: transform .2s, box-shadow .2s, background .2s; }
.button:hover { transform: translateY(-2px); }
.button span { font-size: 20px; font-weight: 400; }
.button-small { min-height: 43px; padding: 0 18px; gap: 14px; font-size: 12px; }
.button-ink { color: white; background: var(--ink); box-shadow: 0 8px 20px rgba(13, 47, 67, .16); }
.button-primary { color: white; background: var(--blue); box-shadow: 0 10px 23px rgba(10, 58, 81, .18); }
.button-primary:hover, .button-ink:hover { background: #164e69; box-shadow: 0 14px 28px rgba(10, 58, 81, .23); }
/* EVERY FILLED CALL TO ACTION ON THIS SITE IS GREEN (2026-08-17, at the owner's request).
   It was coral — this sheet's `--coral` painted the hero, the register pill, the studio's
   Next, the handoff, the person modal, the sheet-approval Keep, the membership CTA and the
   shared `.button-coral`, and the root CLAUDE.md said in as many words that "coral is the
   buy colour". It is not any more: `Create a book` in the mobile drawer went green on
   2026-08-12 with "Country Selector Mobile.dc.html", and one CTA colour on one surface and
   another everywhere else is the "one button, two colours" failure this repo argues against
   twice. Three things are load-bearing. (1) **The class keeps its name.** It is written at
   23 sites across five files plus a `classList.toggle("button-coral", …)` in app.js, and
   `.button` alone carries NO background — so a rename that half-applied would leave a
   button with no paint at all, and in the toggle's case in one STATE only. The repo's own
   idiom is a stale name with the truth written beside it (`users.approved`, the
   illustration response's `"source": "gemini"`, `plan_book_limit_reached`). (2) **Coral is
   still the site's accent** — eyebrows, badges, ribbons, the current step's pip, the
   `.brand-mark` spine, focus tints — and none of that moved. Only FILLED BUTTONS did.
   (3) **Every shadow moved with the background.** A coral-tinted glow under a green button
   reads as a mis-render, not as a tint, and there are FOURTEEN of them in this sheet: this
   rule, `.hero-cta`, `.hm-cta`, `#openRegister`, `.auth-form .button`, `.sub-cta`,
   `.gen-keep`'s two, `.sb-cta`, `.kc-next`, `.ho-cta`, `.kc-pm-cta` — plus the two inside
   phone-only media blocks, `.header-tools .header-account #openRegister` and `.kc-next`'s
   640 rule, which a desktop check cannot see. Twelve more are inlined in the five
   stand-alone page templates (`/checkout`'s `.pay` is a third phone-only one). */
.button-coral { color: white; background: var(--green); box-shadow: 0 10px 23px rgba(69, 111, 92, .26); }
.button-coral:hover { background: var(--green-dark); }

/* --- Hero ("Hero Playful" design) ---------------------------------------
   One full-bleed section: the two-column shell over the dark promises band.
   The band's wave is filled with THIS gradient's bottom colour (#fdf8ec) —
   hardcoded in the markup — so changing the hero background means changing
   the wave's fill with it, or a seam appears across the whole page.
   The keyframes are hero-prefixed: the sheet already carries several
   twinkle/bob animations and a duplicate @keyframes name is last-wins with
   no warning. */
.hero-section { position: relative; overflow: hidden; background: radial-gradient(120% 90% at 78% 12%, #fdf1d6 0%, #fdf8ec 55%); }
.hero-spark { position: absolute; z-index: 0; color: rgba(233, 169, 43, .5); pointer-events: none; animation: hero-twinkle 5.4s ease-in-out infinite; }
.hs1 { top: 26px; left: 16%; font-size: 20px; }
.hs2 { top: 250px; left: 1.5%; font-size: 13px; color: rgba(235, 117, 91, .45); animation-duration: 4.6s; animation-delay: 1.1s; }
.hs3 { bottom: 150px; right: 4%; font-size: 16px; color: rgba(233, 169, 43, .45); animation-duration: 6.2s; animation-delay: .5s; }
@keyframes hero-twinkle { 0%, 100% { opacity: .28; transform: scale(.82) rotate(0deg); } 50% { opacity: 1; transform: scale(1.14) rotate(12deg); } }
@keyframes hero-float-a { 0%, 100% { transform: rotate(-9deg) translateY(0); } 50% { transform: rotate(-9deg) translateY(-13px); } }
@keyframes hero-float-b { 0%, 100% { transform: rotate(2deg) translateY(0); } 50% { transform: rotate(2deg) translateY(-20px); } }
@keyframes hero-float-c { 0%, 100% { transform: rotate(10deg) translateY(0); } 50% { transform: rotate(10deg) translateY(-9px); } }
@keyframes hero-bob { 0%, 100% { transform: translateY(0) rotate(-2deg); } 50% { transform: translateY(-8px) rotate(2deg); } }

/* The bottom padding is the ONLY lever on the gap between the hero and the promises strip
   below it: `.hero-promises` opens on `padding-top: 0` and the strip's own `margin-top` is
   its rhythm against the cards. It is deliberately not `.hero-visual`'s `min-height` or
   `.hero-covers`' scaled `top` — those two are a derived pair (`392px x scale + ~10px`) and
   moving either is where the 65-95px of dead air came from. Tightened 2026-08-06. */
.hero { position: relative; z-index: 1; min-height: 640px; display: grid; grid-template-columns: .87fr 1.13fr; align-items: center; gap: 52px; padding: 46px 0 16px; }
.hero-copy { padding: 18px 0 0 8px; position: relative; z-index: 2; }
/* .kicker is shared with every other section's eyebrow — the hero's pill is
   .hero-eyebrow alone, or every kicker on the site becomes a pill. */
.eyebrow, .kicker { color: var(--coral); text-transform: uppercase; letter-spacing: 2.7px; font-size: 11px; font-weight: 900; }
.eyebrow { display: inline-flex; align-items: center; gap: 8px; }
.eyebrow-star { color: var(--gold); font-size: 15px; }
.hero-eyebrow { gap: 9px; padding: 8px 16px 8px 12px; border: 1.5px solid #f1dca6; border-radius: 999px; background: #fff3d6; color: #b4761f; letter-spacing: 2.2px; font-size: 12px; font-weight: 800; }
.hero-eyebrow .eyebrow-star { width: 22px; height: 22px; display: grid; place-items: center; border-radius: 50%; background: var(--gold-light); color: #7d5a0e; font-size: 11px; }
.hero h1 { margin: 26px 0 0; font-family: var(--display); font-size: clamp(46px, 4.9vw, 70px); line-height: 1.02; font-weight: 400; letter-spacing: -1.5px; }
.hero h1 em { position: relative; display: inline-block; color: var(--coral); font-style: italic; font-weight: 400; }
.hero h1 em::after { content: ""; position: absolute; left: 2px; right: 0; bottom: -6px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 16' preserveAspectRatio='none'%3E%3Cpath d='M2,11 C60,3 120,15 176,7 C222,1 264,9 297,4' fill='none' stroke='%23f0b842' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E") center/100% 100% no-repeat; }
.hero-lede { max-width: 34em; margin: 30px 0 0; color: #5b6f7c; font-size: 19px; line-height: 1.65; text-wrap: pretty; }
.hero-actions { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-top: 34px; }
/* The two CTAs are hero-only classes rather than the shared `.button`, which
   the creator, the dialogs and the order flow all wear. (`.text-button` and
   `.play-icon` went with the old hero — nothing else on the site used them.) */
.hero-cta { display: inline-flex; align-items: center; gap: 12px; padding: 17px 30px; border: 0; border-radius: 999px; color: white; background: var(--green); font-size: 17px; font-weight: 800; text-decoration: none; box-shadow: 0 18px 34px -16px rgba(47, 82, 68, .95); transition: transform .2s cubic-bezier(.34, 1.56, .64, 1), background .18s; }
.hero-cta:hover { color: white; background: var(--green-dark); transform: translateY(-3px) rotate(-1.5deg); }
.hero-cta > span { width: 26px; height: 26px; display: grid; place-items: center; border-radius: 50%; background: rgba(255, 255, 255, .22); font-size: 13px; }
.hero-ghost { display: inline-flex; align-items: center; gap: 12px; padding: 14px 22px 14px 14px; border: 1.5px solid #e2d3b4; border-radius: 999px; background: rgba(255, 253, 247, .7); color: var(--blue); font-size: 16px; font-weight: 800; cursor: pointer; transition: transform .2s cubic-bezier(.34, 1.56, .64, 1), border-color .18s; }
.hero-ghost:hover { border-color: var(--blue); transform: translateY(-3px) rotate(1.5deg); }
.hero-play { width: 34px; height: 34px; display: grid; place-items: center; padding-left: 2px; border-radius: 50%; background: var(--blue); color: #fdf6e6; font-size: 12px; }

.hero-visual { --cover-scale: 1; min-width: 0; min-height: 530px; position: relative; display: flex; align-items: center; justify-content: center; }
.hero-glow { position: absolute; width: 400px; height: 400px; border-radius: 50%; background: radial-gradient(circle, rgba(247, 217, 125, .5), transparent 68%); }
/* The box is a FIXED 600px — the design's own column width — and only ever
   scaled. The covers are a fan: spread across a wider column they stop
   overlapping, and reflowed narrower they bury each other's titles. Fixed
   geometry plus one scale keeps the overlap identical at every width, which
   is what makes the title insets below correct everywhere. */
.hero-covers { position: absolute; top: 0; bottom: 0; left: 50%; width: 600px; transform: translateX(-50%) scale(var(--cover-scale)); }
.hero-cover { position: absolute; overflow: hidden; border-radius: 18px; box-shadow: 0 40px 60px -34px rgba(13, 47, 67, .85); }
.hc-left { width: 238px; height: 322px; left: 2%; top: 88px; z-index: 1; background: linear-gradient(160deg, #3b2f6b, #221a45); animation: hero-float-a 8.5s ease-in-out infinite; }
.hc-mid { width: 288px; height: 392px; left: 50%; top: 52px; margin-left: -144px; z-index: 3; background: linear-gradient(160deg, #1e4e77, #0e2f4c); animation: hero-float-b 7.2s ease-in-out infinite; }
.hc-right { width: 242px; height: 330px; right: 2%; top: 104px; z-index: 2; background: linear-gradient(160deg, #e08a52, #c0603a); animation: hero-float-c 9.1s ease-in-out infinite; }
.hero-cover-emoji { position: absolute; inset: 0; display: grid; place-items: center; padding-bottom: 54px; font-size: 72px; filter: drop-shadow(0 10px 20px rgba(0, 0, 0, .4)); }
.hero-cover-copy { position: absolute; left: 18px; right: 18px; bottom: 22px; text-align: center; color: #fffdf7; text-shadow: 0 2px 14px rgba(0, 0, 0, .6); }
.hero-cover-copy small { display: block; color: rgba(255, 253, 247, .8); font-size: 9.5px; font-weight: 800; letter-spacing: 2.2px; }
.hero-cover-copy strong { display: block; margin-top: 7px; font-family: var(--display); font-size: 21px; line-height: 1.2; font-weight: 400; }
/* The middle cover sits ON the inner edge of both side covers — their titles are
   set in the sliver that stays visible, or they read as cut off mid-word. */
.hc-left .hero-cover-copy { right: 122px; }
.hc-right .hero-cover-copy { left: 122px; }
.hc-left .hero-cover-copy strong, .hc-right .hero-cover-copy strong { font-size: 15px; }
.hc-left .hero-cover-copy small, .hc-right .hero-cover-copy small { font-size: 8.5px; letter-spacing: 1.6px; }
.hc-left .hero-cover-emoji { padding-right: 96px; }
.hc-right .hero-cover-emoji { padding-left: 96px; }
.hero-cover-spark { position: absolute; top: 14px; right: 16px; color: rgba(255, 253, 247, .85); font-size: 13px; }
/* A real book cover REPLACES the crafted plate (fillHeroCovers adds .has-art) — the emoji
   and the sparkle go, the same call `bca-bare` makes in the showcase. The COPY stays and is
   rewritten with the book's own kicker and title, so a filled cover carries the showcase's
   chrome: the tinted theme badge, the language pill and the real title. It crops on purpose:
   the fan is a book-object mock like #bookPreview's cover, not an image viewer, and
   .hero-cover already clips. Everything here is put back on the img's error event. */
.hero-cover-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.hero-cover.has-art .hero-cover-emoji,
.hero-cover.has-art .hero-cover-spark { display: none; }
/* Artwork is whatever Gemini painted, so the title needs a floor of its own to sit on —
   the crafted plates had a dark gradient built into them and a real cover does not. */
.hero-cover.has-art::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(to top, rgba(9, 32, 48, .88), rgba(9, 32, 48, .3) 40%, transparent 66%);
}
.hero-cover.has-art .hero-cover-copy { z-index: 2; }
/* A filled cover is a real book and opens it — an unfilled one is decoration and is
   `aria-hidden` with no role, so neither the pointer nor the ring is offered for it. */
.hero-cover.has-art { cursor: pointer; }
.hero-cover.has-art:focus-visible { outline: 3px solid var(--gold); outline-offset: 3px; }
.hero-cover.has-art .hero-cover-copy strong {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
/* The crafted kickers are typed in caps ("SPACE STORY"); the real ones are written in
   sentence case, so the sheet — not the string — is what sets the case, or an artwork
   error reverting a slot would flip it. */
.hero-cover.has-art .hero-cover-copy small { text-transform: uppercase; }
/* The showcase's own badge and pill (.sb-badge / .sb-lang, absolutely placed against
   .hero-cover — .hero-cover-labels is a STATIC wrapper and must stay one). The pill moves
   to the top edge here: the bottom-right corner it takes on a card is where this cover's
   title now sits. */
.hero-cover .sb-lang { top: 10px; right: 10px; bottom: auto; }
/* The middle cover lies over the inner edge of both side covers, so a side cover has only
   its outer ~116px sliver to say anything in: the pill goes, and the badge follows the
   sliver — left on .hc-left, right on .hc-right. */
.hc-left .sb-lang, .hc-right .sb-lang { display: none; }
/* A side title is set smaller, so it gets the third line the middle one has no room for. */
.hc-left.has-art .hero-cover-copy strong,
.hc-right.has-art .hero-cover-copy strong { -webkit-line-clamp: 3; }
.hc-right .sb-badge { left: auto; right: 9px; }
.hc-left .sb-badge, .hc-right .sb-badge {
  max-width: 104px; padding: 5px 8px; font-size: 8.5px; letter-spacing: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The promises strip sits on the hero's own gradient — no band of its own and therefore
   no wave. Six cards on `minmax(168px, 1fr)`, which is what puts them on one row at
   1120px and wraps them two- and three-up on the way down with no breakpoint of its own.
   The icons are bare emoji, not the tiles the navy band used: at this size a tile behind
   every one of six cards reads as a toolbar. `hp-beat` is the design's own heartbeat on
   the first card only, and it is `hero-`-family named for the reason every animation in
   this sheet is — a duplicate `@keyframes` name is last-wins with no warning. */
.hero-promises { position: relative; padding: 0 34px 62px; }
.hero-promise-grid { position: relative; max-width: 1120px; margin: 34px auto 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 12px; }
.hero-promise { display: flex; align-items: center; gap: 11px; padding: 13px 14px; border: 1.5px solid #ecdcbb; border-radius: 18px; background: rgba(255, 253, 247, .8); transition: transform .18s ease, border-color .18s ease; }
.hero-promise:hover { transform: translateY(-3px); border-color: #eb9d8a; }
.hero-promise > span:last-child { min-width: 0; }
.hero-promise strong { display: block; font-size: 13.5px; line-height: 1.2; font-weight: 700; }
.hero-promise small { display: block; margin-top: 2px; color: #8a7a5f; font-size: 12px; }
.hp-icon { flex: none; font-size: 20px; line-height: 1; }
.hp-beat { animation: hero-heartbeat 2.4s ease-in-out infinite; }
@keyframes hero-heartbeat { 0%, 100% { transform: scale(1); } 14% { transform: scale(1.22); } 28% { transform: scale(1); } 42% { transform: scale(1.16); } 60% { transform: scale(1); } }

.section-heading { max-width: 660px; margin: 0 auto 60px; }
.centered { text-align: center; }
.section-heading h2 { margin: 13px 0 16px; font-family: var(--display); font-size: clamp(38px, 4vw, 52px); line-height: 1.06; font-weight: 500; letter-spacing: -2px; }
.section-heading p { max-width: 560px; margin: 0 auto; color: #687981; font-size: 14px; line-height: 1.8; }

/* The rhythm's second seam device: every banded section opens on the same line —
   its label hard left, a dashed rule running out to the edge of the shell. It is
   deliberately ONE shared class where the rest of this sheet prefixes per section:
   the whole claim the device makes is that the sections open alike, so a `hm-`/`fg-`/
   `faq-` copy each would be three chances for them to stop doing that. The label
   keeps its own per-section class (that is what the section colours), and the rule
   is the `<i>`, filled from `--band-rule` so a dark band can override it in one
   place. `.pricing-section` is exempt on purpose — the design keeps its eyebrow a
   centred pill, and a price list that opened like the others would lose the one
   heading on the page that is allowed to be the loudest. */
.band-eyebrow { display: flex; align-items: center; gap: 14px; }
.band-eyebrow > i { flex: 1 1 auto; min-width: 24px; height: 2px; border: 0; background: repeating-linear-gradient(90deg, var(--band-rule) 0 8px, transparent 8px 16px); }

/* How it works — Claude Design "Four Things" */
/* The first band after the hero, and the first seam: a flat sand band with a torn
   wave edge along its top. `overflow-x: clip` rather than `hidden` — the wave is a
   child of THIS section parked at `translateY(-99%)`, i.e. entirely above its own
   padding box, and `hidden` clips it to nothing. */
.how-section { position: relative; overflow-x: clip; padding: var(--band-pad) 40px; background: var(--band-how); }
/* The seam is filled from the band's own token, never a repeated literal. It hangs
   over the hero's navy promises band, so the sand reads as spilling into the join
   rather than meeting it at a ruled line. */
.hm-seam { position: absolute; left: 0; top: 1px; display: block; width: 100%; height: 46px; transform: translateY(-100%); }
.hm-seam path { fill: var(--band-how); }
.how-inner { position: relative; max-width: 1140px; margin: 0 auto; }
.how-heading { max-width: none; margin: 0; }
/* The label is restyled through .how-kicker alone — .kicker is shared with every other
   section on the page, the same sidestep .hero-eyebrow makes. It is flat lettering now
   rather than a navy pill: it sits on the shared `.band-eyebrow` line, where a filled
   pill would outweigh the dashed rule beside it. */
.how-kicker { display: inline-block; flex: none; color: #a4906f; font-size: 11px; font-weight: 800; letter-spacing: 2.2px; }
.how-heading h2 { margin: 14px 0 0; font-family: var(--display); font-size: 46px; line-height: 1.1; font-weight: 400; letter-spacing: normal; color: var(--ink); text-wrap: balance; }
.how-heading h2 em { font-style: italic; color: var(--coral); }
.how-heading p { max-width: 46em; margin: 8px 0 0; font-family: var(--display); font-style: italic; color: #8a7a5f; font-size: 16.5px; line-height: 1.55; text-wrap: pretty; }

/* Section sparkles. Decorative and aria-hidden; the keyframes are hm-prefixed because this
   sheet already carries hero-/faq-/ft-/sub-/cs-/kc- twinkle and bob variants and a duplicate
   @keyframes name is last-wins with no warning. */
.hm-spark { position: absolute; line-height: 1; animation: hm-twinkle 3.4s ease-in-out infinite; }
.hm-spark-a { top: 9%; left: 7%; color: var(--gold-light); font-size: 20px; }
.hm-spark-b { top: 22%; right: 8%; color: #eb9d8a; font-size: 15px; animation-duration: 4.2s; animation-delay: .6s; }
.hm-spark-c { bottom: 14%; left: 11%; color: #9fc7d4; font-size: 17px; animation-duration: 3.8s; animation-delay: 1.1s; }
@keyframes hm-twinkle { 0%, 100% { opacity: .28; transform: scale(.8) rotate(0deg); } 50% { opacity: 1; transform: scale(1.15) rotate(14deg); } }

/* The two-panel "you bring / we make" block (.hm-*). Static markup, no JS and no ids —
   it replaced a click-through mock of the creator that had to be kept in step with the
   product. It borrows no creator/reader class on purpose: the old section's reuse was an
   anti-drift trade that only paid off because it was live markup, and a static section
   inherits the hazard with none of the benefit. */
/* The explainer video. `.hm-`prefixed like the rest of this band; the frame is the paper
   card the section already uses, so the player reads as part of the shop window rather than
   an embed dropped into it. `aspect-ratio` holds the space before the poster loads, or the
   section jumps as it arrives. */
.hm-video { margin: 30px auto 0; max-width: 860px; text-align: center; }
.hm-video-player { width: 100%; aspect-ratio: 16 / 9; display: block; border-radius: 22px; background: #0d2f43; box-shadow: 0 18px 40px rgba(13, 47, 67, .18); border: 5px solid #fffdf7; }
.hm-video-note { margin: 10px 0 0; font-family: var(--display); font-style: italic; font-size: 15px; color: #8a7a5f; }
@media (max-width: 640px) { .hm-video { margin-top: 22px; } .hm-video-player { border-radius: 16px; border-width: 4px; } }

.hm-split { display: grid; grid-template-columns: 1fr 74px 1fr; align-items: stretch; margin: 44px 0 0; }
.hm-panel { display: flex; flex-direction: column; padding: 26px 24px 24px; border-radius: 28px; }
.hm-bring { background: #fffdf7; border: 2px dashed #e2d3b4; }
/* overflow lives on .hm-make ALONE, for its own sparkle — the bring panel needs none and
   must not gain one by a shared rule. */
.hm-make { position: relative; overflow: hidden; background: #eef4f0; border: 2px solid #cfe0d6; }
.hm-panel-spark { position: absolute; top: 22px; right: 26px; color: #e6bd3f; font-size: 15px; line-height: 1; animation: hm-twinkle 3.2s ease-in-out infinite; }

.hm-head { display: flex; align-items: center; gap: 11px; }
/* The tilt is stated on the element as well as inside the keyframe: with animations
   suppressed the keyframes never run, and the tile would sit straight. */
.hm-badge { width: 44px; height: 44px; flex: none; display: grid; place-items: center; border-radius: 15px; font-size: 22px; line-height: 1; transform: rotate(var(--hm-r, 0deg)); animation: hm-bob 3.6s ease-in-out infinite; }
.hm-bring .hm-badge { --hm-r: -6deg; background: var(--gold-light); border: 2.5px solid #e6bd3f; }
.hm-make .hm-badge { --hm-r: 5deg; background: #456f5c; animation-duration: 4.2s; animation-delay: .4s; }
@keyframes hm-bob { 0%, 100% { transform: translateY(0) rotate(var(--hm-r, 0deg)); } 50% { transform: translateY(-6px) rotate(var(--hm-r, 0deg)); } }
.hm-title { font-family: var(--display); font-size: 25px; font-weight: 400; color: var(--ink); }

/* The list is the design's collapsed chip: icon tile, title, chevron pill, and a body
   that is not there until it is opened. It is a `<details>`, so the whole accordion and
   its keyboard access cost this section no script and no hook — the call the FAQ port
   made. The card chrome lives on the `li` and the padding on the summary, so the entire
   closed chip is the click target and the open body can indent past the icon. */
.hm-list { list-style: none; margin: 20px 0 18px; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.hm-list li { position: relative; border-radius: 19px; background: white; overflow: hidden; transition: border-color .18s; }
.hm-bring .hm-list li { border: 2px solid #ece0c6; }
.hm-make .hm-list li { border: 2px solid #dcebe2; }
.hm-bring .hm-list li:hover { border-color: #eb9d8a; }
.hm-make .hm-list li:hover { border-color: #8cb8a1; }

/* `list-style: none` is what removes the disclosure triangle in Firefox and Safari 17+;
   the ::-webkit-details-marker rule is for the older WebKit that ignores it. Without both
   a stray triangle sits beside the icon tile. */
.hm-drop summary { display: flex; align-items: center; gap: 13px; padding: 15px 16px; cursor: pointer; list-style: none; }
.hm-drop summary::-webkit-details-marker { display: none; }
.hm-drop summary::marker { content: ""; }
.hm-drop summary:focus-visible { outline: 2px solid var(--coral); outline-offset: -4px; border-radius: 17px; }
.hm-drop summary strong { flex: 1; min-width: 0; font-size: 15.5px; font-weight: 800; line-height: 1.25; color: var(--ink); }

.hm-ico { width: 44px; height: 44px; flex: none; display: grid; place-items: center; border-radius: 14px; font-size: 23px; line-height: 1; }
.hm-bring .hm-ico { background: #faf3e2; }
.hm-make .hm-ico { background: #eef4f0; }

/* The open state is a plain transform on the element, never a keyframe: the global
   reduced-motion block kills animations, and a chevron whose rotation lived in a frame
   would still point DOWN on an open card. The transition is the nicety, not the state.
   The glow is stated on the element as well as in the frame for the same reason — with
   animations suppressed the resting halo is what says "this opens", so the keyframe only
   breathes a ring outward and the base shadow is a real style, not a frame's 0% stop.
   It animates box-shadow ONLY: transform belongs to the open state and the two must not
   fight over the same property. `--hm-glow` is per column, the call `--card-line` and
   `--ft-accent` make, so the halo cannot drift off the border it sits inside. */
.hm-chev { width: 26px; height: 26px; flex: none; display: grid; place-items: center; border-radius: 999px; font-size: 13px; font-weight: 800; line-height: 1;
  transition: transform .18s, box-shadow .18s; box-shadow: 0 0 0 0 var(--hm-glow), 0 0 9px var(--hm-glow); animation: hm-chev-glow 2.4s ease-out infinite; }
.hm-bring .hm-chev { --hm-glow: rgba(235,157,138,.5); color: #a4906f; background: #faf3e2; }
.hm-make .hm-chev { --hm-glow: rgba(140,184,161,.5); color: #6b8578; background: #eef4f0; }
/* Ten chips pulsing in lockstep reads as one blinking row rather than ten openable cards. */
.hm-list li:nth-child(2) .hm-chev { animation-delay: .2s; }
.hm-list li:nth-child(3) .hm-chev { animation-delay: .4s; }
.hm-list li:nth-child(4) .hm-chev { animation-delay: .6s; }
.hm-list li:nth-child(5) .hm-chev { animation-delay: .8s; }
/* Opened, the invitation has been taken: the halo goes and the caret points back up. */
.hm-drop[open] .hm-chev { transform: rotate(180deg); box-shadow: none; animation: none; }
@keyframes hm-chev-glow {
  0%, 100% { box-shadow: 0 0 0 0 var(--hm-glow), 0 0 9px var(--hm-glow); }
  60% { box-shadow: 0 0 0 7px transparent, 0 0 15px var(--hm-glow); }
}

.hm-body { margin: 0 16px 15px 73px; font-size: 13.5px; line-height: 1.5; text-wrap: pretty; }
.hm-bring .hm-body { color: #8a7a5f; }
.hm-make .hm-body { color: #6b8578; }

/* The foot chip is pushed to the bottom of the panel, so two columns of unequal length
   still bottom out level. The `auto` top margin is the whole mechanism, so the tinted
   variants below must set colour ONLY — a `margin-top` on either of them outranks the
   base rule and parks the chip directly under the list, with nothing to see until one
   column's copy grows. The 18px floor lives on the list's own bottom margin instead
   (flex items do not collapse margins, so it survives `auto` above it). */
.hm-aside { display: flex; align-items: center; gap: 10px; margin: auto 0 0; padding: 12px 14px; border-radius: 14px; font-family: var(--display); font-style: italic; font-size: 14px; line-height: 1.45; }
.hm-aside-ico { font-size: 18px; font-style: normal; line-height: 1; }
.hm-bring .hm-aside { color: #6b5c45; background: #faf3e2; }
.hm-make .hm-aside { color: #3f6154; background: #e2ede7; }

/* The rail between the panels. It is decorative (aria-hidden), and at the one-column
   breakpoint it turns to point down instead of across. */
.hm-rail { display: flex; flex-direction: column; align-items: center; align-self: stretch; padding: 120px 0 40px; }
.hm-rail-line { flex: 1; width: 3px; border-radius: 999px; background: repeating-linear-gradient(180deg, #e0cfa8 0 6px, transparent 6px 15px); }
.hm-rail-plate { width: 46px; height: 46px; flex: none; margin: 12px 0; display: grid; place-items: center; border-radius: 999px; color: var(--blue); background: #fffdf7; border: 2px solid #e6bd3f; box-shadow: 0 12px 20px -14px rgba(191, 146, 20, .9); animation: hm-nudge 3.4s ease-in-out infinite; }
@keyframes hm-nudge { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(5px); } }

.hm-finish { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 20px; max-width: 820px; margin: 34px auto 0; padding: 20px 26px; border-radius: 24px; background: #fffdf7; border: 2.5px solid #f0b429; box-shadow: 0 30px 52px -46px rgba(191, 146, 20, .95); }
.hm-finish-ico { --hm-r: -5deg; width: 48px; height: 48px; flex: none; display: grid; place-items: center; border-radius: 16px; background: #faf3e2; font-size: 24px; line-height: 1; transform: rotate(var(--hm-r)); animation: hm-bob 3.8s ease-in-out infinite; }
.hm-finish-copy { flex: 1; min-width: 240px; }
.hm-finish-copy strong { display: block; font-family: var(--display); font-size: 21px; font-weight: 400; color: var(--ink); }
.hm-finish-copy small { display: block; margin-top: 3px; font-family: var(--display); font-style: italic; font-size: 14.5px; color: #8a7a5f; line-height: 1.45; }
.hm-cta { flex: none; min-height: 54px; padding: 0 26px; gap: 10px; border-radius: 16px; font-size: 16px; box-shadow: 0 20px 30px -18px rgba(47, 82, 68, .95); }
.hm-cta:hover { transform: translateY(-2px) rotate(-.8deg); }

.how-footnote { text-align: center; color: #6a5a3a; font-size: 14.5px; margin: 44px 0 0; }
.how-footnote strong { color: var(--ink); }
.how-footnote span { color: var(--gold); }

.creator-section { position: relative; overflow: hidden; padding: 108px 0 120px; color: white; background: var(--blue); }
.creator-section .section-shell { position: relative; z-index: 1; }
.creator-section .kicker { color: var(--gold-light); }
.section-heading.light h2 { color: white; }
.section-heading.light p { color: #b9ccd2; }
.creator-blob { position: absolute; border-radius: 50%; filter: blur(1px); opacity: .25; }
.blob-one { width: 420px; height: 420px; left: -240px; top: 160px; background: #226980; }
.blob-two { width: 480px; height: 480px; right: -250px; bottom: 10px; background: #083046; }
.creator-progress { max-width: 685px; display: flex; align-items: flex-start; margin: 0 auto 35px; }
.creator-progress i { flex: 1; margin: 19px -4px 0; border-top: 1px dashed #718b96; }
.progress-step { min-width: 120px; display: flex; flex-direction: column; align-items: center; gap: 8px; border: 0; color: #79939d; background: transparent; cursor: pointer; }
.progress-step span { width: 38px; height: 38px; display: grid; place-items: center; border: 1px solid #68838f; border-radius: 50%; background: var(--blue); font-size: 12px; font-weight: 800; transition: .2s; }
.progress-step small { font-size: 9px; font-weight: 700; letter-spacing: .2px; }
.progress-step.active { color: white; }
.progress-step.active span, .progress-step.complete span { color: var(--ink); background: var(--gold); border-color: var(--gold); }
.progress-step.complete span { font-size: 0; }
.progress-step.complete span::after { content: "✓"; font-size: 13px; }
.creator-layout { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(340px, .8fr); gap: 35px; align-items: start; }
.creator-card { min-height: 665px; padding: 41px 46px 36px; border-radius: 17px 25px 18px 22px; color: var(--ink); background: var(--paper); box-shadow: 0 30px 75px rgba(3, 24, 35, .3); }
.form-panel { display: none; }
.form-panel.active { display: block; animation: panelIn .3s ease both; }
@keyframes panelIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.panel-heading { padding-bottom: 25px; margin-bottom: 26px; border-bottom: 1px solid #e3ded2; }
.panel-count { color: var(--coral); text-transform: uppercase; letter-spacing: 1.6px; font-size: 9px; font-weight: 900; }
.panel-heading h3 { margin: 8px 0 7px; font-family: var(--display); font-size: 29px; font-weight: 500; letter-spacing: -.8px; }
.panel-heading p { margin: 0; color: #78868c; font-size: 11px; line-height: 1.6; }
.field { position: relative; display: flex; flex-direction: column; gap: 8px; margin: 0 0 22px; border: 0; padding: 0; }
.bible-story-field[hidden] { display: none; }
.bible-story-field { padding: 14px; border: 1px solid rgba(222, 168, 68, .38); border-radius: 10px; background: #fffaf0; }
.bible-story-field > small { margin-left: 0; line-height: 1.5; }
.field-row { display: grid; grid-template-columns: 1fr 135px; gap: 14px; }
.field > span:first-child, .field legend { color: #354f5d; font-size: 11px; font-weight: 800; }
.field small, .field legend small { color: #98a2a4; font-size: 9px; font-weight: 500; margin-left: 4px; }
.field input, .field select, .field textarea, .interests-field > input { width: 100%; min-height: 46px; padding: 12px 14px; color: var(--ink); border: 1px solid #d8d5cb; border-radius: 7px; outline: 0; background: white; font-size: 12px; transition: border .2s, box-shadow .2s; }
.field textarea { resize: vertical; line-height: 1.6; }
.field input:focus, .field select:focus, .field textarea:focus, .interests-field > input:focus { border-color: #799fac; box-shadow: 0 0 0 3px rgba(14, 61, 86, .08); }
.field-error input { border-color: var(--coral); box-shadow: 0 0 0 3px rgba(235, 117, 91, .1); }
.field input::placeholder, .field textarea::placeholder, .interests-field > input::placeholder { color: #a9aeae; }
.photo-upload { min-height: 92px; display: flex; align-items: center; gap: 13px; padding: 14px; border: 1px dashed #b7c3c3; border-radius: 9px; background: #f7f7f1; cursor: pointer; transition: .2s; }
.photo-upload:hover, .photo-upload.dragging { border-color: var(--coral); background: #fff8f5; }
.photo-upload input { position: absolute; opacity: 0; pointer-events: none; }
.upload-icon, .upload-preview { flex: 0 0 auto; width: 48px; height: 48px; display: grid; place-items: center; overflow: hidden; border-radius: 50%; color: var(--coral); background: #f7ddd4; font-size: 23px; }
.upload-preview { display: none; background-size: cover; background-position: center; }
.photo-upload.has-image .upload-icon { display: none; }
.photo-upload.has-image .upload-preview { display: block; }
.upload-copy { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.upload-copy strong { overflow: hidden; font-size: 11px; white-space: nowrap; text-overflow: ellipsis; }
.upload-copy small { color: #8e9a9e; font-size: 9px; }
.upload-button { margin-left: auto; padding: 8px 11px; border: 1px solid #ccd2cf; border-radius: 6px; background: white; font-size: 9px; font-weight: 800; white-space: nowrap; }
.privacy-note { display: flex; align-items: center; gap: 5px; margin: 2px 0 0; color: #9aa3a3; font-size: 8px; }
.privacy-note span { color: var(--green); }
.photo-consent { display: flex; align-items: flex-start; gap: 9px; padding: 10px 11px; margin-top: 9px; border: 1px solid #d8d5cb; border-radius: 8px; background: #f7f7f1; cursor: pointer; }
.photo-consent input { width: 15px; height: 15px; margin: 1px 0 0; accent-color: var(--green); }
.photo-consent > span { display: flex; flex-direction: column; gap: 3px; color: var(--ink); }
.photo-consent strong { font-size: 9px; }
.photo-consent small { color: #7d8c90; font-size: 8px; line-height: 1.45; }
.ai-connection.limited { border-color: #ead6a7; background: #fff8e6; }
.ai-connection.limited .ai-connection-dot { background: #d49d35; box-shadow: 0 0 0 4px rgba(212,157,53,.12); }
.interests-field legend { margin-bottom: 10px; }
.chip-grid { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 9px; }
.choice-chip { min-height: 35px; padding: 0 11px; border: 1px solid #d8d5cb; border-radius: 8px; color: #49606a; background: white; font-size: 9px; font-weight: 700; cursor: pointer; transition: .2s; }
.choice-chip span { margin-right: 5px; font-size: 13px; filter: saturate(.8); }
.choice-chip:hover { border-color: #91a7ad; transform: translateY(-1px); }
.choice-chip.selected { color: var(--blue); border-color: #a9c4c6; background: #e7f1ef; box-shadow: inset 0 0 0 1px #a9c4c6; }
/* Life skills and values: interests chips, but capped — the note carries the promise
   that a pick is shown happening rather than taught. */
.lesson-field .chip-grid { margin-bottom: 4px; }
.lesson-note { display: block; color: var(--ink-soft); font-size: 11px; line-height: 1.5; }
.form-actions { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding-top: 10px; }
.form-actions.end { justify-content: flex-end; }
.form-actions .button { min-width: 190px; min-height: 48px; font-size: 11px; }
.back-button { border: 0; padding: 10px 0; color: #6f7e84; background: transparent; font-size: 11px; font-weight: 800; cursor: pointer; }
.back-button:hover { color: var(--coral); }
.mode-toggle { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; padding: 5px; margin-bottom: 25px; border-radius: 9px; background: #ecece5; }
.mode-toggle button { min-height: 41px; border: 0; border-radius: 6px; color: #7c878a; background: transparent; font-size: 10px; font-weight: 800; cursor: pointer; }
.mode-toggle button span { margin-right: 7px; }
.mode-toggle button.active { color: var(--ink); background: white; box-shadow: 0 2px 8px rgba(13, 47, 67, .08); }
.ai-connection { min-height: 48px; display: flex; align-items: center; gap: 10px; padding: 9px 12px; margin: -13px 0 20px; border: 1px solid #d8d5cb; border-radius: 8px; color: #66787e; background: #f5f3ec; }
.ai-connection-dot { width: 9px; height: 9px; flex: 0 0 auto; border-radius: 50%; background: #d49d35; box-shadow: 0 0 0 4px rgba(212,157,53,.12); }
.ai-connection > span:last-child { display: flex; flex-direction: column; gap: 2px; }
.ai-connection strong { color: var(--ink); font-size: 9px; }
.ai-connection small { font-size: 8px; }
.ai-connection.connected { border-color: #bed5c3; background: #eef7ef; }
.ai-connection.connected .ai-connection-dot { background: #579068; box-shadow: 0 0 0 4px rgba(87,144,104,.12); }
.ai-connection.disconnected { border-color: #edcabf; background: #fff4f0; }
.ai-connection.disconnected .ai-connection-dot { background: var(--coral); box-shadow: 0 0 0 4px rgba(235,117,91,.12); }
.story-mode { display: none; }
.story-mode.active { display: block; }
.theme-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 9px; }
.theme-card { min-height: 103px; display: flex; flex-direction: column; align-items: flex-start; padding: 13px; border: 1px solid #d8d5cb; border-radius: 8px; color: var(--ink); background: white; text-align: left; cursor: pointer; transition: .2s; }
.theme-card > span { font-size: 22px; line-height: 1; color: var(--coral); }
.theme-card strong { margin-top: 12px; font-size: 10px; }
.theme-card small { margin: 3px 0 0; font-size: 8px; }
.theme-card.selected { border-color: #7fa2aa; background: #edf5f3; box-shadow: inset 0 0 0 1px #7fa2aa; }
.magic-button { width: 100%; min-height: 67px; display: flex; align-items: center; gap: 12px; padding: 11px 15px; border: 0; border-radius: 9px; color: white; background: var(--blue); text-align: left; cursor: pointer; transition: .2s; }
.magic-button:hover { background: #164e69; transform: translateY(-1px); }
.magic-symbol { width: 39px; height: 39px; display: grid; place-items: center; border-radius: 50%; color: var(--gold); background: rgba(255,255,255,.1); font-size: 19px; }
.magic-button > span:nth-child(2) { display: flex; flex-direction: column; gap: 3px; }
.magic-button strong { font-family: var(--display); font-size: 14px; }
.magic-button small { color: #aac2cb; font-size: 8px; }
.magic-arrow { margin-left: auto; font-size: 21px; }
.magic-button.loading .magic-symbol { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.generation-progress { display: none; padding: 13px 14px; margin-top: 10px; border: 1px solid #c8d8d9; border-radius: 8px; background: #f3f8f7; }
.generation-progress.show { display: block; }
.generation-progress > div { display: flex; align-items: center; justify-content: space-between; gap: 15px; color: var(--ink); font-size: 9px; font-weight: 800; }
.generation-progress > div strong { color: #6d8187; font-size: 8px; }
.generation-progress > i { height: 5px; display: block; overflow: hidden; margin: 9px 0 7px; border-radius: 5px; background: #dce7e5; }
.generation-progress > i b { width: 8%; height: 100%; display: block; border-radius: inherit; background: linear-gradient(90deg, var(--coral), var(--gold)); transition: width .35s ease; }
.generation-progress > small { color: #75878b; font-size: 8px; }
.generated-story { display: none; align-items: flex-start; gap: 10px; padding: 12px 14px; margin-top: 10px; border: 1px solid #b9d3c0; border-radius: 8px; background: #edf7ee; }
.generated-story.show { display: flex; }
.generated-check { width: 22px; height: 22px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 50%; color: white; background: #579068; font-size: 10px; }
.generated-copy { flex: 1; }
.generated-story strong { font-size: 10px; }
.generated-story p { margin: 3px 0 0; color: #65806d; font-size: 8px; line-height: 1.5; }
.generated-source { display: inline-flex; margin-top: 5px; color: #5a7562 !important; font-size: 7px !important; font-weight: 800 !important; text-transform: uppercase; letter-spacing: .7px; }
.read-story-link { align-self: center; flex: 0 0 auto; padding: 8px 0 8px 12px; border: 0; color: var(--green); background: transparent; font-size: 8px; font-weight: 900; cursor: pointer; }
.read-story-link:hover { color: var(--coral); }
.idea-prompt { display: flex; gap: 10px; padding: 13px; border-radius: 8px; color: #7a6436; background: #fff2c9; }
.idea-prompt p { margin: 0; font-size: 9px; line-height: 1.5; }
.idea-prompt strong { display: block; }
.char-count { position: absolute; right: 10px; bottom: 8px; color: #a1a7a7; font-size: 8px !important; font-weight: 500 !important; }
.format-options { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.format-card { min-height: 86px; position: relative; display: grid; grid-template-columns: 36px 1fr auto; align-items: center; gap: 10px; padding: 14px; border: 1px solid #d8d5cb; border-radius: 8px; color: var(--ink); background: white; text-align: left; cursor: pointer; transition: .2s; }
.format-card.selected { border-color: #7fa2aa; background: #edf5f3; box-shadow: inset 0 0 0 1px #7fa2aa; }
.format-book { width: 29px; height: 40px; display: block; border-radius: 2px 5px 5px 2px; background: var(--blue); box-shadow: 4px 3px 0 #d5d0c1; }
.format-book.softcover { background: var(--coral); box-shadow: 2px 2px 0 #d5d0c1; transform: rotate(3deg); }
.format-card > span:nth-child(2) { display: flex; flex-direction: column; gap: 3px; }
.format-card strong { font-size: 10px; }
.format-card small { margin: 0; font-size: 8px; }
.format-card b { font-family: var(--display); font-size: 13px; }
.format-card i { position: absolute; top: -7px; right: 10px; padding: 3px 7px; border-radius: 10px; color: white; background: var(--coral); font-size: 7px; font-style: normal; text-transform: uppercase; letter-spacing: .4px; }
.book-includes { padding: 14px 16px; margin: 22px 0 4px; border-radius: 8px; background: #f1efe7; }
.book-includes > strong { font-family: var(--display); font-size: 11px; }
.book-includes div { display: grid; grid-template-columns: 1fr 1fr; gap: 7px 15px; margin-top: 9px; }
.book-includes span { color: #65787e; font-size: 8px; }
.payment-gate-note { display: flex; align-items: center; justify-content: flex-end; gap: 6px; margin: 12px 0 0; color: #899596; font-size: 8px; }
.payment-gate-note span { width: 15px; height: 15px; display: grid; place-items: center; border-radius: 50%; color: white; background: #579068; font-size: 7px; }

.book-preview { position: sticky; top: 24px; min-height: 665px; padding: 26px 28px 22px; border: 1px solid rgba(255,255,255,.13); border-radius: 17px 24px 19px 22px; background: rgba(255,255,255,.075); backdrop-filter: blur(8px); }
.preview-label { display: flex; align-items: center; color: #d8e5e8; text-transform: uppercase; letter-spacing: 1.4px; font-size: 8px; font-weight: 800; }
.preview-label > span { width: 6px; height: 6px; margin-right: 7px; border-radius: 50%; background: var(--gold); box-shadow: 0 0 0 4px rgba(233,169,43,.12); }
.preview-label small { margin-left: auto; color: #78939d; letter-spacing: 0; text-transform: none; }
.book-stage { height: 485px; position: relative; display: grid; place-items: center; perspective: 900px; }
.book { width: 258px; height: 345px; position: relative; transform: rotateY(-8deg) rotateZ(-2.5deg); filter: drop-shadow(18px 25px 24px rgba(0,0,0,.28)); transition: transform .3s; }
.book:hover { transform: rotateY(-2deg) rotateZ(-1deg) translateY(-3px); }
.book-spine { position: absolute; z-index: 5; left: -9px; top: 2px; bottom: 3px; width: 17px; border-radius: 5px 0 0 5px; background: #e5a729; box-shadow: inset -3px 0 4px rgba(108,66,6,.25); }
.book-cover { position: absolute; z-index: 4; inset: 0; overflow: hidden; border-radius: 3px 10px 10px 3px; background: var(--blue); transform-origin: left; }
.book-cover > img { width: 100%; height: 100%; object-fit: cover; object-position: 58% center; filter: saturate(.88) brightness(.83); transform: scale(1.12); }
.cover-vignette { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(2,26,42,.42) 0%, transparent 42%, rgba(2,24,38,.28) 100%); }
.cover-copy { position: absolute; z-index: 2; left: 18px; right: 18px; top: 26px; text-align: center; text-shadow: 0 2px 8px rgba(0,0,0,.5); }
.cover-kicker { color: #f4cf71; letter-spacing: 1.8px; font-size: 6px; font-weight: 900; }
.cover-copy h3 { margin: 9px 0 7px; color: white; font-family: var(--display); font-size: 26px; line-height: .92; font-weight: 500; letter-spacing: -.8px; }
.cover-copy p { max-width: 180px; margin: 0 auto; color: #e7eff0; font-family: var(--display); font-size: 8px; line-height: 1.3; font-style: italic; }
.cover-photo { width: 43px; height: 43px; position: absolute; z-index: 3; right: 15px; bottom: 15px; display: grid; place-items: center; border: 3px solid #f0c65c; border-radius: 50%; background: var(--blue); background-size: cover; background-position: center; box-shadow: 0 3px 10px rgba(0,0,0,.3); }
.cover-photo span { color: var(--gold); }
.book-pages { position: absolute; right: -8px; top: 5px; bottom: -4px; width: 15px; border-radius: 0 8px 8px 0; background: repeating-linear-gradient(90deg, #eee8d6 0 2px, #d0c9b7 2px 3px); transform: skewY(4deg); }
.stage-star { position: absolute; color: var(--gold); }
.star-one { top: 50px; left: 28px; font-size: 21px; transform: rotate(15deg); }
.star-two { right: 25px; bottom: 67px; font-size: 14px; }
.star-three { left: 57px; bottom: 75px; color: #91b5bd; font-size: 31px; }
.preview-meta { min-height: 53px; display: flex; align-items: center; justify-content: space-between; gap: 15px; padding: 13px 0 0; border-top: 1px solid rgba(255,255,255,.13); }
.preview-meta > div { display: flex; flex-direction: column; gap: 4px; }
.preview-meta > div:last-child { text-align: right; }
.preview-meta strong { font-family: var(--display); font-size: 13px; }
.preview-meta span { color: #87a1aa; font-size: 8px; }
.preview-hint { display: flex; gap: 7px; margin: 14px 0 0; color: #819ba4; font-size: 8px; line-height: 1.5; }
.preview-hint span { color: var(--gold); }

/* ---- "Forever Gift": the claim beside the three editions ("Section Rhythm.dc.html") ----
   The keyframes are `fg-`prefixed: this sheet already carries hero-/faq-/ft-/sub-/
   hm- twinkle and bob variants, and a duplicate `@keyframes` name is last-wins with
   no warning. The tilt is stated on the ELEMENT as well as inside the keyframe —
   the global reduced-motion block (`animation-duration: .01ms`) stops the keyframe
   dead, and a book whose only tilt lived in the frame would sit straight.
   An edition's colour is one custom property, `--fg-tint` on the virtue tiles and a
   per-format background on the blocks — the `--ft-accent` / `.tint-*` call, not the
   design's per-item inline styles.
   An edition is still ONE node carrying its block, its name, its price and its chip,
   so hiding an unshippable format hides one node and the parts can never fall out of
   step. `fg-sway` went with the "Most loved" badge the design does not carry here. */
@keyframes fg-twinkle { 0%, 100% { opacity: .28; transform: scale(.8) rotate(0deg); } 50% { opacity: 1; transform: scale(1.15) rotate(14deg); } }
@keyframes fg-bob { 0%, 100% { transform: translateY(0) rotate(var(--fg-r, 0deg)); } 50% { transform: translateY(-6px) rotate(var(--fg-r, 0deg)); } }
/* The page's strongest full stop, and the rhythm's fourth seam device: navy, CRESTING
   into the paper above it, everything inside it re-coloured for a dark band. It used
   to be two cream gradient layers whose top and bottom edges were tuned to the
   neighbours they met; a flat band needs none of that tuning, which is the argument
   the banded rhythm makes. It was arched at the top before that, which the design
   calls an odd floating slab — there is no radius here now and none below either:
   the band rises on `.fg-seam` and spills out on `.pricing-seam`, so neither this
   section nor `.examples-section` owes an arch any padding (see the `--band-*` block).
   `overflow-x: clip` and never `hidden`: the crest and the two twinkles above it
   hang outside this section's box on purpose. */
.fg-section { position: relative; overflow-x: clip; color: #fffdf7;
  padding: var(--band-pad) 0;
  background: var(--band-gift); }
/* The crest, filled from the RECEIVING band's own token — this band is what the wave
   is made of, so it reads `--band-gift`, the same call `.hm-seam` makes for its own
   sand. `top: 1px` under a full translate is the one-pixel overlap every seam here
   carries against a navy-to-paper hairline. The 56-unit viewBox drawn 54px tall is
   the design's own squash, and `.hm-seam` carries the same 48-into-46. */
.fg-seam { position: absolute; left: 0; top: 1px; display: block; width: 100%; height: 54px; transform: translateY(-100%); }
.fg-seam path { fill: var(--band-gift); }
/* The twinkles moved OUT of the band and into the gap the crest opens above it —
   the design's own placement. Both are gold: they sit over `.examples-section`'s
   paper now, where the coral one washed out. The resting opacity is stated on the
   ELEMENT and not left as the keyframe's 0% stop, the rule `hm-bob`'s tilt and the
   chip's glow both keep: with animations suppressed the frames never run, and two
   full-strength gold ✦ on cream are louder than the twinkle they stand in for.
   A running animation still owns the property, so this only shows when it is off. */
.fg-twinkle { position: absolute; pointer-events: none; color: var(--gold-light); opacity: .55; animation: fg-twinkle 3.4s ease-in-out infinite; }
.fg-twinkle-one { top: -26px; left: 16%; font-size: 15px; animation-duration: 4.6s; }
.fg-twinkle-two { top: -14px; right: 24%; font-size: 11px; animation-duration: 5.4s; animation-delay: .8s; }

/* A dark band overrides the shared eyebrow rule's colour in ONE place, the same call
   `--fg-tint` makes for an edition: the `.band-eyebrow` markup is identical to the
   cream sections' and only the two tokens move. */
.fg-section { --band-rule: rgba(255,253,247,.35); }
.fg-head { max-width: none; margin: 0; }
.fg-kicker { display: inline-block; flex: none; color: rgba(247,217,125,.9); font-size: 11px; font-weight: 800; letter-spacing: 2.2px; text-transform: uppercase; }
.fg-say h2 { max-width: 20em; margin: 0; font-family: var(--display); font-size: clamp(30px, 3.2vw, 40px); font-weight: 400; line-height: 1.1; letter-spacing: -.8px; text-wrap: balance; }
/* Coral on navy is the one accent this band cannot keep — it is the section's own
   background family. The design swaps it for the gold it uses everywhere else here. */
.fg-say h2 em { color: var(--gold-light); font-style: italic; }
.fg-say p { max-width: 32em; margin: 12px 0 0; color: rgba(255,253,247,.8); font-family: var(--display); font-style: italic; font-size: 17px; line-height: 1.55; text-wrap: pretty; }

/* The band is the design's two columns: what the book IS on the left (the claim, then
   the three virtues stacked), and the three editions standing on the right. There is no
   plank and no dashed tag any more — a shelf under a row that is now a column of its own
   had nothing to stand on, and the tag's claim ("one story · three ways") is what the
   editions say by carrying the same three prices for the same book.
   The columns are PLACED, not wrapped: `.fg-say` and `.fg-virtues` are both column 1 and
   `.fg-editions` spans both rows of column 2, so the stack at 1040px is one line of CSS
   rather than a second div in the markup that only exists to be a column.
   Both tracks are `minmax(0, 1fr)` and the stacked one has to be as well: a bare `1fr`
   is `minmax(auto, 1fr)`, and the editions row's min-content is the sum of three DEFINITE
   block widths, so the track grows to fit them and drags the claim and the virtue cards
   out with it — under `overflow-x: clip` that is a silent crop, not a scrollbar. */
.fg-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 38px; align-items: center; margin-top: 16px; }
.fg-say { grid-column: 1; align-self: end; }
.fg-virtues { grid-column: 1; align-self: start; display: flex; flex-direction: column; gap: 12px; margin-top: 20px; }
.fg-editions { --fg-scale: 1; grid-column: 2; grid-row: 1 / span 2; align-self: center; display: flex; align-items: flex-end; justify-content: center; gap: 22px; }

.fg-edition { flex: 0 1 auto; min-width: 0; text-align: center; transition: transform .22s cubic-bezier(.34, 1.56, .64, 1); }
.fg-edition:hover { transform: translateY(-8px); }
/* `hidden` has to be beaten in the sheet: an unshippable edition is hidden by
   `renderPrices`, and the display above wins over the attribute on its own (the
   `.addon-card[hidden]` / `.kc-star[hidden]` call). */
.fg-edition[hidden] { display: none; }

/* Each edition is ONE block — the object, not a cover mock. The tints code the FORMAT
   (slate device, gold softcover, green hardcover with its gilt spine), which is the one
   thing the three of them are allowed to differ in.
   The three are ONE knob: `--fg-scale` on the row, every figure derived from the
   full-size one, so a narrower phone is a single number and the aspect ratios cannot
   drift apart the way three hand-tuned pixel triples do — the call `--cover-scale`
   makes for the hero fan. It has to be a LAYOUT width and never `transform: scale()`:
   a transform leaves the min-content contribution untouched, and that contribution IS
   what oversizes the grid track above. Later breakpoint blocks win, so they stay in
   descending file order. */
.fg-block { position: relative; display: block; max-width: 100%; box-shadow: 0 22px 34px -18px rgba(0,0,0,.6); }
.fg-device { width: calc(148px * var(--fg-scale, 1)); height: calc(106px * var(--fg-scale, 1)); border-radius: 13px; border: calc(5px * var(--fg-scale, 1)) solid #4a698f; background: #5f7fa8; }
.fg-book-soft { --fg-r: -2deg; width: calc(124px * var(--fg-scale, 1)); height: calc(166px * var(--fg-scale, 1)); border-radius: 5px 13px 13px 5px; background: var(--gold); transform: rotate(var(--fg-r)); animation: fg-bob 5.6s ease-in-out infinite; }
.fg-book-hard { width: calc(140px * var(--fg-scale, 1)); height: calc(198px * var(--fg-scale, 1)); border-radius: 5px 14px 14px 5px; border-left: calc(13px * var(--fg-scale, 1)) solid var(--gold-light); background: var(--green); }
/* The sticker that says the digital book rides along with a printed one. It hangs
   outside its block, which is why no edition may clip — and it is absolutely
   positioned, so it adds nothing to the row's width and is the first thing a row
   sized to exactly fit loses. Measure the rightmost sticker, not the sum of blocks. */
.fg-incl { position: absolute; right: calc(-12px * var(--fg-scale, 1)); bottom: -8px; display: grid; place-items: center; width: calc(52px * var(--fg-scale, 1)); height: calc(38px * var(--fg-scale, 1)); border: 3px solid #4a698f; border-radius: 7px; background: #5f7fa8; color: #fdf6e6; font-size: calc(14px * var(--fg-scale, 1)); font-style: normal; box-shadow: 0 10px 18px -10px rgba(0,0,0,.7); transform: rotate(-8deg); }

.fg-name { display: block; margin-top: calc(22px * var(--fg-scale, 1)); font-size: 13.5px; font-weight: 800; }
.fg-price { display: block; margin-top: 2px; color: var(--gold-light); font-family: var(--display); font-size: 24px; }
.fg-note { display: block; margin-top: 2px; color: rgba(255,253,247,.72); font-size: 12px; }
.fg-chip { display: inline-block; margin-top: 8px; padding: 5px 10px; border: 1px solid rgba(255,253,247,.28); border-radius: 99px; background: rgba(255,253,247,.12); color: rgba(255,253,247,.85); font-size: 10.5px; font-weight: 800; letter-spacing: 1.1px; text-transform: uppercase; }

.fg-virtue { display: flex; align-items: flex-start; gap: 13px; padding: 14px 15px; border: 1.5px solid rgba(255,253,247,.2); border-radius: 18px; background: rgba(255,253,247,.07); transition: transform .18s ease; }
.fg-virtue:hover { transform: translateY(-3px) rotate(.6deg); }
.fg-virtue-icon { width: 40px; height: 40px; flex: none; display: grid; place-items: center; border-radius: 13px; background: var(--fg-tint); color: var(--blue); font-size: 19px; line-height: 1; animation: fg-bob 3.6s ease-in-out infinite; transform: rotate(var(--fg-r)); }
.fg-virtue-gold { --fg-tint: var(--gold-light); --fg-r: -6deg; }
.fg-virtue-coral { --fg-tint: #fdd9cd; --fg-r: 5deg; }
.fg-virtue-green { --fg-tint: #d7e7de; --fg-r: -4deg; }
.fg-virtue-coral .fg-virtue-icon { animation-duration: 4s; animation-delay: .3s; }
.fg-virtue-green .fg-virtue-icon { animation-duration: 4.4s; animation-delay: .6s; }
.fg-virtue > span:not(.fg-virtue-icon) { flex: 1; min-width: 0; }
.fg-virtue strong { display: block; font-size: 15px; font-weight: 700; }
.fg-virtue small { display: block; margin-top: 3px; color: rgba(255,253,247,.75); font-size: 13px; line-height: 1.5; text-wrap: pretty; }

/* One column below the design's own 460px minimum: the editions drop UNDER the claim
   rather than beside it, and they keep their row so the three formats are still read
   against each other. */
@media (max-width: 1040px) {
  .fg-split { grid-template-columns: minmax(0, 1fr); gap: 28px; }
  .fg-say, .fg-virtues, .fg-editions { grid-column: 1; }
  .fg-editions { grid-row: auto; margin-top: 6px; }
}
/* The row keeps all three formats side by side at every width — that is what makes
   them readable against each other — so the books get SMALLER rather than wrapping
   or being cropped. One number a step; the phone sizes are what a 320px screen can
   hold with the hardcover's sticker still inside the shell. */
@media (max-width: 760px) {
  .fg-editions { --fg-scale: .8; gap: 16px; }
  .fg-price { font-size: 21px; }
}
@media (max-width: 470px) {
  .fg-editions { --fg-scale: .68; gap: 14px; }
}
@media (max-width: 360px) {
  .fg-editions { --fg-scale: .55; gap: 10px; }
}

.reviews-section { padding: 108px 0 120px; background: var(--cream); }
.review-grid { display: grid; grid-template-columns: repeat(3, 1fr); align-items: center; gap: 23px; }
.review-card { min-height: 295px; display: flex; flex-direction: column; padding: 34px 30px 28px; margin: 0; border-radius: 13px 18px 14px 20px; background: var(--paper); box-shadow: 0 12px 30px rgba(41,56,60,.07); }
.review-card.featured { min-height: 330px; position: relative; padding-top: 53px; background: var(--blue); color: white; transform: rotate(-1deg); }
.review-card blockquote { margin: 18px 0 25px; font-family: var(--display); font-size: 17px; line-height: 1.55; }
.review-card figcaption { display: flex; align-items: center; gap: 11px; margin-top: auto; }
.review-card figcaption > span { width: 37px; height: 37px; display: grid; place-items: center; border-radius: 50%; background: #e6d9c2; font-size: 9px; font-weight: 900; }
.review-card figcaption div { display: flex; flex-direction: column; gap: 3px; }
.review-card figcaption strong { font-size: 10px; }
.review-card figcaption small { color: #879398; font-size: 8px; }
.featured figcaption > span { color: var(--ink); background: var(--gold-light); }
.featured figcaption small { color: #9fb4bb; }
.quote-mark { position: absolute; top: 21px; right: 27px; color: rgba(255,255,255,.12); font-family: var(--display); font-size: 70px; line-height: 1; }

/* ── FAQ: cards, topic chips and the owl ("FAQ Playful.dc.html") ───────────
   The old rule-separated <summary> list is gone; a question is a card whose
   tint comes from its .tint-* class (icon tile, open border, chevron fill), so
   nothing here is computed in JS the way the design's component does it. */
/* overflow-x: clip, not hidden — `hidden` makes the section a scroll container and
   the sticky intro inside it then has nothing to stick to (same fix as .pricing-section). */
.faq-section { position: relative; overflow-x: clip; padding: var(--band-pad) 0; background: var(--band-faq); }
.faq-twinkle { position: absolute; pointer-events: none; animation: faq-twinkle 5s ease-in-out infinite; }
.faq-twinkle.t1 { top: 80px; left: 6%; color: rgba(197,138,26,.45); font-size: 22px; }
.faq-twinkle.t2 { top: 230px; right: 7%; color: rgba(235,117,91,.5); font-size: 15px; animation-duration: 4.2s; animation-delay: .9s; }
.faq-twinkle.t3 { bottom: 150px; left: 11%; color: rgba(74,111,165,.35); font-size: 17px; animation-duration: 6s; animation-delay: 1.6s; }
@keyframes faq-twinkle { 0%, 100% { opacity: .25; transform: scale(.8); } 50% { opacity: 1; transform: scale(1.2); } }
@keyframes faq-bob { 0%, 100% { transform: translateY(0) rotate(-2deg); } 50% { transform: translateY(-8px) rotate(2deg); } }

/* The heading opens on the shared `.band-eyebrow` line, left-aligned, and the gold "?"
   tile is gone with the two-column layout: the design sets "FAQs" plain, and a 66px tile
   beside one word is the loudest thing in a band whose job is to answer quietly.
   The section is NARROW here (900px, not the full shell) — one column of questions reads
   as a list, and the dashed rule stops at the same edge the cards do rather than running
   out over an empty second column. */
.faq-section .section-shell { max-width: 900px; }
.faq-kicker { display: inline-block; flex: none; color: #a4906f; font-size: 11px; font-weight: 800; letter-spacing: 2.2px; }
.faq-heading { max-width: none; margin: 0 0 22px; text-align: left; }
.faq-heading h2 { margin: 12px 0 0; font-size: clamp(30px, 3.6vw, 40px); line-height: 1.1; }
.faq-heading p { max-width: 46em; margin: 8px 0 0; color: #8a7a5f; font-family: var(--display); font-style: italic; font-size: 16.5px; line-height: 1.55; text-wrap: pretty; }

/* One strip across the foot, in the flow — the owl rides its left edge rather than
   hanging off a card's corner, since there is no second card beside it any more. */
.faq-help { display: flex; align-items: center; flex-wrap: wrap; gap: 16px; margin-top: 20px; padding: 18px 20px; border: 2px solid #ecdcbb; border-radius: 22px; background: #fdf8ec; }
.faq-help-owl { flex: none; font-size: 28px; line-height: 1; animation: faq-bob 6s ease-in-out infinite; }
.faq-help-copy { flex: 1; min-width: 220px; }
.faq-help strong { display: block; font-size: 15.5px; }
.faq-help-copy > span { display: block; margin-top: 3px; color: #8a7a5f; font-size: 13.5px; line-height: 1.5; }
/* The owl's CTA is a button now, not a mailto: it opens the contact dialog. Both selectors
   stay so the rule survives whichever element the markup carries. */
.faq-help a,
.faq-help button { flex: none; display: inline-flex; align-items: center; gap: 9px; padding: 12px 22px; border: 0; border-radius: 999px; background: var(--blue); color: #fffdf7; font: inherit; font-size: 14.5px; font-weight: 800; text-decoration: none; cursor: pointer; transition: transform .18s ease, background .15s; }
.faq-help a:hover,
.faq-help button:hover { background: #164a63; transform: translateY(-2px); }

.faq-list { display: flex; flex-direction: column; gap: 10px; margin-top: 22px; }
.faq-card { padding: 16px 18px; border: 1.5px solid #f0e4cd; border-radius: 18px; background: var(--paper); box-shadow: 0 12px 26px -28px rgba(13,47,67,.35); transition: padding .18s ease, border-color .18s ease; }
.faq-card[hidden] { display: none; }
.faq-card[open] { padding: 18px 18px 20px; border-color: var(--faq-tint-fg); box-shadow: 0 26px 46px -28px rgba(13,47,67,.55); }
.faq-card.tint-coral { --faq-tint-bg: #fbe0d8; --faq-tint-fg: #c14a2c; }
.faq-card.tint-green { --faq-tint-bg: #e2efe4; --faq-tint-fg: #3f7d5a; }
.faq-card.tint-gold { --faq-tint-bg: #f7ecc9; --faq-tint-fg: #a07d3c; }
.faq-card.tint-blue { --faq-tint-bg: #d9eced; --faq-tint-fg: #0e3d56; }
.faq-card summary { display: flex; align-items: center; gap: 11px; cursor: pointer; list-style: none; }
.faq-card summary::-webkit-details-marker { display: none; }
.faq-card summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 6px; border-radius: 18px; }
.faq-card summary strong { flex: 1; min-width: 0; font-size: 15.5px; font-weight: 800; line-height: 1.3; }
.faq-icon { flex: none; font-size: 19px; line-height: 1; }
/* The caret is DRAWN, not typed. `⌄` (U+2304) sits low in its em box in the fonts that carry
   it and falls back to something else in the fonts that do not, so `place-items: center`
   centred the LINE BOX and left the glyph visibly off-centre in the circle — and the open
   state rotates the pill 180deg, which points the same error the other way, which is where
   it shows. The glyph in the markup is hidden (font-size: 0) and the chevron is two borders
   on ::before. `place-items: center` centres that 7px BOX, not the ink inside it: the ink is
   the two edges of a square rotated 45deg, and measured against the circle its weight lands
   ~2px high, so the nudge is what centres it optically. The nudge lives in the CHILD's own
   frame, before the parent's rotation, so the open state carries it round and the caret is
   centred pointing either way (measured: 276.2 vs a 276.5 circle centre closed, 537.6 vs
   537.5 open). Changing the 7px box or the 2px borders moves that figure. */
.faq-chev { width: 26px; height: 26px; flex: none; display: grid; place-items: center; border-radius: 50%; background: #f3e9d5; color: #a4906f; font-size: 0; line-height: 1; transition: transform .22s ease, background .18s, color .18s; }
.faq-chev::before { content: ""; width: 7px; height: 7px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; border-radius: 0 0 2px 0; transform: translateY(2px) rotate(45deg); }
.faq-card[open] .faq-chev { background: var(--faq-tint-fg); color: var(--paper); transform: rotate(180deg); }
.faq-card p { margin: 11px 0 0; padding-left: 30px; max-width: none; color: #5b6f7c; font-size: 14px; line-height: 1.6; text-wrap: pretty; }



/* ---- the closing band ("Section Rhythm.dc.html", section 7) ---------------
   The coral full stop between the FAQ and the footer, restored by the section
   rhythm: the page's bands now end on a colour rather than trailing off in the
   cream they started in, and the footer's own wave takes its fill from this band
   (`--ft-wave`) so the two meet without a seam. It carries the SECOND torn wave —
   the rhythm's first seam device used a second time, at the other end of the page,
   so the run of bands opens and closes the same way.
   Three things are load-bearing. (1) The wave hangs entirely above this section's
   padding box, so the section may not be `overflow: hidden`; nothing here needs
   clipping, so it carries no overflow at all. (2) The fill is `var(--band-final)`,
   the same token the background is — one value, so the seam cannot drift off the
   band it belongs to. (3) The CTA carries a bare `data-open-creator` beside its
   `#creator` href and rides the delegate at the foot of app.js, exactly as the FAQ
   strip and the shop window's closing button do: logged out that opens the register
   modal, like every other "Create a book" on the page. The classes are `fc-`prefixed
   and this section declares no keyframes — a duplicate `@keyframes` name in this
   sheet is last-wins with no warning, and it needs none. */
.final-cta { position: relative; padding: var(--band-pad) 30px; background: var(--band-final); color: #fffdf7; text-align: center; }
.fc-seam { position: absolute; left: 0; top: 1px; display: block; width: 100%; height: 38px; transform: translateY(-100%); }
.fc-seam path { fill: var(--band-final); }
.fc-inner { position: relative; max-width: 660px; margin: 0 auto; }
.fc-eyebrow { display: block; color: rgba(255,253,247,.85); font-size: 11px; font-weight: 800; letter-spacing: 2.2px; text-transform: uppercase; }
.final-cta h2 { margin: 12px 0 0; font-family: var(--display); font-size: clamp(30px, 4vw, 40px); font-weight: 400; line-height: 1.08; letter-spacing: -.6px; text-wrap: balance; }
.final-cta p { margin: 10px 0 0; color: rgba(255,253,247,.85); font-size: 16.5px; line-height: 1.55; text-wrap: pretty; }
.fc-btn { display: inline-flex; align-items: center; gap: 10px; margin-top: 20px; padding: 16px 30px; border-radius: 999px; background: var(--blue); color: #fffdf7; font-size: 16px; font-weight: 800; text-decoration: none; transition: transform .2s cubic-bezier(.34,1.56,.64,1), background .15s; }
.fc-btn:hover { transform: translateY(-3px) rotate(-1.5deg); background: #164a63; }
.fc-btn span { opacity: .7; }
.fc-note { display: block; margin-top: 12px; color: rgba(255,253,247,.8); font-size: 12.5px; }


/* ---- the site footer: the "Footer Playful" Claude Design ------------------
   Everything hangs off `.site-footer`, never the bare `footer` element: the legal
   pages (/privacy, /terms) have a shell with a footer of their own, and the bare
   rule below is what dresses it. The design's
   `sc-for` loops are static markup here (four socials, two columns, three legal
   pills) and its `style-hover=` attributes are real `:hover` rules.
   A column's colour is one custom property, `--ft-accent`, set on the column and
   read by both the heading swash and the link dots — the same call the FAQ port
   made with `.tint-*`, rather than the design's per-item inline styles.
   The wave is filled from `--ft-wave` because it has to match whatever sits
   ABOVE the footer, and the two pages differ: `/shelf` inlines its own cream on the
   element, and here it is the coral `.final-cta` — the closing band the section
   rhythm restored to this slot. Change what lands above the footer and this value
   moves with it, or a seam runs the width of the page. */
footer { color: #d8e2e3; background: #082737; }
@keyframes ft-twinkle { 0%, 100% { opacity: .3; transform: scale(.8); } 50% { opacity: 1; transform: scale(1.15); } }
@keyframes ft-bob { 0%, 100% { transform: translateY(0) rotate(-3deg); } 50% { transform: translateY(-7px) rotate(3deg); } }
.site-footer { position: relative; overflow: hidden; padding: 96px 30px 0; color: #fdf6e6; background: #0b2f42; --ft-wave: var(--band-final); }
.ft-wave { position: absolute; top: -1px; left: 0; display: block; width: 100%; height: 56px; }
.ft-wave path { fill: var(--ft-wave); }
.ft-star { position: absolute; pointer-events: none; animation: ft-twinkle 5s ease-in-out infinite; }
.ft-star-one { top: 62px; left: 2.5%; color: rgba(233,169,43,.5); font-size: 15px; }
.ft-star-two { top: 68px; right: 3%; color: rgba(235,117,91,.5); font-size: 12px; animation-duration: 4.2s; animation-delay: .8s; }
.ft-star-three { bottom: 96px; left: 2%; color: rgba(253,246,230,.3); font-size: 13px; animation-duration: 6s; animation-delay: 1.5s; }
.footer-main { position: relative; z-index: 1; max-width: 1280px; margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(232px, 1fr)); gap: 40px; padding-bottom: 44px; }
.footer-brand { min-width: 0; }
.brand-light { color: #fdf6e6; }
.site-footer .brand-mark { animation: ft-bob 7s ease-in-out infinite; }
/* the middle spine is `var(--blue)` — all but invisible on the navy footer */
.site-footer .brand-mark i:nth-child(2) { background: #fdf6e6; }
.footer-brand p { margin: 20px 0 0; color: #fdf6e6; font-family: var(--display); font-size: 23px; line-height: 1.35; }
.ft-socials { display: flex; gap: 9px; margin-top: 22px; }
.ft-social { width: 42px; height: 42px; display: grid; place-items: center; border: 1px solid rgba(253,246,230,.2); border-radius: 14px; background: rgba(253,246,230,.09); font-size: 16px; text-decoration: none; transition: transform .18s cubic-bezier(.34,1.56,.64,1), background .18s; }
.ft-social:hover { transform: translateY(-4px) rotate(-6deg); background: rgba(253,246,230,.18); }
.footer-links { min-width: 0; }
.ft-col-explore { --ft-accent: var(--coral); }
.ft-col-help { --ft-accent: var(--gold); }
.ft-col-title { position: relative; display: inline-block; padding-bottom: 9px; font-family: var(--display); font-size: 19px; }
.ft-col-title::after { content: ""; position: absolute; left: 0; right: 2px; bottom: 2px; height: 5px; border-radius: 6px; background: var(--ft-accent); opacity: .75; transform: rotate(-1.2deg); }
.ft-col-list { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; margin-top: 12px; }
/* "Request a feature" is a button (it opens a dialog) sitting in a column of
   anchors — style both or it renders as an unstyled control beside them. */
.footer-links a, .footer-links button { display: inline-flex; align-items: center; gap: 8px; padding: 6px 0; border: 0; background: transparent; color: rgba(253,246,230,.72); font: inherit; font-size: 14.5px; text-align: left; text-decoration: none; cursor: pointer; transition: transform .18s cubic-bezier(.34,1.56,.64,1), color .15s; }
.footer-links a:hover, .footer-links button:hover { transform: translateX(4px); color: #fdf6e6; }
.ft-dot { width: 5px; height: 5px; flex: none; border-radius: 50%; background: var(--ft-accent); opacity: .8; }
.ft-link-label { white-space: nowrap; }
/* The sign-up is a slim CAPSULE and carries NO CARD — no border, no panel, no
   fill around the group. It was a bordered 22px-radius panel holding a 44px boxed
   input, which read as the heaviest thing in the footer; the design's revision
   replaces it with a 300px cream pill and a 30px round send button, and says in
   as many words not to reintroduce the panel. Two things follow from that.
   (1) The column's spacing is the flex `gap` alone, so every child states
   `margin: 0` — a <p> left to the UA sheet brings ~1em top AND bottom and the
   rhythm doubles with nothing in the rule to explain it.
   (2) The rest at 2px `transparent` is load-bearing: the focus ring is a
   border-COLOUR change, so the capsule cannot grow 4px and shove the column
   down when the input is tabbed into.
   `.ft-news-icon` is the bare bobbing envelope now, not the 38px gold tile it
   used to name — the design's heading is one line with no icon plate. */
.newsletter { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 9px; }
.ft-news-head { display: flex; align-items: baseline; gap: 8px; }
.ft-news-icon { flex: none; color: var(--gold); font-size: 15px; line-height: 1; animation: ft-bob 3.4s ease-in-out infinite; }
.newsletter strong { font-family: var(--display); font-size: 19px; font-weight: 400; line-height: 1.2; }
.newsletter p { max-width: 26em; margin: 0; color: rgba(253,246,230,.7); font-size: 13.5px; line-height: 1.5; }
/* The design's sign-up is a plain <div>; it stays a <form id="newsletterForm">
   because app.js binds that id UNGUARDED — losing it throws at load and every
   listener registered after it silently never runs. Its focus ring is
   :focus-within, which is the design's `focus` state flag without the state. */
.newsletter form { width: 100%; max-width: 300px; display: flex; align-items: center; gap: 6px; margin: 0; padding: 4px; border: 2px solid transparent; border-radius: 999px; background: #fffdf7; }
.newsletter form:focus-within { border-color: var(--gold); }
.newsletter input { flex: 1; min-width: 0; padding: 0 2px 0 8px; border: 0; outline: 0; color: var(--ink); background: none; font-size: 13.5px; }
.newsletter button { width: 30px; height: 30px; flex: none; display: grid; place-items: center; border: 0; border-radius: 999px; color: #5d420a; background: var(--gold); font-size: 14px; font-weight: 800; cursor: pointer; transition: transform .18s cubic-bezier(.34,1.56,.64,1), background .18s; }
.newsletter button:hover { transform: translateX(2px) rotate(-12deg); background: var(--gold-light); }
.ft-news-note { display: block; margin: 0; color: rgba(253,246,230,.5); font-size: 11.5px; }
.footer-bottom { position: relative; max-width: 1280px; margin: 0 auto; display: flex; align-items: center; gap: 18px; flex-wrap: wrap; border-top: 1px solid rgba(253,246,230,.14); padding: 20px 0 26px; color: rgba(253,246,230,.6); font-size: 13px; }
.ft-legal { display: flex; align-items: center; gap: 6px; margin-left: auto; flex-wrap: wrap; }
/* All three legal pills are anchors now (the terms modal is the /terms page), but the
   rule stays written for a button too: `.footer-legal` still carries its own small
   sizing, and a pill that reverted to it would sit at 8px beside a 13px anchor. */
.site-footer .footer-legal { padding: 7px 12px; border: 0; border-radius: 999px; color: rgba(253,246,230,.6); background: transparent; font: inherit; font-size: 13px; text-decoration: none; cursor: pointer; transition: background .16s, color .16s; }
.site-footer .footer-legal:hover { color: #fdf6e6; background: rgba(253,246,230,.1); }

dialog { padding: 0; border: 0; outline: 0; color: var(--ink); background: var(--paper); box-shadow: 0 35px 90px rgba(0,0,0,.35); }
dialog::backdrop { background: rgba(5,26,37,.76); backdrop-filter: blur(4px); }
/* `place-items: center` centres the LINE BOX, and the × is not centred inside its own
   line box: U+00D7 is drawn on the font's math axis, roughly half a cap-height above the
   baseline, which lands its ink about .06em ABOVE the em box's middle. In a 36px circle
   that reads as an × sitting high — visible the moment a hover paints the circle behind
   it. The `padding-top` pushes the content box down by half of itself and cancels it; it
   is in `em` so every size below (21px, 19px, 40px, 42px, 44px) inherits the same optical
   correction rather than a pixel that is only right at 24px. Horizontal needs nothing —
   the glyph's ink is centred on its advance width. */
.dialog-close { width: 36px; height: 36px; position: absolute; z-index: 8; top: 14px; right: 14px; display: grid; place-items: center; padding: .13em 0 0; border: 0; border-radius: 50%; color: var(--ink); background: rgba(255,255,255,.88); font-size: 24px; line-height: 1; cursor: pointer; }
/* A close button whose ✕ is an SVG rather than the `×` glyph. The optical nudge above is a
   correction for one font's placement of U+00D7 and is wrong for another — a drawing needs
   none of it, so it is taken back off here rather than being made a per-font guess. */
.dialog-close.is-mark { padding: 0; }
.dialog-close.is-mark svg { display: block; }
.example-dialog { width: min(900px, calc(100% - 30px)); border-radius: 14px; }
.example-spread { min-height: 530px; display: grid; grid-template-columns: 1.05fr .95fr; }
.example-page { min-width: 0; }
.example-art img { width: 100%; height: 100%; object-fit: cover; }
.example-copy { display: flex; flex-direction: column; justify-content: center; padding: 60px; background: #f5ead3; }
.example-copy > span { color: var(--coral); font-size: 9px; font-weight: 900; letter-spacing: 1.8px; }
.example-copy h3 { margin: 15px 0 20px; font-family: var(--display); font-size: 36px; line-height: 1.05; font-weight: 500; }
.example-copy p { margin: 0; font-family: var(--display); font-size: 17px; line-height: 1.8; }
.example-copy small { margin-top: 35px; color: #8b8c83; font-size: 9px; line-height: 1.5; }
.story-dialog { width: min(1120px, calc(100% - 28px)); max-height: min(94vh, 900px); overflow: auto; border-radius: 18px 26px 18px 24px; background: #eee6d6; }
.story-dialog-header { min-height: 94px; display: flex; align-items: center; justify-content: space-between; gap: 30px; padding: 24px 58px 20px 34px; background: var(--paper); border-bottom: 1px solid #d8d0c0; }
.story-dialog-header h2 { margin: 5px 0 3px; font-family: var(--display); font-size: 28px; line-height: 1; font-weight: 500; letter-spacing: -1px; }
.story-dialog-header p { margin: 0; color: #7d898c; font-size: 9px; }
.fullscreen-toggle-btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 13px; border: 1px solid #d8d0c0; border-radius: 20px; background: var(--paper); color: var(--ink); font-size: 8px; font-weight: 900; letter-spacing: .5px; text-transform: uppercase; cursor: pointer; transition: all .2s; }
.fullscreen-toggle-btn:hover { background: #e8ded0; border-color: #b8b0a0; transform: translateY(-1px); }
.fullscreen-toggle-btn .fs-icon { font-size: 11px; }

/* This block is DESKTOP-ONLY, and the gate is the exact complement of the phone
   sheet's own (`(max-width: 760px), (max-width: 900px) and (max-height: 500px)`):
   `!(A or B)` is `(min-width: 761px) and (min-height: 501px), (min-width: 901px)`.
   It has to be gated here rather than answered down there, because `openStoryPreview`
   puts `.fullscreen` on the dialog on EVERY open (phone included) and these rules
   carry `!important` — they outrank a phone sheet that carries none, whatever its
   source order, so every book on a phone rendered in this desktop flex column
   instead of the grid sheet, and native fullscreen from picture-only mode
   (`requestFullscreen`, so `:fullscreen` matches there too) did the same. Gating
   the block is also what keeps the breakpoint OUT of app.js — reader.md item 12. */
@media (min-width: 761px) and (min-height: 501px), (min-width: 901px) {
.story-dialog:fullscreen, .story-dialog.fullscreen { width: 100vw !important; height: 100vh !important; max-width: 100vw !important; max-height: 100vh !important; border-radius: 0 !important; margin: 0 !important; padding: 0 !important; background: #eae1d1 !important; display: flex !important; flex-direction: column !important; overflow: hidden !important; }
/* Fullscreen: keep the spread a book-shaped card centered in the band, not
   stretched full-bleed. Full-bleed on a wide monitor blows each page huge and
   floats short copy as a lonely island — so it keeps its 8:5 book shape and is
   centred, rather than taking the whole screen. */
/* `align-content: center` is gone with the fixed page height it went with: a
   centred grid row sizes to its content, and the pages now ask for 100% of a
   row that has to STRETCH for that to mean anything. */
.story-dialog:fullscreen .story-reader, .story-dialog.fullscreen .story-reader { flex: 1 1 0%; min-height: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-self: center; aspect-ratio: 8 / 5; height: auto; width: auto; max-width: calc(100% - 40px); margin: 16px auto 14px; background: transparent; box-shadow: none; filter: drop-shadow(0 20px 40px rgba(40,25,5,.32)); }
/* Fullscreen fills the band the same way the windowed reader does — the old
   `min(78vh, 728px)` left a third of a 1080p screen as empty backdrop. `100%`
   of a stretched grid row, the same as the windowed reader; `!important`
   because that is what the rule it replaces carried, and the late reader block
   at the foot of this file outranks neither. */
.story-dialog:fullscreen .story-art-page, .story-dialog:fullscreen .story-copy-page, .story-dialog.fullscreen .story-art-page, .story-dialog.fullscreen .story-copy-page { height: 100% !important; max-height: none !important; }
.story-dialog:fullscreen .story-page-copy h3, .story-dialog.fullscreen .story-page-copy h3 { font-size: clamp(32px, 3.2vw, 48px); }
.story-dialog:fullscreen .story-page-copy p, .story-dialog.fullscreen .story-page-copy p { font-size: clamp(17px, 1.7vw, 23px); line-height: 1.85; }
}

.preview-before-payment { display: inline-flex; align-items: center; gap: 7px; padding: 8px 11px; border-radius: 20px; color: #557160; background: #e7f2e9; font-size: 8px; font-weight: 900; white-space: nowrap; }
.preview-before-payment span { width: 17px; height: 17px; display: grid; place-items: center; border-radius: 50%; color: white; background: #579068; font-size: 8px; }
.story-reader { min-height: 645px; display: grid; grid-template-columns: 1fr 1fr; padding: 22px 32px 12px; filter: drop-shadow(0 18px 22px rgba(51, 45, 34, .14)); }
/* Fixed height (not min-height): every spread must be exactly the same size,
   otherwise the book visibly grows/shrinks between the cover and story pages. */
.story-art-page, .story-copy-page { min-width: 0; height: 600px; position: relative; overflow: hidden; }
/* Outer corners square (radius 0): any rounding on the dark art page reveals a
   light triangle of dialog background at the top-left/bottom-left corners. */
.story-art-page { border-radius: 0; background: var(--blue); }
.story-art-page::after { content: ""; position: absolute; z-index: 4; top: 0; right: -1px; bottom: 0; width: 12px; background: linear-gradient(90deg, transparent, rgba(18,31,34,.2)); }
.story-art-page > img { width: 100%; height: 100%; object-fit: cover; object-position: 52% center; transition: filter .3s, transform .4s, object-position .4s; }
.story-art-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(7,36,51,.08), rgba(7,31,44,.28)); transition: background .3s; }
.story-scene-badge { position: absolute; z-index: 3; left: 24px; bottom: 21px; display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: 20px; color: white; background: rgba(6,31,44,.72); backdrop-filter: blur(7px); }
.story-scene-badge span { color: var(--gold-light); font-size: 15px; }
.story-scene-badge small { font-size: 7px; letter-spacing: 1.2px; font-weight: 900; }
.story-cover-copy { position: absolute; z-index: 3; inset: 45px 35px auto; display: flex; flex-direction: column; align-items: center; color: white; text-align: center; text-shadow: 0 3px 12px rgba(0,0,0,.46); }
.story-cover-copy > span { color: var(--gold-light); font-size: 7px; letter-spacing: 2px; font-weight: 900; }
.story-cover-copy strong { max-width: 370px; margin-top: 14px; font-family: var(--display); font-size: clamp(36px, 4vw, 54px); line-height: .95; font-weight: 500; }
.story-cover-copy small { margin-top: 12px; font-family: var(--display); font-size: 12px; font-style: italic; }
.story-copy-page { display: flex; flex-direction: column; padding: 31px 38px 24px 45px; border-radius: 0; color: #314b57; background: #fffdf7; box-shadow: inset 10px 0 18px rgba(43,42,35,.07); overflow-y: auto; }
.story-copy-page::after { content: ""; position: absolute; inset: 0; opacity: .22; pointer-events: none; background-image: radial-gradient(#a69b85 .45px, transparent .45px); background-size: 5px 5px; }
.story-page-topline { position: relative; z-index: 1; display: flex; align-items: center; justify-content: space-between; color: #a19b8c; text-transform: uppercase; letter-spacing: 1.5px; font-size: 7px; font-weight: 900; }
.tiny-stars { color: var(--gold); }
.story-page-copy { position: relative; z-index: 1; max-width: 430px; margin: auto; }
.story-page-copy h3 { margin: 13px 0 18px; font-family: var(--display); font-size: clamp(27px, 3vw, 39px); line-height: 1.07; font-weight: 500; letter-spacing: -1px; }
.story-page-copy p { margin: 0; color: #4b5e64; font-family: var(--display); font-size: clamp(15px, 1.6vw, 19px); line-height: 1.82; }
.story-reader-footer { min-height: 49px; display: flex; align-items: center; justify-content: center; gap: 26px; padding: 0 32px 10px; }
.story-nav-button { min-width: 92px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 8px; border: 0; color: var(--ink); background: transparent; font-size: 9px; font-weight: 900; cursor: pointer; }
.story-nav-button:disabled { opacity: .3; cursor: default; }
.story-progress-dots { display: flex; align-items: center; justify-content: center; gap: 5px; }
.story-progress-dots button { width: 6px; height: 6px; padding: 0; border: 0; border-radius: 50%; background: #b6ae9f; cursor: pointer; transition: .2s; }
.story-progress-dots button.active { width: 19px; border-radius: 6px; background: var(--coral); }
.story-approval-bar { min-height: 72px; display: grid; grid-template-columns: auto auto 1fr auto; align-items: center; gap: 18px; padding: 12px 30px; border-top: 1px solid #d8d0c0; background: var(--paper); }
.story-approval-bar p { display: flex; align-items: center; gap: 7px; margin: 0; color: #65777d; font-size: 8px; white-space: nowrap; }
.story-approval-bar p span { width: 18px; height: 18px; display: grid; place-items: center; border-radius: 50%; color: white; background: #579068; font-size: 8px; }
.story-approval-bar .button { justify-self: end; min-height: 45px; padding-inline: 18px; gap: 14px; font-size: 9px; }
.edit-story-button { justify-self: start; padding: 10px 0; border: 0; color: #61757d; background: transparent; font-size: 9px; font-weight: 900; cursor: pointer; }
.edit-story-button:hover { color: var(--coral); }
.order-dialog { width: min(760px, calc(100% - 30px)); padding: 44px 50px 50px; border-radius: 17px 23px 18px 22px; }
.order-heading { text-align: center; }
.order-icon { width: 45px; height: 45px; display: grid; place-items: center; margin: 0 auto 13px; border-radius: 50%; color: var(--coral); background: #f8ded5; }
.order-heading h2 { margin: 8px 0 7px; font-family: var(--display); font-size: 38px; font-weight: 500; }
.order-heading p { margin: 0; color: #7c8b90; font-size: 10px; }
.order-content { display: grid; grid-template-columns: 220px 1fr; gap: 30px; margin-top: 32px; }
.order-thumb { height: 280px; position: relative; overflow: hidden; border-radius: 4px 11px 11px 4px; box-shadow: 9px 14px 25px rgba(13,47,67,.2); }
.order-thumb img { width: 100%; height: 100%; object-fit: cover; filter: brightness(.72) saturate(.9); }
.order-thumb div { position: absolute; inset: 25px 18px auto; color: white; text-align: center; text-shadow: 0 2px 8px rgba(0,0,0,.5); }
.order-thumb span { color: var(--gold-light); font-size: 6px; letter-spacing: 1.4px; }
.order-thumb strong { display: block; margin-top: 8px; font-family: var(--display); font-size: 23px; line-height: 1; font-weight: 500; }
.order-summary { display: flex; flex-direction: column; }
.order-summary > div { min-height: 44px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #e1ddd3; font-size: 10px; }
.order-summary > div span { color: #7c888c; }
.order-summary > div strong { font-size: 10px; }
.order-summary .order-total { min-height: 58px; border-bottom: 0; font-family: var(--display); font-size: 16px; }
.order-summary .order-total span { color: var(--ink); }
.order-summary .order-total strong { font-family: var(--display); font-size: 21px; }
.order-summary .button { width: 100%; min-height: 47px; font-size: 10px; }
.order-summary > small { margin-top: 9px; color: #98a2a3; text-align: center; font-size: 7px; }
/* The book order form and its success panel are gone from this sheet: checkout is
   /checkout, a page of its own with its own styles. The membership modal that inherited
   these rules is now the "Subscribe Modal" design and wears `.sub-*` instead, so
   .order-dialog / .order-heading / .order-summary above are ORPHANS — left in place
   deliberately rather than swept out alongside a markup change, since .order-thumb is
   still a live book-object mock and untangling which of these the /checkout page's own
   sheet leans on is its own pass with its own grep. `.pay-methods` is NOT an orphan:
   `loadPayMethods` renders the gateway picker into it inside the new modal. */

.header-account { display: flex; align-items: center; gap: 14px; flex: none; }

/* The country picker, left of "Log in". It rides in the header rather than above the
   price list because it states a fact about the visitor — it answers what a printed book
   costs AND whether one can be posted to them — not because it is a pricing control.
   It is `hidden` until app.js has both a country list and a country to show — the
   attribute is on the SHELL now (the pill is a <button>, which cannot hold the drop
   panel), and `display: inline-flex` is precisely what an attribute cannot beat, so the
   sheet says so.

   Two things are load-bearing here. (1) `#countryPicker` is the positioning context for
   the `.cs-panel` app.js hangs off it, so it is `position: relative` and the pill is not.
   (2) The `<select>` is CLIPPED, not `display: none` — it is still the value every pick is
   written into, and the register field's own reason applies here too: parked at the foot
   of the pill, anything the browser wants to point at the control points at the pill. It
   is clipped UNCONDITIONALLY, with no `.cs-on` state and no zero-opacity overlay left, and
   that is deliberate: there is no visible-but-unmounted pill to fall back for. The shell
   is `hidden` until `syncHeaderCountry` has a country list, and the only thing that fills
   that list is `ensureCountryOptions`, which mounts the panel on the very next line — no
   list means a hidden pill, a list means a mounted one. */
.country-picker-shell { position: relative; display: inline-flex; flex: none; }
#countryPicker[hidden] { display: none; }
.country-picker { display: inline-flex; align-items: center; gap: 7px; padding: 7px 11px; border: 1.5px solid #e4d7ba; border-radius: 999px; background: rgba(255, 255, 255, .5); color: var(--ink); font: inherit; text-align: left; cursor: pointer; transition: border-color .18s, background .18s, box-shadow .15s; }
.country-picker:hover { border-color: var(--blue); background: rgba(255, 255, 255, .82); }
.country-picker.is-open { border-color: var(--gold); background: #fff; box-shadow: 0 0 0 4px rgba(233, 169, 43, .16); }
.country-picker:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
#countryPicker > select { position: absolute; left: 14px; bottom: 0; width: 1px; height: 1px; padding: 0; border: 0; opacity: 0; pointer-events: none; }
.country-picker-flag { flex: none; font-size: 15px; line-height: 1; }
/* The ISO code, so no ellipsis and no max-width: two letters fit everywhere, which is the
   whole reason the pill stopped carrying the country's name. */
.country-picker-name { color: var(--ink-soft); font-size: 13px; font-weight: 700; letter-spacing: .02em; }
/* The drawer's two spans, off everywhere but the drawer band at the foot of this file.
   They are NOT `.country-picker-name` with an override, though the design draws them as
   one: the 760 band caps that class at `max-width: 116px`, which is a sentence clipped
   mid-word at every width the drawer exists at — and separate classes make the hide a
   plain `display: none` here rather than a third undo down there. */
.country-picker-line, .country-picker-change { display: none; }
/* A fixed 12px box holding an SVG, not a glyph with a baseline nudge: the `⌄` character's
   ink sits low in its em box, so the -4px it used to need flips the wrong way the moment
   the mark is rotated. `transform-origin: 50% 50%` is what makes it pivot in place. */
.country-picker-caret { flex: none; display: block; width: 12px; height: 12px; color: #9a8c72; transform-origin: 50% 50%; transition: transform .2s; }
.country-picker-caret svg { display: block; width: 100%; height: 100%; }
.country-picker.is-open .country-picker-caret { transform: rotate(180deg); }

.account-link { position: relative; border: 0; background: transparent; font-size: 15px; font-weight: 700; white-space: nowrap; cursor: pointer; transition: transform .2s, background .2s, border-color .2s, color .15s; }
/* `#openRegister` declares a `display` (and the drawer gives one to `#openLogin` too), so
   `[hidden]` has to be said here or the UA's own `[hidden] { display: none }` — a lower
   origin than any author rule — loses to it and a signed-in member is offered Register for
   ever. `updateAccountUI` sets `.hidden` on both in both directions and always has; nothing
   else was hiding them. The `.acc[hidden]` / `.addon-card[hidden]` call, a fourth time. */
.account-link[hidden] { display: none; }

/* logged-out: outlined "Log in" + a filled "Register" pill (green since 2026-08-17 — see
   the note over `.button-coral`; it read "coral" here until then) */
#openLogin { padding: 11px 20px; color: var(--blue); border: 1.5px solid #cdbf9f; border-radius: 999px; }
#openLogin:hover { border-color: var(--blue); background: rgba(255, 255, 255, .5); }
#openRegister { display: inline-flex; align-items: center; gap: 9px; padding: 12px 22px; color: #fff; background: var(--green); border-radius: 999px; box-shadow: 0 12px 24px -10px rgba(47, 82, 68, .75); }
#openRegister:hover { background: var(--green-dark); transform: translateY(-1px); }

/* logged-in: "My books" as a bar control (2026-08-25, at the owner's request) — the row
   that sat in the chip's menu, standing between the country pill and the chip itself.

   Four things are load-bearing. (1) It declares a `display`, so `.header-books[hidden]` has
   to be said here or the shelf is offered to a logged-out visitor — the `.acc[hidden]` /
   `.plan-meter[hidden]` / `.addon-card[hidden]` call, a fifth time. (2) The LABEL is
   `display: none` HERE and comes back at both ends — inside the drawer, where the same node
   is a full-width row with room for its words, and on a full desktop from 1301 up (the block
   below), where it is the icon in front of the words; what is left in the middle is the
   compact band, where the circle is all the bar has room for. The accessible name is the
   button's own `aria-label` at every width, because a name assembled out of a span the sheet hides is a
   name that depends on a media query — the trap `#registerCountryField` and the country
   pill are both commented for. (3) It is a 40px circle in this base rule, not a pill: at the
   widths this rule is all there is — the compact band and the studio's row — the bar already
   carries a pill (the meter) and a pill-shaped chip, and a third rounded box between them
   reads as a third statement rather than a control. From 1301 there is room for the words and
   it IS a pill. (4) The hover lift matches `#openRegister`'s,
   so the two filled-ish bar controls behave the same way under the pointer. */
.header-books {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px; flex: none;
  width: 40px; height: 40px; padding: 0; color: var(--blue); cursor: pointer;
  border: 1.5px solid #e6d9bd; border-radius: 50%; background: #fffdf7;
  transition: transform .2s, background .2s, border-color .2s, color .15s;
}
.header-books[hidden] { display: none; }
.header-books svg { width: 19px; height: 19px; flex: none; }
.header-books:hover { color: var(--ink); border-color: #cdbf9f; background: #f7eeda; transform: translateY(-1px); }
.header-books .hb-label { display: none; font-size: 15px; font-weight: 700; white-space: nowrap; }

/* ...and on a FULL desktop it is the icon IN FRONT OF THE WORDS (2026-08-25, at the owner's
   request): the circle above is the phone's treatment and the compact band's, this is the
   wide bar's. Three things are load-bearing. (1) **The band starts at 1301, not at 1101.**
   The 1101–1300 block below measured the signed-in bar with the ring collapsed to its bare
   dial and found ~3px of slack before the chip cleared the header's rounded edge; the words
   are ~72px in English and longer in nine other catalogs ("Meine Bücher"), so a pill in that
   band is the wordmark painted over again — the failure `fragments/memberbar`'s ≤340 block
   exists for. 1301 is the width the compact band itself already names as "enough room".
   (2) **The accessible name does not change with the viewport.** `aria-label` stays on the
   button at every width and the span is `aria-hidden`-free plain text, so a screen reader
   reads the label and not the label twice — the button's `aria-label` overrides its content
   for the name, which is exactly why the span may appear and disappear without the name
   moving. (3) The radius is the pill's, not the circle's, and the hover lift is the same
   one, so the control behaves identically in both shapes. (4) **It is scoped to
   `.header-tools`, so the STUDIO's copy stays a circle at every width.** `moveAccountToStudio`
   moves this very node out of that wrapper and into `#kc-account`, and `.kc-top` is ✕ Close ·
   five labelled stamps · the cluster — the row the ≤640 block already documents as starved by
   a fourth item. The scope is what keeps the words off it without a second `display: none`
   for `renderHeaderBooks` to fight. */
@media (min-width: 1301px) {
  .header-tools .header-books { width: auto; height: auto; padding: 10px 18px 10px 14px; border-radius: 999px; }
  .header-tools .header-books .hb-label { display: inline; }
}

/* logged-in: the account chip ("Account Chip.dc.html", option 1a) — one pill carrying the
   monogram in the PLAN's own tint, the plan as an eyebrow, the member's name, and a caret
   over a menu holding everything the ☺ ring, the loose name and the navy "My books" pill
   used to do separately, Log out included.

   Six things are load-bearing. (1) The tier is a class on the wrapper setting four custom
   properties, so the header borrows the membership cards' own colours in one place and no
   new colour is introduced for this component. (2) `.acc` declares a `display`, so
   `.acc[hidden]` has to be said or the chip renders for a logged-out visitor — the
   `.addon-card[hidden]` call. (3) The caret's 180° is stated on the element under a state
   class, never as a keyframe: the reduced-motion block kills every animation here, and a
   caret whose only turn lived in a frame would point down over an open menu. (4) The name
   is a visitor's own and has no length this bar can count on — capped and ellipsised, which
   needs `min-width: 0` on the stack, since it is a flex child and would otherwise refuse to
   shrink. (5) The eyebrow is uppercased by the SHEET, so the accessible name stays a
   readable word. (6) Focus-visible is stated: the prototype leans on the UA default, which
   a pill this shape shows badly. */
.acc { position: relative; display: inline-block; --acc-fill: #f5efe1; --acc-ring: 2px dashed #d9c9a6; --acc-ink: #8a7a5f; --acc-eyebrow: #c26a52; }
.acc[hidden] { display: none; }

/* What is LEFT of the membership's monthly books — a RING COUNTER ("Generation Credits
   Badge - B.html"), to the chip's left on this bar and, moved node and all, on the studio's
   top row. It was a flat paper pill reading "📚 3 of 10 left"; the ring is a traffic light
   instead — green while the month is untouched, gold once it is part spent, coral when it
   is gone — and the numeral in the dial carries the count with the fraction demoted to a
   subline, so the two stop competing for the same glance. It STATES the month and offers
   nothing: the coral `.pm-buy` cart that rode the spent state to `/checkout` was removed on
   2026-08-06 (markup, both collapsed treatments and `#planMeterBuy` in `renderPlanMeter`),
   so the pre-paid book road is reached from the checkout page and not from this bar. Seven
   things are load-bearing.

   (1) `.plan-meter` declares a `display`, so `.plan-meter[hidden]` has to be said (the
       `.acc[hidden]` / `.addon-card[hidden]` call) or the ring renders for a free account.
   (2) The three state classes are `is-full` / `is-part` / `is-out`, exactly one at a time,
       and `renderPlanMeter` sets all three on EVERY call: a one-way `add` leaves gold on a
       member the month after theirs rolled over, the same hazard that function already pins
       for the text and the `hidden`.
   (3) The gold arc is `--arc`, the conic-gradient stop written from `left / limit`. It is a
       presentational figure with as many values as a tier has books, so it rides an inline
       custom property rather than a class per fraction.
   (4) **`.pm-read` is the `role="img"` group, not `.plan-meter`.** It went one box in when
       the spent state still carried a cart link (a link inside `role="img"` is unreachable)
       and stays there: index, the studio's moved copy and `/shelf` all name it, so moving it
       back would be three edits in lockstep for nothing on screen.
   (5) The SUBLINE is what drops on a narrow bar, never the numeral: the dial alone still
       states the count, where "books left" on its own states nothing.
   (6) Below the studio's 640 the ring collapses to the bare 44px dial — the same tap square
       `.kc-exit` takes there. Colour is carrying the state at that size, which is why the
       traffic light is the design and not a tint.
   (7) The dial's centre is drawn by `::after`, so its fill has to MATCH the ring's own
       background in every state; a shared token there would flatten the light back out. */
.plan-meter { position: relative; display: inline-flex; align-items: center; gap: 0; flex: none; padding: 6px 16px 6px 6px; border: 1.5px solid #e6d9bd; border-radius: 999px; background: #fffdf7; line-height: 1; white-space: nowrap; }
.plan-meter[hidden] { display: none; }
.pm-read { display: inline-flex; align-items: center; gap: 11px; }
.pm-dial { position: relative; width: 34px; height: 34px; flex: none; display: grid; place-items: center; border-radius: 50%; background: #eadfc6; }
.pm-dial::after { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: #fffdf7; }
.pm-num { position: relative; z-index: 1; font-size: 13px; font-weight: 800; line-height: 1; color: var(--ink); }
.pm-txt { display: flex; flex-direction: column; line-height: 1.25; }
.pm-word { font-size: 13px; font-weight: 800; color: var(--ink); }
.pm-sub { font-size: 11px; color: #8a7a5f; }
.plan-meter.is-full { border-color: #c7ddd3; background: #f2f7f4; }
.plan-meter.is-full .pm-dial { background: var(--green); }
.plan-meter.is-full .pm-dial::after { background: #f2f7f4; }
.plan-meter.is-full .pm-num, .plan-meter.is-full .pm-word { color: #315645; }
.plan-meter.is-full .pm-sub { color: #5b7a6c; }
.plan-meter.is-part { border-color: #ecd9a8; background: #fffaef; }
.plan-meter.is-part .pm-dial { background: conic-gradient(var(--gold) 0 var(--arc, 180deg), #eadfc6 var(--arc, 180deg) 360deg); }
.plan-meter.is-part .pm-dial::after { background: #fffaef; }
.plan-meter.is-part .pm-num, .plan-meter.is-part .pm-word { color: #7a5606; }
.plan-meter.is-part .pm-sub { color: #8a7332; }
.plan-meter.is-out { border-color: #f2c2b5; background: #fdf1ee; }
.plan-meter.is-out .pm-dial { background: var(--coral-dark); }
.plan-meter.is-out .pm-dial::after { background: #fdf1ee; }
.plan-meter.is-out .pm-num, .plan-meter.is-out .pm-word { color: #b3402a; }
.plan-meter.is-out .pm-sub { color: #a8543f; }

/* The meter lives INSIDE the account menu (2026-08-25, at the owner's request) — on a phone
   and on a desktop, so there is ONE place a family looks for what the month has left. What
   went with it are every collapse rule it used to need: the 1101–1300 band that folded it to
   a bare dial, the drawer's `display: none`, and the studio's own `.kc-top` pair. All four
   existed because a BAR runs out of width; `.acc-menu` is 216px at its narrowest and has room
   for the words at every size, and a bare dial in a dropdown is a number with nothing to say
   what it counts. They are deleted rather than overridden — an undo rule per band is exactly
   the drift the 1101 comment warned about.
   It takes `.acc-row`'s radius and full width so it reads as part of the list, and keeps its
   own border and tint, which are the traffic light. It is NOT an `.acc-row` and must not
   become one: `accountMenu`'s keyboard walk enumerates that class, and a count of what is
   left is a fact about the account rather than a place to go — the same argument that took it
   out of the NAV drawer on 2026-08-06, pointed at a menu where it is finally in the right
   list. The studio gets it for free now: `moveAccountToStudio` moves `#accountChip` and the
   meter is a DESCENDANT, so it must never be appended separately again. */
.acc-menu .plan-meter { display: flex; width: 100%; margin: 0 0 4px; padding: 9px 12px; border-radius: 11px; white-space: normal; }
/* And the `[hidden]` has to be re-stated INSIDE the menu, for the reason the block at the top
   of this sheet already gives about `.plan-meter[hidden]`: the rule above declares a `display`
   at the same (0,2,0) specificity and sits LATER, so it wins, and `renderPlanMeter`'s hide
   stops working — a free account and an uncapped tier would both read "0 of 0" in the menu,
   which is the one thing every comment about this element says it must never do. */
.acc-menu .plan-meter[hidden] { display: none; }
.acc-menu .pm-sub { white-space: normal; }
.acc.acc-silver { --acc-fill: #e8eef2; --acc-ring: 2px solid #b9cdd8; --acc-ink: #3c5c6d; --acc-eyebrow: #5f7f91; }
.acc.acc-gold { --acc-fill: #fdf0cf; --acc-ring: 2px solid #e6bd3f; --acc-ink: #8a6410; --acc-eyebrow: #a07a12; }
.acc-chip { display: flex; align-items: center; gap: 11px; padding: 5px 13px 5px 6px; border: 1.5px solid #ecdcbb; border-radius: 999px; background: #fdf8ec; font-family: inherit; cursor: pointer; transition: border-color .18s, background .18s; }
.acc-chip:hover { border-color: #e6bd3f; background: #fdf1dc; }
.acc-chip:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.acc.is-open .acc-chip { border-color: #e6bd3f; }
.acc-mono { width: 36px; height: 36px; flex: none; display: grid; place-items: center; border: var(--acc-ring); border-radius: 50%; background: var(--acc-fill); color: var(--acc-ink); font-family: var(--display); font-size: 17px; line-height: 1; }
.acc-stack { min-width: 0; text-align: left; line-height: 1.15; }
.acc-eyebrow { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 9.5px; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase; color: var(--acc-eyebrow); }
.acc-name { display: block; max-width: 132px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 14.5px; font-weight: 700; color: var(--ink); }
.acc-caret { flex: none; display: grid; place-items: center; transform: rotate(0deg); transition: transform .18s; }
.acc.is-open .acc-caret { transform: rotate(180deg); }
.acc-menu { position: absolute; right: 0; top: calc(100% + 10px); z-index: 20; min-width: 216px; display: grid; gap: 2px; padding: 7px; border: 1.5px solid #ecdcbb; border-radius: 16px; background: var(--paper); box-shadow: 0 26px 46px -28px rgba(13, 47, 67, .55); }
.acc-menu[hidden] { display: none; }
.acc-row { display: block; width: 100%; padding: 9px 12px; border: 0; border-radius: 11px; background: transparent; color: var(--ink); font-family: inherit; font-size: 13.5px; font-weight: 600; text-align: left; white-space: nowrap; cursor: pointer; transition: background .18s; }
.acc-row:hover { background: #fdf8ec; }
.acc-row:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
.acc-sep { height: 1px; margin: 4px 6px; background: #f0e4cb; }
/* Share & earn ("Affiliate Placements 1a"). The design gives every row an icon; this menu
   has never had one and the three siblings still do not, so the accent is carried by the
   ONE row that is new — a gold star and the gold tint `.acc-row.is-current` already uses
   on `/shelf`. Two things are load-bearing. (1) **The star is a `::before` glyph and may
   not become markup**: this row carries `data-i18n`, and `applyI18n` writes `textContent`
   on a language switch, so an `<svg>` child would survive exactly until somebody picked
   German — the reason the drawer's tiles are `::before` data URIs. A `content` glyph is
   not a child and survives. (2) `/shelf` inlines its own copy of this rule from the same
   figures, the rule the whole `.acc*` block keeps: change one and change the other. */
.acc-refer { display: flex; align-items: center; gap: 9px; background: #fdf5e4; }
.acc-refer::before { content: "✦"; flex: none; color: #b47f12; font-size: 13px; line-height: 1; }
.acc-refer:hover { background: #fdf1dc; }
/* The design's own coral for this row and for the no-plan eyebrow, NOT `--coral-dark`
   (#d75b45): they are different values, and `/shelf` inlines its copy of this block from
   the same figure — a token here would make the two headers render different corals. */
.acc-out { color: #c26a52; }
.acc-out:hover { background: #fdeee9; }
.field-row-equal { grid-template-columns: 1fr 1fr; }
.character-builder { padding: 15px; border: 1px solid #ddd8cd; border-radius: 10px; background: #faf7ef; }
.additional-characters { display: flex; flex-direction: column; gap: 12px; }
.character-row {
  display: grid;
  grid-template-columns: 1fr 160px 44px;
  grid-template-areas: "name role remove" "gender species species" "photo photo photo" "consent consent consent";
  gap: 12px; align-items: end; padding: 15px 16px; border-radius: 13px; background: #fff;
  box-shadow: inset 0 0 0 1px #e6e1d4, 0 5px 16px rgba(45,60,70,.05);
}
.character-row label { display: flex; flex-direction: column; gap: 6px; color: #566169; font-size: 11px; font-weight: 700; letter-spacing: .01em; }
.character-row .extra-name-label { grid-area: name; }
.character-row .extra-role-label { grid-area: role; }
.character-row .extra-gender-label { grid-area: gender; }
.character-row .extra-species-label { grid-area: species; }
.character-row input, .character-row select { min-height: 44px; padding: 11px 12px; border: 1px solid #d8d5cb; border-radius: 10px; font-size: 14px; background: #fff; }
.character-row .remove-character { grid-area: remove; align-self: end; width: 44px; height: 44px; display: grid; place-items: center; border: 1px solid #eccfc8; border-radius: 10px; background: #fbe3dd; color: var(--coral-dark); font-size: 20px; line-height: 1; cursor: pointer; transition: background .15s; }
.character-row .remove-character:hover { background: #f6cfc6; }
.character-row .character-photo { grid-area: photo; display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 10px; }
/* The companion's own thumbnail, matching the child's `.upload-preview` circle above it:
   the classic form told a grown-up a photo had landed in words alone, which is the one
   thing a photo field cannot do — a wrong picture reads as "✓ Photo added" just as
   happily as the right one. Hidden until there IS one, so an empty row keeps its shape. */
.extra-photo-preview { display: none; flex: 0 0 auto; width: 48px; height: 48px; border-radius: 50%; background: #f7ddd4 center/cover no-repeat; box-shadow: 0 3px 8px -3px rgba(13,47,67,.4); }
.extra-photo-preview.has-image { display: block; }
.photo-upload-btn { display: inline-flex; align-items: center; gap: 6px; padding: 10px 15px; border-radius: 10px; background: var(--blue); color: #fff; font-weight: 800; font-size: 13px; cursor: pointer; white-space: nowrap; }
.photo-upload-btn:hover { background: #164e69; }
.photo-status { font-size: 12px; font-weight: 800; padding: 5px 11px; border-radius: 999px; }
.photo-status.ok { background: #e4f0ea; color: var(--green); }
.photo-status.warn { background: #fbe3dd; color: var(--coral-dark); }
.character-row .character-consent { grid-area: consent; flex-direction: row; align-items: flex-start; gap: 9px; color: #6d7b80; font-size: 12px; font-weight: 600; line-height: 1.4; }
.character-row .character-consent input { min-height: auto; width: 17px; height: 17px; margin-top: 1px; flex: none; }
.character-consent { grid-column: 1 / 3; flex-direction: row !important; align-items: flex-start; font-weight: 600 !important; line-height: 1.45; }
.character-consent input { min-height: 0; }
.remove-character { width: 35px; height: 35px; border: 0; border-radius: 50%; color: var(--coral); background: #f9e4df; cursor: pointer; }
.add-character-button { align-self: flex-start; padding: 9px 11px; border: 1px dashed #9dafb0; border-radius: 7px; color: var(--blue); background: white; font-size: 9px; font-weight: 900; cursor: pointer; }
.character-note { margin: 0 !important; line-height: 1.5; }
.terms-consent { display: flex; align-items: flex-start; gap: 9px; padding: 11px; margin: 4px 0 15px; border-radius: 8px; color: #60737a; background: #f2f0e9; font-size: 8px; line-height: 1.55; }
.terms-consent input { margin-top: 2px; accent-color: var(--green); }
.terms-consent button, .terms-consent a { padding: 0; border: 0; color: var(--coral); background: transparent; font-size: inherit; font-weight: 900; text-decoration: underline; cursor: pointer; }
.addon-field { gap: 8px; }
.addon-card { display: grid; grid-template-columns: auto auto 1fr auto; align-items: center; gap: 10px; padding: 11px; border: 1px solid #d8d5cb; border-radius: 8px; cursor: pointer; }
.addon-card > span { font-size: 20px; }.addon-card div { display: flex; flex-direction: column; gap: 2px; }.addon-card strong { font-size: 9px; }.addon-card small { margin: 0; font-size: 8px; }.addon-card b { color: var(--coral); font-size: 9px; }
/* The `display: grid` above beats the `hidden` attribute, so the audio add-on would still
   be on the page for a language the narrator cannot speak — the same reason .kc-star has
   its own [hidden] rule. */
.addon-card[hidden] { display: none; }
.addon-narration-note { margin: 0; padding: 9px 11px; border: 1px dashed #d8d5cb; border-radius: 8px; background: #fbf8f1; color: #7a6a52; font-size: 8px; line-height: 1.55; }
.digital-book { color: white; background: linear-gradient(145deg, #315f76, #0e3d56) !important; font-size: 24px; }
.generation-theatre { min-height: 68px; display: flex !important; justify-content: center !important; position: relative; margin-bottom: 10px; border-radius: 7px; background: linear-gradient(130deg,#e7f5f2,#fff3d4); overflow: hidden; }
.generation-theatre > span { display: inline-grid; place-items: center; font-size: 23px; animation: magicBounce 1.2s ease-in-out infinite alternate; }
.generation-theatre .magic-trail { color: var(--gold); font-size: 14px; animation: magicTwinkle .7s ease-in-out infinite alternate; }
.generation-theatre .magic-book { animation-delay: .2s; }.generation-theatre .magic-world { animation-delay: .4s; }
.idea-bubble { position: absolute; bottom: 3px; color: #628087 !important; font-size: 7px !important; }
@keyframes magicBounce { to { transform: translateY(-8px) rotate(4deg); } }
@keyframes magicTwinkle { to { opacity: .25; transform: scale(.6) rotate(45deg); } }

/* Plan cards live in the pricing row now; the block at the end of this file
   restyles them. What is left here is the shared skeleton both used. */
.plan-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 22px; align-items: center; }
.plan-card { min-height: 430px; position: relative; display: flex; flex-direction: column; align-items: center; padding: 35px 28px 28px; border: 1px solid #dfd8c8; border-radius: 16px 22px 17px 24px; background: white; box-shadow: 0 15px 35px rgba(13,47,67,.07); text-align: center; }
.plan-card.featured { min-height: 470px; padding-top: 50px; color: white; border-color: transparent; background: var(--blue); box-shadow: 0 28px 55px rgba(13,47,67,.22); }
.plan-card > small { margin-top: 12px; color: var(--coral); letter-spacing: 1.8px; font-size: 8px; font-weight: 900; }.plan-card.featured > small { color: var(--gold-light); }
.plan-card h3 { margin: 8px 0 5px; font-family: var(--display); font-size: 34px; }.plan-card > strong { font-family: var(--display); font-size: 28px; }.plan-card > strong em { color: #87999e; font-family: var(--sans); font-size: 10px; font-style: normal; }
.plan-card ul { width: 100%; display: flex; flex-direction: column; gap: 12px; padding: 23px 0; margin: 18px 0; border-top: 1px solid #e3ddd1; list-style: none; color: #65777e; text-align: left; font-size: 10px; }.plan-card.featured ul { color: #c3d3d8; border-color: #426174; }.plan-card li::before { content: "✓"; margin-right: 8px; color: var(--green); font-weight: 900; }.plan-card.featured li::before { color: var(--gold-light); }
.plan-card .button { width: 100%; margin-top: auto; color: var(--ink); background: #f3ead8; font-size: 10px; }.plan-ribbon { position: absolute; top: 14px; right: -8px; padding: 6px 12px; color: var(--ink); background: var(--gold); font-size: 8px; font-weight: 900; transform: rotate(2deg); }

.audio-player { display: flex; align-items: center; gap: 9px; padding: 9px 32px; border-bottom: 1px solid #d8d0c0; background: #f7f2e8; }
.audio-player button { padding: 8px 11px; border: 1px solid #d5ccbd; border-radius: 20px; color: var(--ink); background: white; font-size: 8px; font-weight: 900; cursor: pointer; }.audio-player label { margin-left: auto; color: #788488; font-size: 8px; }.audio-player select { padding: 5px; border: 1px solid #d5ccbd; border-radius: 5px; }.audio-player small { color: #929a99; font-size: 7px; }
.story-reader { position: relative; }.preview-watermark { position: absolute; z-index: 7; inset: 45% 4% auto; overflow: hidden; color: rgba(255,255,255,.72); text-align: center; text-shadow: 0 1px 5px rgba(0,0,0,.45); font-size: clamp(13px,2vw,22px); font-weight: 900; letter-spacing: 3px; transform: rotate(-16deg); pointer-events: none; }.story-dialog.paid .preview-watermark { display: none; }
.edit-page-button { padding: 9px 10px; border: 1px solid #e4bd55; border-radius: 7px; color: #7b5410; background: #fff3cc; font-size: 8px; font-weight: 900; cursor: pointer; }
.discount-row { color: var(--green); }.discount-row strong { color: var(--green); }

/* background: transparent — the two panels cover the whole dialog, and dialog's own
   paper background otherwise bleeds through the antialiased pixels of the rounded
   overflow clip as a white hairline down the navy panel's corners. */
/* The dialog SCROLLS at every width, not only on a phone. The register form grew a confirm
   box and a reassurance line, which took it past a 900px-tall laptop viewport — and the UA
   sheet caps a `<dialog>` at `calc(100% - 6px - 2em)` regardless, so with `overflow: hidden`
   alone the "Already have an account?" switch was not merely below the fold, it was
   unreachable. `overflow-x` stays hidden (the hairline guard below still needs it) and only
   the Y axis opens; the two are allowed to differ because neither is `visible`.
   The cap is `dvh`, the figure the phone band already argues for: a UA `100%` on a phone is
   the TALL viewport and parks the last field under the URL bar. */
.account-dialog { width: min(960px,calc(100% - 28px)); display: none; grid-template-columns: .72fr 1fr; border: 0; border-radius: 18px 24px 18px 24px; overflow: hidden; background: transparent; }.account-dialog[open] { display: grid; max-height: calc(100dvh - 28px); overflow-y: auto; overscroll-behavior: contain; }
/* Left: cosy floating-book story scene.
   It draws its OWN left corners. `.account-dialog` is `background: transparent` and leans on
   the rounded `overflow` clip, through which that transparency reads as a white hairline down
   the NAVY panel's corners — and a scrolling dialog clips the same way, so the guard has to
   be on the panel. The cream column needs no such rule: a cream-on-transparent hairline over
   a cream panel is nothing to see. The phone band restates these as TOP corners, because
   there the panel is a strip across the top rather than a column down the left. */
/* The panel's floor is the REGISTER column's own height, not a number of its own and not the
   design's literal `min-height: 0`. Both tabs live in one grid, so a floor lower than the
   taller form lets the dialog shrink when somebody taps "Log in instead" — the whole modal
   re-centres under the pointer for a tab switch. It came down from 620 because the form it
   stands beside is paired now (see `.auth-pair`) and no longer needs the height. */
.account-visual { position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; min-height: 580px; padding: 28px 30px 30px; border-radius: 18px 0 0 24px; color: #fdf6e6; background: radial-gradient(120% 90% at 20% 10%, #12506b 0%, #0e3d56 45%, #0a3145 100%); }
.av-brand { position: relative; z-index: 3; }
/* The mark and the wordmark on this panel are the SITE's, with no recolouring of any kind
   (2026-08-15, at the owner's request). Until then two tints rode here — a `#7fb2d6` middle
   spine and an `#eb9d8a` `.kids` — on the reasoning that `--blue` IS this panel's own
   background, so the mark had been drawing two bars of three since the dialog was built.
   That was true and is the accepted cost now: the middle spine reads as a faint dark bar
   between the coral and the gold rather than as a third spine — on THIS panel, which is
   navy. In the phone band the ribbon took the site header's cream (2026-08-16) and the cost
   goes with it: over cream the `--blue` spine is simply the third spine. The ask was for the logo the
   homepage draws, and a mark that changes colour when a dialog opens is not that logo.
   `.account-brand-mark` (near the top of this sheet) is the unused rule the tint was
   originally written on — it is still unused, and re-attaching it would put both the tint
   and a 1.35 scale back. The wordmark stays cream, which is `.brand-light` on the anchor
   itself and not a rule of this panel's: the word takes the panel's own text colour exactly
   as the site footer's copy of the logo does. */
.av-twinkle { position: absolute; color: var(--gold-light); animation: av-twinkle 3.4s ease-in-out infinite; }
.av-twinkle.t1 { top: 13%; left: 22%; font-size: 16px; } .av-twinkle.t2 { top: 30%; left: 70%; font-size: 22px; animation-delay: .4s; } .av-twinkle.t3 { top: 62%; left: 13%; font-size: 13px; color: #eb9d8a; animation-delay: .8s; } .av-twinkle.t4 { top: 76%; left: 62%; font-size: 15px; animation-delay: .2s; } .av-twinkle.t5 { top: 8%; left: 52%; font-size: 12px; color: #bfe0cf; animation-delay: .6s; }
@keyframes av-twinkle { 0%,100% { opacity: .25; transform: scale(.8); } 50% { opacity: 1; transform: scale(1.15); } }
@keyframes av-floaty { 0%,100% { transform: translateY(0) rotate(var(--r,0deg)); } 50% { transform: translateY(-14px) rotate(var(--r,0deg)); } }
.av-scene { position: relative; flex: 1; min-height: 148px; margin: 6px 0 4px; }
.av-book { position: absolute; width: 92px; aspect-ratio: 3/4; border-radius: 7px 13px 13px 7px; display: grid; place-items: center; font-size: 38px; box-shadow: 0 26px 44px -18px rgba(0,0,0,.55); animation: av-floaty 7s ease-in-out infinite; }
.av-book.b1 { left: 6%; top: 18%; --r: -9deg; background: linear-gradient(150deg,#eb755b,#d75b45); }
.av-book.b2 { left: 40%; top: 6%; --r: 5deg; width: 108px; font-size: 40px; z-index: 2; background: linear-gradient(150deg,#3f6f5e,#2d5546); animation-duration: 8.4s; animation-delay: .6s; }
.av-book.b3 { left: 68%; top: 30%; --r: 11deg; background: linear-gradient(150deg,var(--gold),#c58a1a); animation-duration: 6.6s; animation-delay: .3s; }
.av-book.b4 { left: 26%; top: 52%; --r: -4deg; width: 80px; font-size: 29px; background: linear-gradient(150deg,#5f7fa8,#3f5f86); animation-duration: 7.8s; animation-delay: .9s; }
.av-greeting { position: relative; z-index: 3; max-width: 440px; }
.av-kicker { color: var(--gold-light); font-size: 12px; font-weight: 800; letter-spacing: 2.5px; text-transform: uppercase; }
.account-visual h2 { margin: 10px 0 0; font-family: var(--display); font-size: 30px; line-height: 1.05; text-wrap: balance; }
.account-visual p { margin-top: 10px; color: #bcd4dd; font-size: 13.5px; line-height: 1.6; }
/* Right: form */
/* The column is the FORM's, not a 440px card centred in it. The design pairs the register
   fields into rows (see `.auth-pair`), and a 440px cap in a column with ~490px to give
   would have put each half at ~205px — the change would have bought nothing. It stretches
   and starts at the top for the same reason: a centred short form under a fixed-height
   navy panel walks up and down as the tabs are switched. */
.account-form-wrap { display: flex; flex-direction: column; justify-content: flex-start; align-items: stretch; padding: 26px 34px 28px; background: linear-gradient(180deg,#fffdf7,#fdf3e0); }
.account-form-wrap > * { width: 100%; }
/* The LOGIN form is two fields in a column sized to the register one, so started at the top
   it sits under a third of a screen of empty cream. It is centred again — and only here,
   because centring the register form is what made the tabs walk up and down. The phone band
   puts it back to the top at the same specificity, where the dialog scrolls. */
.account-dialog.is-login .account-form-wrap { justify-content: center; }
/* Swash-underline tabs (Claude Design) */
.account-tabs { display: flex; gap: 8px; margin-bottom: 12px; background: none; padding: 0; }
.account-tabs button { position: relative; padding: 6px 6px 12px; border: 0; background: none; font-family: var(--display); font-size: 19px; font-weight: 700; color: #b3a486; cursor: pointer; transition: color .15s; }
.account-tabs button::after { content: ""; position: absolute; left: 4px; right: 4px; bottom: 3px; height: 6px; border-radius: 6px; background: var(--coral); opacity: 0; transform: rotate(-1.3deg); transition: opacity .18s; }
.account-tabs button:nth-child(2)::after { background: var(--green); }
.account-tabs button.active { color: var(--ink); }
.account-tabs button.active::after { opacity: .4; }
/* The star belongs to the PHONE switcher only (see the band at the foot of this sheet).
   It is in the markup at every width because the tabs are one pair of nodes, so the width
   that does not want it has to say so here. */
.account-tabs .tab-star { display: none; }
.auth-form { display: none; flex-direction: column; gap: 14px; padding-top: 0; }.auth-form.active { display: flex; }
.auth-form h3 { margin: 0 0 2px; font-family: var(--display); font-size: 28px; line-height: 1.05; color: var(--ink); text-wrap: balance; }
.auth-form label { display: flex; flex-direction: column; gap: 7px; }
/* First name and surname side by side. It is the form's own `gap` between the two, and the
   row is a plain grid so the pair reads as one question — the register form is already the
   tallest thing in this dialog and a second full-width field would push the CTA off a short
   laptop viewport. It stacks under 560, where two 14px-padded fields are too narrow to type
   a name into. */
/* Two fields on one line. `.auth-name-row` is the given/family name pair and stays paired
   at EVERY width — two short boxes side by side is the shape a name has, and the design's
   phone frame (2a) keeps them — while `.auth-pair` is the register form's email+country and
   password+confirm rows, which go full width on a phone: a searchable country dropdown and
   a password box with a peek button in it have nothing to give at 360px. The pair only
   stacks under 360, where "First name" itself no longer fits its own half. */
.auth-name-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 560px) { .auth-pair { grid-template-columns: 1fr; } .auth-pair .cs-panel { left: 0; right: 0; width: auto; } }
/* The email + country row is the ONE pair that keeps both columns on a phone (2026-08-14):
   the register form was scrolling to reach its own CTA, and a full-width country field is
   76px of that. It costs the country cell its name — `.cs-code` states the ISO code here,
   the header pill's own answer to a control with no room for "United Kingdom" — and the
   cell is a FIXED 126px rather than `auto`, or the placeholder's "Select your country"
   sizes the column and leaves the email box a slot. The panel keeps the right-pinned,
   viewport-capped treatment the base `.auth-pair` rule gives it, because the rule above
   hands the stacked pairs their own edges back and this row is no longer one of them. The
   band has a FLOOR at 360, where `.auth-name-row` stacks every pair anyway (the rule below):
   a full-width field stating "ZA" is two letters in a box with room for the country. */
@media (min-width: 360px) and (max-width: 560px) {
  .auth-pair-country { grid-template-columns: minmax(0,1fr) 126px; }
  .auth-pair-country .cs-panel { left: auto; right: 0; width: min(420px, calc(100vw - 44px)); }
  .auth-pair-country .cs-trigger { padding-left: 12px; padding-right: 12px; gap: 6px; }
  .auth-pair-country .cs-flag { width: auto; font-size: 20px; }
  .auth-pair-country .cs-code { display: block; }
  /* The name goes at this width whether or not a country is picked: unpicked it would be
     "Select your country" ellipsised to "S…" in a 126px cell, which says less than the
     globe and the "Country" label above it already do. */
  .auth-pair-country .cs-value { display: none; }
}
@media (max-width: 359px) { .auth-name-row { grid-template-columns: 1fr; } }
.auth-form .lbl { font-size: 14px; font-weight: 700; color: var(--ink); }
.auth-form .lbl-row { display: flex; align-items: baseline; justify-content: space-between; }
.auth-form .forgot { font-size: 13px; }
/* The register form's country select takes the text fields' own rule rather than a rule of
   its own — a native select beside these would be the only unstyled control in the dialog.
   `appearance: none` is what stops the platform drawing its own box over the border; the
   chevron is the background image that replaces the one that goes with it. This is now the
   FALLBACK styling: mountCountrySelect() builds the searchable picker over it and clips it
   away, so what is dressed here is the no-JS visit and the one where /api/countries never
   answered. Both still have to look like the form they are in. */
/* The height is STATED (2026-08-15) and is not left to be `padding + whatever line box the
   platform's font resolved to`. The country picker's trigger stands beside the email box in
   the register form's one paired row and is a flex row whose tallest child is a 24px flag
   emoji, so the two can only be held level by a number they are both given — see
   `.cs-trigger`, which carries the same figure. The padding stays because it is what centres
   the text and gives `::placeholder` its inset; `min-height` and not `height`, so nothing
   here can clip. `.auth-form .cs-query` (the country panel's own search box) is inside
   `.auth-form` and takes its `min-height` back, or the search strip grows to a field. */
.auth-form input, .auth-form select { width: 100%; min-height: 51px; padding: 14px 16px; border: 1.5px solid #e2d5b8; border-radius: 14px; background: #fffdf7; font: inherit; font-size: 16px; color: var(--ink); transition: border-color .15s, background .15s; }
.auth-form select { appearance: none; -webkit-appearance: none; padding-right: 44px; cursor: pointer; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23b3a486' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 16px center; background-size: 12px 8px; }
.auth-form input:focus, .auth-form select:focus { outline: none; border-color: var(--gold); background-color: #fff; }
.auth-form input::placeholder { color: #b3a486; }
.auth-form .button { margin-top: 6px; width: 100%; min-height: 56px; border-radius: 15px; font-size: 17px; font-weight: 800; box-shadow: 0 16px 30px -12px rgba(47,82,68,.55); }
.auth-form .button:hover { transform: translateY(-2px); }
.auth-switch { margin: 4px 0 0; text-align: center; color: #8a7a5c; font-size: 14.5px; }
/* The refusal, said in the form rather than in the corner (see setAuthError in app.js).
   `[hidden]` needs its own rule here for the reason `.addon-card[hidden]` does: the form is
   a flex column, so a `display` the attribute cannot beat has to be beaten in the sheet —
   except this one is a `<p>` and only needs the attribute honoured, which the reset already
   does; it is written out anyway so a later `display:` on the class cannot silently paint a
   cleared error. */
.auth-error { margin: -4px 0 0; padding: 12px 14px; border: 1.5px solid #f0c0b4; border-radius: 13px; background: #fdeee9; color: #a33b26; font-size: 14.5px; font-weight: 700; line-height: 1.45; }
.auth-error[hidden] { display: none; }
.linklike { border: 0; background: none; padding: 0; font: inherit; font-weight: 700; color: var(--coral); cursor: pointer; }
.linklike:hover { color: var(--coral-dark); }
/* The password boxes' SHOW/HIDE, the register form's confirm box and the reassurance under
   its CTA ("Register Mobile.html"). Four things are load-bearing.
   (1) `.pw-wrap` is a `<span>` inside a `<label>` that is a flex COLUMN — it is the
       positioning context and nothing else, so the input keeps `.auth-form input`'s own rule
       untouched apart from the right padding the button has to sit in. That padding is what
       stops a long password running under the button; it is not decoration.
   (2) The peek button is `type="button"`. Inside a form, a button with no type SUBMITS —
       here that would post a half-typed registration on the first tap of SHOW.
   (3) The mismatch is drawn twice on purpose: `.pw-note` says it in words beside the label
       and `.mismatch` colours the border, because the note sits at the top of a field the
       thumb is at the bottom of on a phone.
   (4) `.foot` is `display: contents` here so the desktop dialog lays out exactly as it did —
       the button, the note and the switch stay direct flex children of `.auth-form` and keep
       its 18px gap and their own margins. It only becomes a box in the phone band below,
       where it is the group pushed to the bottom of the column. */
.pw-wrap { position: relative; display: block; }
.pw-wrap input { padding-right: 48px; }
/* `min-height` rather than more padding: the label is 11px type and the button sits INSIDE a
   field a thumb is already aiming at, so a mis-tap lands in the password box and looks like
   SHOW simply not working. 34px in a ~52px field is the most that can be given without the
   control reading as a second field. */
.pw-peek { position: absolute; top: 50%; right: 7px; transform: translateY(-50%); display: grid; place-items: center; width: 34px; min-height: 34px; padding: 0; border: 0; border-radius: 9px; background: #f3ead8; cursor: pointer; }
.pw-peek:hover { background: #ece0c7; }
/* The eye is a background image on a `::before` and may never become markup: the button
   carried `data-i18n` until this design, and `applyI18n` writes `textContent` — a child
   would survive exactly until somebody picked German, the drawer tiles' own rule. It is an
   SVG rather than 👁/🙈 for the reason the country caret is one: an emoji's ink sits where
   its font puts it, and the "hidden" half of that pair is a monkey. Every `#` in the URI is
   written `%23` or the colour is read as a fragment and the icon is simply absent.
   `.is-on` is written by app.js and means the password is LEGIBLE — a struck-through eye. */
.pw-peek::before { content: ""; width: 19px; height: 19px; background: no-repeat center / 19px 19px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a7a5c' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1.8 12S5.5 5.2 12 5.2 22.2 12 22.2 12 18.5 18.8 12 18.8 1.8 12 1.8 12Z'/%3E%3Ccircle cx='12' cy='12' r='3.1'/%3E%3C/svg%3E"); }
.pw-peek.is-on::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a7a5c' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 5.6 20.5 18.4'/%3E%3Cpath d='M9.6 6.1A9.6 9.6 0 0 1 12 5.2c6.5 0 10.2 6.8 10.2 6.8a17 17 0 0 1-3.4 4.1M6.4 7.9A16.7 16.7 0 0 0 1.8 12s3.7 6.8 10.2 6.8a9.7 9.7 0 0 0 3.2-.5'/%3E%3Cpath d='M10 10a3.1 3.1 0 0 0 4.2 4.3'/%3E%3C/svg%3E"); }
.pw-wrap.mismatch input { border-color: var(--coral); }
.pw-note { font-family: var(--sans); font-size: 12.5px; font-weight: 800; color: #9c3620; }
.pw-note[hidden] { display: none; }
/* The ✕ belongs to the CREAM column here (the design's 1a), not to the white circle the
   other dialogs wear: this one is a two-colour card and a white puck in the corner of the
   cream side reads as a third surface. It keeps its 34px box — the circle was the tap
   target, not the decoration. The phone band MOVES it and nothing else: the ribbon above it
   is the site header's cream there too, so this treatment is the right one at every width. */
.account-dialog .dialog-close { top: 12px; right: 13px; width: 34px; height: 34px; background: transparent; color: #b3a486; font-size: 21px; }
.account-dialog .dialog-close:hover { background: #f4e9d4; color: var(--ink); }
.auth-form .foot { display: contents; }
.auth-form .register-consent { flex-direction: row; align-items: flex-start; gap: 11px; }.auth-form .register-consent input { width: 20px; min-height: 20px; height: 20px; padding: 0; margin: 2px 0 0; accent-color: var(--coral); flex: none; }.auth-form .register-consent span { font-size: 14px; color: #5c5340; line-height: 1.5; font-weight: 400; }
/* ---- The register form's country picker (Claude Design "Country Select") ------------
   A shell around #registerCountry, built by mountCountrySelect() once /api/countries has
   answered. Three things are load-bearing. (1) `.cs-field` repeats `.auth-form label`'s
   own layout, because it is a div and that rule is what spaces a label above its control.
   (2) The native select is CLIPPED, never `display: none` — it keeps `required`, and a
   select the browser cannot focus blocks submit with nothing at all to see; it is parked
   at the foot of the trigger so the validation bubble points at the field the visitor can
   actually see. (3) The keyframes are `cs-`prefixed: the design's helmet calls its own
   `pop`, and a duplicate @keyframes name in this sheet is last-wins with no warning. */
.cs-field { position: relative; display: flex; flex-direction: column; gap: 7px; }
.cs-field.cs-on > select { position: absolute; left: 16px; bottom: 0; width: 1px; height: 1px; min-height: 0; padding: 0; border: 0; opacity: 0; pointer-events: none; }
/* It stands beside the email box in the register form's one paired row, so it is the SAME
   HEIGHT as `.auth-form input` and takes that height from a number rather than from its
   contents (2026-08-15). It had `padding: 15px` and no height at all, and its tallest child
   is `.cs-flag` — a 24px emoji — so the trigger came out ~57px against the input's ~50 and
   ~53 against ~46 in the phone band, which is the mismatch that was visible on a phone. The
   vertical padding is therefore ZERO and `min-height` is the whole of it: with `align-items:
   center` the flag, the name and the chevron sit in the middle of a box whose height is
   fixed, so a bigger flag, a two-letter code or a longer name cannot push it out of line
   again. `min-height` and not `height`, so a name that somehow wrapped grows the box rather
   than spilling out of it. The radius matches the input's 14 for the same reason the height
   does; the horizontal padding is written as longhands to match the convention the phone
   band states in full at `.auth-form input` down there — `.auth-pair-country .cs-trigger`
   sets its own 12px and outranks this rule on specificity either way, so nothing here is
   being clobbered, it just reads the same as every other vertical-padding rule in this
   dialog. The figure is repeated in
   `.auth-form input` rather than shared through a custom property because the two boxes reach
   it differently — this one IS the number, that one is a floor under a padded line box — and
   a variable would read as though one of them derived the other. */
.cs-trigger { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 51px; padding-top: 0; padding-bottom: 0; padding-left: 16px; padding-right: 16px; border: 1.5px solid #e2d5b8; border-radius: 14px; background: var(--paper); color: var(--ink); font: inherit; text-align: left; cursor: pointer; transition: border-color .15s, box-shadow .15s; }
.cs-trigger:hover { border-color: #d8c8a4; }
.cs-field.is-open .cs-trigger { border-color: var(--gold); box-shadow: 0 0 0 4px rgba(233,169,43,.16); }
.cs-trigger:focus-visible { outline: none; border-color: var(--gold); box-shadow: 0 0 0 4px rgba(233,169,43,.16); }
.cs-chosen { display: flex; align-items: center; gap: 12px; min-width: 0; }
.cs-flag { flex: none; width: 30px; text-align: center; font-size: 24px; line-height: 1; }
.cs-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16.5px; font-weight: 700; color: var(--ink); }
/* The ISO code, painted at every width and shown only where the sheet asks for it (the
   phone band's email + country row). Hidden here rather than left out of the trigger, so
   `refreshCountryTrigger` has one paint path and no resize listener to keep it honest. */
.cs-code { display: none; font-size: 16.5px; font-weight: 700; color: var(--ink); letter-spacing: .2px; }
.cs-value.is-placeholder { font-weight: 400; color: #a08e6c; }
.cs-chevron { flex: none; display: grid; place-items: center; color: var(--ink); transition: transform .2s; }
.cs-field.is-open .cs-chevron { transform: rotate(180deg); }
.cs-panel { position: absolute; left: 0; right: 0; top: calc(100% + 10px); z-index: 20; border: 1.5px solid #e9dcbf; border-radius: 20px; background: var(--paper); box-shadow: 0 30px 60px -24px rgba(60,42,14,.45), 0 4px 14px -6px rgba(60,42,14,.18); overflow: hidden; animation: cs-pop .16s ease-out; }
/* Flipped by mountCountrySelect()'s place() when the dialog leaves no room below. */
.cs-panel.cs-up { top: auto; bottom: calc(100% + 10px); }
/* Paired with the email box, the field is a ~205px grid cell and a list that narrow
   ellipsises half the country names in it. The panel is pinned to the field's RIGHT edge
   and given a width of its own — it may overhang the cell because nothing between it and
   the dialog clips, and the dialog itself only scrolls on the Y axis. Capped against the
   VIEWPORT, not `100%`: a percentage here resolves against the 205px cell. Under 560 the
   pair stacks and the panel goes back to the field's own edges, or 420px of list hangs off
   a 360px phone. `place()` in app.js sets the height and never the width. */
.auth-pair .cs-panel { left: auto; right: 0; width: min(420px, calc(100vw - 56px)); }
.cs-panel[hidden] { display: none; }
@keyframes cs-pop { from { opacity: 0; transform: translateY(-6px) scale(.985); } to { opacity: 1; transform: none; } }
.cs-search { padding: 14px 14px 12px; border-bottom: 1px solid #f0e6ce; }
.cs-search-box { display: flex; align-items: center; gap: 10px; padding: 11px 14px; border: 1.5px solid #ecdfc4; border-radius: 14px; background: #faf3e2; }
.cs-search-box svg { flex: none; }
.auth-form .cs-query { flex: 1; min-width: 0; width: auto; min-height: 0; padding: 0; border: 0; border-radius: 0; background: none; font-size: 15.5px; }
.auth-form .cs-query:focus { outline: none; background: none; }
.cs-clear { flex: none; width: 22px; height: 22px; border: 0; border-radius: 999px; background: #ecdfc4; color: #7a6a4c; font-size: 13px; line-height: 1; cursor: pointer; }
.cs-clear:hover { background: #e0cfa8; }
.cs-clear[hidden] { display: none; }
.cs-list { padding: 8px; overflow-y: auto; overscroll-behavior: contain; }
.cs-list::-webkit-scrollbar { width: 10px; }
.cs-list::-webkit-scrollbar-track { background: transparent; }
.cs-list::-webkit-scrollbar-thumb { border: 3px solid var(--paper); border-radius: 99px; background: #e6d9bd; }
.cs-list::-webkit-scrollbar-thumb:hover { background: #d8c8a4; }
.cs-group { padding: 8px 12px 6px; color: #b3a486; font-size: 11.5px; font-weight: 800; letter-spacing: 1.8px; text-transform: uppercase; }
.cs-group + .cs-group, .cs-pop-rows + .cs-group { padding-top: 12px; }
.cs-row { width: 100%; display: flex; align-items: center; gap: 12px; padding: 11px 12px; border: 0; border-radius: 13px; background: transparent; color: var(--ink); font: inherit; font-size: 16px; font-weight: 500; cursor: pointer; transition: background .12s; }
.cs-row:hover, .cs-row.is-active { background: #f6efdd; }
.cs-row.is-selected { background: #f0e9d5; font-weight: 700; }
.cs-row-flag { flex: none; width: 30px; text-align: center; font-size: 23px; line-height: 1; }
.cs-row-name { flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cs-row-tick { flex: none; color: var(--green); font-size: 15px; font-weight: 800; opacity: 0; }
.cs-row.is-selected .cs-row-tick { opacity: 1; }
.cs-empty { padding: 34px 20px 38px; text-align: center; }
.cs-empty-mark { font-size: 34px; }
.cs-empty-title { margin-top: 10px; font-family: var(--display); font-size: 19px; color: var(--ink); }
.cs-empty-copy { margin: 6px 0 0; color: #8a7a5c; font-size: 14.5px; }
/* `hidden` is what the in-place filter sets, and these rules are what make it bite over
   the display each of them declares above. */
.cs-row[hidden], .cs-group[hidden], .cs-pop-rows[hidden], .cs-empty[hidden] { display: none; }

/* ---- The header pill's copy of that panel (Claude Design "Header Country Picker") ----
   Everything above is shared; these are the four differences the header needs. (1) The
   panel is hung off a PILL, not a full-width field, so it cannot inherit `left: 0;
   right: 0` — it is a fixed width pinned to the pill's right edge, capped against the
   viewport so 352px never hangs off a narrow screen. (2) `.cs-query` is scoped to
   `.auth-form` above, deliberately (it is undoing that form's own `input` rule), so the
   header repeats the two rules rather than un-scoping them. (3) The hint strip is the
   design's, and it is the one place this control says what a country is FOR — the pill
   itself has room for two letters. (4) The list's own cap is set by `place()` in app.js,
   which measures the room; the value here is only what it opens at. */
#countryPicker .cs-panel { left: auto; right: 0; width: min(352px, calc(100vw - 28px)); top: calc(100% + 12px); }
#countryPicker .cs-panel.cs-up { top: auto; bottom: calc(100% + 12px); }
#countryPicker .cs-query { flex: 1; min-width: 0; width: auto; padding: 0; border: 0; border-radius: 0; background: none; font: inherit; font-size: 15.5px; color: var(--ink); }
#countryPicker .cs-query:focus { outline: none; background: none; }
#countryPicker .cs-query::placeholder { color: #b3a486; }
#countryPicker .cs-list { max-height: min(328px, calc(100vh - 212px)); }
.cs-hint { display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding: 10px 16px 12px; border-top: 1px solid #f0e6ce; color: #6c604a; font-size: 12px; }
.cs-hint kbd { padding: 2px 6px; border: 1px solid #e6d9bd; border-radius: 6px; background: #faf3e2; font: inherit; font-size: 11px; font-weight: 700; color: #5f5540; }

.portal-dialog { width: min(1000px,calc(100% - 28px)); min-height: 600px; max-height: 92vh; overflow: auto; border-radius: 18px 25px 18px 25px; }.portal-header { display: flex; align-items: center; justify-content: space-between; padding: 35px 45px 22px; background: var(--blue); color: white; }.portal-header h2 { margin: 7px 0 4px; font-family: var(--display); font-size: 32px; }.portal-header p { margin: 0; color: #9fb6be; font-size: 9px; }.portal-header button { border: 0; color: #c9d8dc; background: transparent; font-size: 9px; font-weight: 900; cursor: pointer; }
.portal-tabs { display: flex; gap: 5px; padding: 12px 45px 0; border-bottom: 1px solid #ddd7ca; }.portal-tabs button { padding: 12px 16px; border: 0; border-bottom: 3px solid transparent; color: #728087; background: transparent; font-size: 9px; font-weight: 900; cursor: pointer; }.portal-tabs button.active { color: var(--coral); border-color: var(--coral); }.portal-panel { display: none; padding: 30px 45px 45px; }.portal-panel.active { display: block; }.portal-books { display: grid; grid-template-columns: repeat(3,1fr); gap: 15px; margin-bottom: 25px; }.portal-book { min-height: 160px; display: flex; flex-direction: column; padding: 17px; border-radius: 11px; background: #f5efe3; }.portal-book span { font-size: 28px; }.portal-book h3 { margin: 10px 0 4px; font-family: var(--display); }.portal-book p { margin: 0 0 12px; color: #748187; font-size: 8px; }.portal-book div { display: flex; gap: 6px; margin-top: auto; }.portal-book button { padding: 7px 9px; border: 0; border-radius: 5px; color: white; background: var(--blue); font-size: 7px; font-weight: 900; cursor: pointer; }.empty-portal { grid-column: 1 / -1; padding: 55px; color: #879295; border: 1px dashed #d1ccc1; border-radius: 10px; text-align: center; font-size: 10px; }.portal-membership { padding: 25px; border-radius: 12px; background: #f6f0e3; }
/* The invite panel. The referral list is a definition-style row rather than a <table>: it
   carries three unequal things (who, what they bought, what it earned) and has to collapse
   to one column on a phone, which a table row cannot do without losing its headers. */
.portal-affiliate h3 { margin: 0 0 6px; font-family: var(--display); }
.portal-affiliate h4 { margin: 28px 0 10px; font-family: var(--display); font-size: 12px; }
.aff-link { padding: 18px; border-radius: 12px; background: #f6f0e3; }
.aff-link label { display: block; margin-bottom: 7px; color: #6d7a80; font-size: 8px; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; }
.aff-link-row { display: flex; gap: 8px; }
.aff-link-row input { flex: 1; min-width: 0; padding: 11px 12px; border: 1px solid #ddd7ca; border-radius: 8px; background: white; font-size: 10px; color: var(--blue); }
.aff-hint { margin: 9px 0 0; color: #7a868b; font-size: 8px; }
.aff-totals { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; margin-top: 14px; }
.aff-totals div { padding: 14px; border-radius: 10px; background: #f5efe3; text-align: center; }
.aff-totals span { display: block; margin-bottom: 5px; color: #77848a; font-size: 8px; }
.aff-totals strong { font-family: var(--display); font-size: 15px; color: var(--blue); }
/* The payout block. It replaced a single line of grey text, so it keeps that block's class
   and grows a card around it — a request for real money should not read like a footnote. */
.aff-payout { margin: 14px 0 0; padding: 16px 18px; border: 1px solid #e4ded1; border-radius: 12px; color: #7a868b; font-size: 9px; }
.aff-payout p { margin: 0 0 8px; }
.aff-payout p:last-child { margin-bottom: 0; }
.aff-payout strong { color: var(--blue); }
.aff-payout label { display: block; margin: 12px 0 6px; color: #6d7a80; font-size: 8px; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; }
.aff-payout textarea { width: 100%; box-sizing: border-box; margin-bottom: 8px; padding: 11px 12px; border: 1px solid #ddd7ca; border-radius: 8px; background: white; font-family: inherit; font-size: 10px; color: var(--blue); resize: vertical; }
.aff-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.aff-row { display: grid; grid-template-columns: 1fr auto auto; gap: 14px; align-items: center; padding: 13px 15px; border: 1px solid #e4ded1; border-radius: 10px; }
/* A referral who has actually bought is the point of the page — it reads as a win, not a row. */
.aff-row.is-buyer { border-color: #cfe0d2; background: #f4faf5; }
.aff-who strong { display: block; font-size: 11px; color: var(--blue); }
.aff-who span { display: block; color: #6f7c82; font-size: 9px; word-break: break-all; }
.aff-who small { color: #8b9599; font-size: 8px; }
.aff-bought { color: #55636a; font-size: 9px; text-align: right; }
.aff-none { color: #97a1a5; font-style: italic; }
.aff-earned { min-width: 62px; font-family: var(--display); font-size: 13px; color: var(--blue); text-align: right; }
@media (max-width: 700px) {
  .aff-row { grid-template-columns: 1fr; gap: 6px; }
  .aff-bought, .aff-earned { text-align: left; }
  .aff-totals { grid-template-columns: 1fr; }
}

/* The member's own account is a PAGE now (/account, templates/account.html) and took
   its `acct-`prefixed rules with it — a settings screen is not the marketing scale, and
   this sheet is a deploy payload. Nothing else in here matched that prefix. */

/* ---- Share & earn (affiliate) --------------------------------------------- */
.aff-intro { padding: 30px; border-radius: 14px; background: #f6f0e3; text-align: center; }
.aff-badge { display: inline-block; margin-bottom: 12px; padding: 6px 14px; border-radius: 999px; color: #8a580c; background: #fff6e5; font-size: 9px; font-weight: 900; }
.aff-intro h3 { margin: 0 0 10px; font-family: var(--display); font-size: 22px; }
.aff-intro p { margin: 0 auto 14px; max-width: 460px; color: var(--ink-soft); font-size: 11px; line-height: 1.7; }
.aff-fineprint { color: #879295; font-size: 9px; line-height: 1.7; }
.aff-suspended { margin: 0 0 16px; padding: 12px 16px; border-radius: 10px; color: var(--coral-dark); background: #fbe3dd; font-size: 10px; font-weight: 700; }
.aff-figures { display: grid; grid-template-columns: repeat(3,1fr); gap: 12px; margin-bottom: 18px; }
.aff-figure { padding: 18px 14px; border-radius: 12px; background: #f5efe3; text-align: center; }
.aff-figure strong { display: block; font-family: var(--display); font-size: 24px; color: var(--ink); }
.aff-figure small { display: block; margin-top: 4px; color: #748187; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
.aff-link-card { padding: 20px; border-radius: 12px; background: #f6f0e3; margin-bottom: 16px; }
.aff-link-card label { display: block; margin-bottom: 8px; font-size: 9px; font-weight: 900; text-transform: uppercase; letter-spacing: .05em; color: #748187; }
.aff-link-row { display: flex; gap: 8px; }
.aff-link-row input { flex: 1; min-width: 0; padding: 11px 13px; border: 1px solid var(--border); border-radius: 8px; background: white; font-size: 10px; color: var(--ink); }
.aff-share-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.aff-share { color: var(--green); font-size: 10px; font-weight: 800; text-decoration: none; cursor: pointer; }
.aff-share:hover { text-decoration: underline; }
.aff-code { color: #879295; font-size: 9px; }
.aff-code b { color: var(--ink); letter-spacing: .08em; }
.aff-history-title { margin: 22px 0 10px; font-family: var(--display); font-size: 15px; }
.aff-history { display: flex; flex-direction: column; gap: 6px; }
.aff-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 14px; border-radius: 9px; background: #f5efe3; font-size: 10px; }
.aff-row b { color: var(--green); font-weight: 900; white-space: nowrap; }
.aff-row-out b { color: var(--ink-soft); }

/* Rewards at checkout — hidden entirely for members with no balance. */
.credit-row { display: flex; align-items: center; gap: 10px; padding: 11px 0; border-top: 1px dashed var(--border); cursor: pointer; font-size: 10px; }
.credit-row input { width: 16px; height: 16px; flex: none; }
.credit-row span { flex: 1; font-weight: 700; }
.credit-row small { display: block; margin-top: 2px; color: #879295; font-size: 9px; font-weight: 400; }
.credit-row strong { color: var(--green); white-space: nowrap; }

/* The payment methods the payment provider offers for this buyer's currency. The list is
   rendered from /api/payments/options, so a gateway added or removed over there changes
   this picker with no code here. A radio each, because paying is not a place to be clever
   about which one is "selected". */
.pay-methods { padding: 12px 0 4px; border-top: 1px dashed var(--border); }
.pay-methods-label { display: block; margin-bottom: 8px; font-size: 9px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: #879295; }
.pay-method-list { display: grid; gap: 7px; }
.pay-method { display: flex; align-items: center; gap: 10px; padding: 10px 13px; border: 1px solid var(--border); border-radius: 11px; cursor: pointer; font-size: 10px; font-weight: 700; background: var(--paper); }
.pay-method:hover { border-color: var(--coral); }
.pay-method input { width: 16px; height: 16px; flex: none; accent-color: var(--coral); }
.pay-method.chosen { border-color: var(--coral); background: #fff6f3; }
.pay-method .pay-mode { margin-left: auto; padding: 2px 8px; border-radius: 999px; background: #fff6e5; color: #8a580c; font-size: 8px; letter-spacing: 0.06em; }
/* Admin-only, and quiet: TEST is the badge that has to be noticed, so LIVE must not wear the
   same colour beside it. Same distinction the checkout page's `.flag.live` keeps. */
.pay-method .pay-mode.live { background: #e6f1e8; color: #2f6b41; }
/* The gateways are still in sandbox. Saying so beats a buyer believing they were charged —
   and it disappears by itself the day live credentials land, because the flag is the
   provider's own `mode`. */
.pay-test-note { margin: 9px 0 0; padding: 8px 12px; border-radius: 9px; background: #fff6e5; color: #8a580c; font-size: 9px; font-weight: 700; }
.pay-methods-empty { margin: 9px 0 0; color: var(--coral-dark); font-size: 9px; font-weight: 700; }

/* ---- The membership modal — the "Subscribe Modal" design -------------------
   #planPayDialog. Everything is `sub-`prefixed, keyframes included: this sheet already
   carries several twinkle/bob animations (the hero's, the FAQ's, the footer's) and a
   duplicate @keyframes name is last-wins with no warning.

   The dialog element IS the card — no inner wrapper — so the design's own full-screen
   centring grid is the native `<dialog>`'s, and its tinted backdrop is the sheet's
   existing `dialog::backdrop`. That is also why `overflow` is left alone: the three
   sparkles are pinned OUTSIDE the rounded edge on purpose and a clip would eat them. */
@keyframes sub-popin { from { opacity: 0; transform: translateY(16px) scale(.97); } to { opacity: 1; transform: none; } }
@keyframes sub-twinkle { 0%, 100% { opacity: .3; transform: scale(.8) rotate(0deg); } 50% { opacity: 1; transform: scale(1.15) rotate(14deg); } }
/* The tier mark's tilt lives IN the keyframe, not on the element — the design carried it
   on a `--r` custom property, and one element uses this. A `transform` added to
   `.sub-tier-mark` later would be overridden by the animation with nothing to see. */
@keyframes sub-bob { 0%, 100% { transform: translateY(0) rotate(-6deg); } 50% { transform: translateY(-4px) rotate(-6deg); } }

/* The card is taller than a laptop viewport once the gateway picker is in it, so it has to
   scroll — and the scroll is on the BODY, not on the dialog. Scrolling the dialog would
   mean clipping it, and the three sparkles are pinned outside its rounded edge on purpose;
   a flex column with a fixed head and an auto body keeps them and still reaches the CTA. */
.sub-dialog { position: relative; width: min(560px, calc(100% - 28px)); max-height: calc(100dvh - 32px); background: var(--paper); border-radius: 28px; box-shadow: 0 50px 90px -50px rgba(13, 47, 67, .9); animation: sub-popin .24s ease-out; }
.sub-dialog[open] { display: flex; flex-direction: column; }

.sub-sparkle { position: absolute; animation: sub-twinkle 3.2s ease-in-out infinite; }
.sub-sparkle-a { top: -14px; left: 38px; color: var(--gold); font-size: 18px; }
.sub-sparkle-b { top: 30px; right: -14px; color: #eb9d8a; font-size: 13px; animation-duration: 4s; animation-delay: .6s; }
.sub-sparkle-c { bottom: 60px; left: -16px; color: #9fc7d4; font-size: 15px; animation-duration: 3.6s; animation-delay: 1s; }

.sub-head { position: relative; flex: none; padding: 26px 26px 24px; border-radius: 28px 28px 0 0; color: #fdf6e6; background: var(--blue); overflow: hidden; }
.sub-close { position: absolute; top: 18px; right: 18px; width: 34px; height: 34px; display: grid; place-items: center; border: 0; border-radius: 50%; color: #fdf6e6; background: rgba(253, 246, 230, .14); font-size: 15px; line-height: 1; cursor: pointer; transition: transform .16s, background .16s; }
.sub-close:hover { background: rgba(253, 246, 230, .28); transform: rotate(90deg); }
.sub-tier { display: flex; align-items: center; gap: 14px; padding-right: 44px; }
/* A container that happens to look like a stamp: the tier glyph is written in from the
   plan card, so nothing here may assume one particular character inside it. */
.sub-tier-mark { flex: none; width: 56px; height: 56px; display: grid; place-items: center; border-radius: 18px; background: #dfe9ee; font-size: 28px; line-height: 1; box-shadow: 0 14px 22px -14px rgba(0, 0, 0, .8); animation: sub-bob 3.6s ease-in-out infinite; }
.sub-tier-name { min-width: 0; }
.sub-eyebrow { display: block; font-size: 10.5px; font-weight: 800; letter-spacing: 1.8px; text-transform: uppercase; color: var(--gold-light); }
.sub-tier-name strong { display: block; margin-top: 4px; font-family: var(--display); font-size: 28px; font-weight: 400; line-height: 1.1; }
.sub-blurb { display: block; margin-top: 12px; font-family: var(--display); font-style: italic; font-size: 15px; color: #9fc0d0; }

.sub-body { min-height: 0; overflow-y: auto; padding: 24px 26px 26px; }
.sub-question { display: block; font-family: var(--display); font-style: italic; font-size: 18px; }

.sub-cycles { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 13px; }
.sub-cycle { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 3px; padding: 14px 15px; border: 2px solid #ece0c6; border-radius: 17px; background: var(--paper); color: var(--ink); text-align: left; cursor: pointer; transition: border-color .16s; }
.sub-cycle:hover { border-color: #eb9d8a; }
.sub-cycle.is-on { border-color: var(--coral); background: #fff6ec; box-shadow: 0 16px 26px -20px rgba(215, 91, 69, .95); }
.sub-cycle-label { font-size: 12.5px; font-weight: 800; letter-spacing: 1.2px; text-transform: uppercase; color: #a4906f; }
.sub-cycle.is-on .sub-cycle-label { color: #8a7a5f; }
.sub-cycle-price { font-family: var(--display); font-size: 26px; font-weight: 400; color: #6b5c45; }
.sub-cycle.is-on .sub-cycle-price { color: var(--ink); }
.sub-cycle-note { font-family: var(--display); font-style: italic; font-size: 13px; color: #a4906f; }
.sub-cycle.is-on .sub-cycle-note { color: #8a7a5f; }
/* The saving ribbon. It is hidden outright when the admin gives no months away — a
   "0 months free" flash is the same promise-nothing the share reward's badge avoids. */
.sub-save { position: absolute; top: -10px; right: 12px; padding: 4px 9px; border-radius: 99px; background: #e6efe9; color: var(--green); font-size: 10px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; white-space: nowrap; transform: rotate(-3deg); }
.sub-cycle.is-on .sub-save { background: var(--green); color: #fff; }
.sub-save[hidden] { display: none; }

.sub-inside { margin-top: 20px; padding: 16px 18px; border: 2px dashed #e2d3b4; border-radius: 18px; background: #faf3e2; }
.sub-inside-label { display: block; font-size: 10.5px; font-weight: 800; letter-spacing: 1.6px; text-transform: uppercase; color: #a4906f; }
.sub-features { display: flex; flex-direction: column; gap: 10px; margin: 12px 0 0; padding: 0; list-style: none; }
.sub-features li { display: flex; align-items: flex-start; gap: 10px; font-size: 14.5px; line-height: 1.4; }
.sub-features li::before { content: "✓"; flex: none; width: 20px; height: 20px; margin-top: 1px; display: grid; place-items: center; border-radius: 99px; background: #e6efe9; color: var(--green); font-size: 11px; font-weight: 900; }

/* A coupon code on a membership (V48). It sits above "Due today" because that is what it
   changes, and it is the one control in this dialog that asks the server — everything else
   here is priced off /api/pricing, which knows nothing about a code. */
.sub-coupon { margin-top: 16px; }
.sub-coupon-label { display: block; margin-bottom: 7px; font-size: 10.5px; font-weight: 800; letter-spacing: 1.6px; text-transform: uppercase; color: #a4906f; }
.sub-coupon-row { display: flex; gap: 8px; }
.sub-coupon-row input { flex: 1; min-width: 0; padding: 11px 13px; border-radius: 13px; border: 1.5px solid var(--line, #eee0c4); background: #fff; font: inherit; font-size: 14px; letter-spacing: .06em; text-transform: uppercase; }
.sub-coupon-row input::placeholder { letter-spacing: normal; text-transform: none; color: #a4906f; }
.sub-coupon-row button { flex: none; padding: 11px 17px; border-radius: 13px; border: 1.5px solid var(--line, #eee0c4); background: #fffdf7; color: var(--blue); font: inherit; font-weight: 800; font-size: 14px; cursor: pointer; }
.sub-coupon-row button:hover:not(:disabled) { background: #fff; border-color: #d6c9ab; }
.sub-coupon-row button:disabled { opacity: .6; cursor: default; }
.sub-coupon-note { margin: 8px 2px 0; font-size: 12.5px; line-height: 1.45; color: #a34a30; }
.sub-coupon-note.is-good { color: var(--green); }
/* `hidden` has to beat a `display` the attribute cannot, the same reason `.addon-card[hidden]`
   and `.kc-star[hidden]` exist. These two are block-level by default, so a plain [hidden] would
   work — it is written anyway, so a later `display:` on either cannot silently un-hide it. */
.sub-coupon-note[hidden], .sub-coupon-off[hidden] { display: none; }

.sub-due { display: flex; align-items: center; gap: 12px; margin-top: 16px; padding: 14px 16px; border-radius: 17px; color: #fdf6e6; background: var(--blue); }
.sub-due span { flex: 1; font-family: var(--display); font-size: 17px; }
.sub-due strong { font-family: var(--display); font-size: 28px; font-weight: 400; }
.sub-coupon-off { margin: 8px 2px 0; font-size: 13px; line-height: 1.45; color: var(--green); }

/* The gateway picker is the site's own component, rendered in by `loadPayMethods`. Its
   type scale is the marketing page's tiny one, which reads as a footnote beside the
   design's 14px body — so it is brought up to size here rather than restyled at source,
   where /checkout and the rest of the sheet still want the small version. */
.sub-body .pay-methods { margin-top: 18px; padding: 14px 0 0; border-top: 2px dashed #e2d3b4; }
.sub-body .pay-methods-label { margin-bottom: 10px; font-size: 10.5px; letter-spacing: 1.6px; color: #a4906f; }
.sub-body .pay-method { padding: 12px 14px; border-radius: 13px; font-size: 13.5px; }
.sub-body .pay-method .pay-mode { font-size: 9.5px; }
.sub-body .pay-test-note, .sub-body .pay-methods-empty { font-size: 11.5px; }

/* The admin-only sandbox lane (V46). `hidden` has to be beaten in the sheet because the rule
   below declares `display: flex` — the same reason `.addon-card[hidden]` and `.kc-star[hidden]`
   exist — and a lane control that stayed visible for a member is exactly what it must not be. */
.sub-lane { display: flex; align-items: flex-start; gap: 10px; margin-top: 16px; padding: 11px 13px; border-radius: 14px; border: 1.5px dashed #c9bda3; background: #fbf6ea; cursor: pointer; }
.sub-lane[hidden] { display: none; }
.sub-lane:has(input:checked) { border-style: solid; border-color: #8a6d12; background: #fdf3d3; }
.sub-lane:has(input:focus-visible) { outline: 3px solid rgba(235, 117, 91, .45); outline-offset: 2px; }
.sub-lane input { flex: none; margin: 2px 0 0; width: 16px; height: 16px; accent-color: var(--coral); }
.sub-lane span { flex: 1; min-width: 0; font-size: 11.5px; line-height: 1.5; color: #8a7a5d; }
.sub-lane strong { display: block; font-size: 12.5px; color: #5c4409; }

.sub-cta { width: 100%; margin-top: 18px; padding: 17px; border: 0; border-radius: 17px; color: #fff; background: var(--green); font-size: 17px; font-weight: 800; cursor: pointer; transition: transform .16s, background .16s; box-shadow: 0 20px 32px -16px rgba(47, 82, 68, .95); }
.sub-cta:hover:not(:disabled) { background: var(--green-dark); transform: translateY(-2px) rotate(-.6deg); }
.sub-cta:disabled { opacity: .65; cursor: default; }

.sub-secure { display: flex; align-items: center; justify-content: center; gap: 7px; margin: 12px 0 0; font-size: 12.5px; color: #a4906f; }

/* Under ~400px the two cycle cards cannot hold a Georgia price and a ribbon side by side,
   so they stack — and the ribbon comes inboard, where a -10px overhang would be clipped
   by nothing but still sits over the card above it. */
@media (max-width: 400px) {
  .sub-head, .sub-body { padding-left: 18px; padding-right: 18px; }
  .sub-cycles { grid-template-columns: 1fr; }
  .sub-save { top: 8px; right: 10px; }
}

/* "A friend invited you" above the login/register tabs. */
.referral-note { display: flex; align-items: center; gap: 8px; margin: 0 0 14px; padding: 10px 14px; border-radius: 10px; color: #8a580c; background: #fff6e5; font-size: 10px; font-weight: 700; }
.footer-legal { padding: 0; border: 0; color: inherit; background: transparent; font-size: 8px; cursor: pointer; }
.edit-page-dialog { width: min(560px,calc(100% - 28px)); padding: 38px; border-radius: 15px; }.edit-page-dialog h2 { margin: 8px 0; font-family: var(--display); font-size: 29px; }.edit-page-dialog p { color: #748289; font-size: 9px; line-height: 1.6; }.edit-page-dialog textarea { width: 100%; padding: 12px; margin: 12px 0; border: 1px solid #d4d0c5; border-radius: 8px; resize: vertical; }
.edit-page-dialog .page-edit-error { margin: 0 0 12px; color: #a43c2c; font-size: 12px; font-weight: 750; }
.edit-page-dialog .page-edit-error[hidden] { display: none; }

.toast { position: fixed; z-index: 100; left: 50%; bottom: 25px; display: flex; align-items: center; gap: 10px; padding: 11px 16px; border-radius: 8px; color: white; background: var(--ink); box-shadow: 0 15px 35px rgba(0,0,0,.25); opacity: 0; pointer-events: none; transform: translate(-50%, 15px); transition: .25s; }
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast span { width: 19px; height: 19px; display: grid; place-items: center; border-radius: 50%; color: var(--ink); background: var(--gold); font-size: 9px; }
.toast p { margin: 0; font-size: 10px; font-weight: 700; }

.reveal { opacity: 0; transform: translateY(18px); transition: opacity .65s ease, transform .65s ease; }
.reveal.visible { opacity: 1; transform: none; }
.reveal-delay, .reveal-delay-1 { transition-delay: .12s; }
.reveal-delay-2 { transition-delay: .24s; }

/* The header bar between the nav's own breakpoint and a full-width desktop was ALREADY
   too narrow for a signed-in member: brand + five nav links + their name + "My books"
   spilled past the rounded edge at 1024px with no country pill in sight. It has to be
   made to fit before anything else is put in it, so this band goes compact — tighter
   nav links and gaps, a capped name, and the country as its CODE. */
@media (max-width: 1300px) {
  .site-header { gap: 14px; }
  .desktop-nav a { padding: 8px 9px 12px; font-size: 14px; }
  .header-account { gap: 10px; }
  .acc-name { max-width: 104px; }
  /* The meter needs no answer to this band any more — it is inside the account menu
     since 2026-08-25 and no longer competes for room on this row. */
  /* Nothing shortens the country pill any more: it is a flag and two letters at every
     width, so the signed-in bar has nothing left to squeeze out of it. */
}


/* Below this the nav does not fit beside a signed-in account block at all. It used to
   simply drop, with the links reachable only from the footer; there IS a drawer now (see
   the `.header-tools` block at the foot of this sheet), so the whole tools node folds
   into it and the bar keeps the brand and the hamburger alone. */
@media (max-width: 980px) {
  .hero { grid-template-columns: 1fr 1fr; padding-bottom: 36px; }
  /* The 600px cover box will not fit a ~490px column: scale it as one. The
     notes are pinned to the column, not to the box, so they close in on the
     covers as it shrinks — the lower one lands on the left title. */
  .hero-visual { min-height: 420px; --cover-scale: .7; }
  .creator-layout { grid-template-columns: 1fr; }
  .book-preview { position: relative; top: auto; min-height: auto; display: grid; grid-template-columns: 1fr 200px; align-items: center; }
  .preview-label, .preview-meta, .preview-hint { grid-column: 1 / -1; }
  .book-stage { height: 390px; }
  .plan-grid { grid-template-columns: 1fr; max-width: 560px; margin: auto; }
  .plan-card, .plan-card.featured { min-height: 390px; }
}

/* The cover fan is one fixed-geometry box; every width it has to fit gets its
   own scale, and these steps have to stay in ascending-specificity file order
   with the blocks around them or a later block wins at the wrong width. */
@media (max-width: 880px) {
  .hero-visual { min-height: 380px; --cover-scale: .62; }
}

@media (max-width: 760px) {
  /* The whole rhythm comes down together: one figure moved, not six paddings. The two
     seam waves keep their pixel heights — a wave that scaled with the padding would
     be a ripple here — so `.pricing-section`'s `--band-pad + 14px` still has to clear
     a 58px dip, which at 52px it does with 8px to spare. */
  :root { --band-pad: 52px; }
  .section-shell { width: min(100% - 32px, 620px); }
  /* The bar is shorter here (smaller margin and padding), so the anchor offset shrinks with it. */
  html { scroll-padding-top: 96px; }
  .site-header { width: calc(100% - 24px); margin-top: 14px; padding: 10px 12px 10px 14px; gap: 10px; border-radius: 18px; }
  .site-header .button { display: none; }
  /* THE BAR IS A FIXED WIDTH AND THE BRAND WAS NOT ALLOWED TO GIVE WAY, so on a narrow phone
     the account chip was pushed off the right-hand end of it — the monogram half-drawn past
     the white, on `/shelf` where the chip is the only way to an account. A flex item's
     `min-width` is `auto`, which is the intrinsic width of the wordmark, so
     `justify-content: space-between` had nothing to take from and overflowed instead.
     The brand yields (it is a link home, and the mark still says whose site this is) and the
     account end keeps its `flex: none`, because a squashed monogram is not a target a thumb
     can hit. Fixing this on `.site-header` rather than in shelf.html on purpose: the same bar
     is on every page, and the phone that showed it is not the only narrow one. */
  .brand { min-width: 0; overflow: hidden; }
  .brand-word { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .header-account { gap: 8px; }
  .header-account .account-link { font-size: 13px; }
  /* Narrowed, never hidden: a phone is where the guess is most often wrong. The country
     still reads as its name here — the nav is gone at this width, so the bar has room
     for one — and gives way to the code only in the 560 block below. */
  .country-picker { padding: 6px 9px; gap: 5px; }
  .country-picker-name { max-width: 116px; }
  .country-picker-caret { width: 11px; height: 11px; }
  #openLogin, #openRegister { padding: 9px 14px; }
  .brand-word { font-size: 20px; }
  /* Stacked, the `gap` is the air between the two CTAs and the fan of covers under them, and
     the bottom padding is the air between that fan and the promises strip. The two moved in
     opposite directions on 2026-08-06 (28 -> 44, 48 -> 24): the buttons were crowding the
     books above them while the books sat a long way off the first promise card. */
  .hero { display: flex; flex-direction: column; align-items: stretch; gap: 44px; min-height: 0; padding: 40px 0 24px; }
  .hero-copy { padding: 0; text-align: center; }
  .hero h1 { margin-top: 16px; font-size: clamp(42px, 12vw, 60px); letter-spacing: -1.2px; }
  .hero-lede { margin-inline: auto; font-size: 16px; }
  .hero-actions { justify-content: center; }
  /* Stacked, the fan has no column to be centred in, so the scale is anchored to the TOP
     of the visual and the covers' own 52px inset is scaled out of the way with it. The
     centred origin the two-column widths keep cannot do that: it floors the gap under the
     CTAs at 52px x scale plus half of whatever min-height goes unused, which is the 65-95px
     of dead air this block existed to leave. Overlap is untouched — the origin moves the
     box, not the fan's geometry, so the fixed 600px width and the title insets still hold.
     `top` may be set against `bottom: 0` (it changes the box height) ONLY because origin y
     is `0`: at `50% 50%` the origin would depend on the height it is setting. Every
     min-height below is `392px x scale` — the fan's own content height — plus ~10px, so
     the two numbers move together; the 980/880 blocks above keep the centred origin. */
  .hero-covers { top: calc(-52px * var(--cover-scale)); transform-origin: 50% 0; }
  .hero-visual { min-height: 375px; --cover-scale: .93; }
  .hero-promises { padding-inline: 18px; }
  .hero-promise-grid { margin-top: 20px; }
  .how-section { padding-inline: 20px; }
  .how-heading h2 { font-size: 32px; }
  .how-heading p { font-size: 16px; }
  /* One column, and the rail between the panels lies down and points down the page. */
  .hm-split { grid-template-columns: 1fr; gap: 16px; margin-top: 32px; }
  .hm-panel { padding: 24px 20px 22px; border-radius: 22px; }
  .hm-rail { flex-direction: row; width: 100%; max-width: 240px; justify-self: center; padding: 0; }
  .hm-rail-line { width: auto; height: 3px; background: repeating-linear-gradient(90deg, #e0cfa8 0 6px, transparent 6px 15px); }
  /* The rotation is stated as well as animated: with animations suppressed the keyframes
     never run, and the plate would point right on a phone. */
  .hm-rail-plate { margin: 0 12px; transform: rotate(90deg); animation-name: hm-nudge-down; }
  @keyframes hm-nudge-down { 0%, 100% { transform: rotate(90deg) translateX(0); } 50% { transform: rotate(90deg) translateX(5px); } }
  .hm-finish { margin-top: 30px; padding: 18px 20px; text-align: center; }
  .hm-finish-copy { min-width: 0; }
  .hm-cta { width: 100%; justify-content: center; }
  .creator-section { padding: 80px 0 85px; }
  .creator-progress { justify-content: center; }
  .creator-progress i { max-width: 60px; }
  .progress-step { min-width: 58px; }
  .progress-step small { display: none; }
  .creator-card { min-height: auto; padding: 30px 21px 26px; }
  .field-row { grid-template-columns: 1fr 110px; }
  .field-row-equal { grid-template-columns: 1fr; }
  .character-row { grid-template-columns: 1fr 44px; grid-template-areas: "name remove" "role role" "gender gender" "species species" "photo photo" "consent consent"; }
  .theme-grid { grid-template-columns: 1fr; }
  .theme-card { min-height: 70px; display: grid; grid-template-columns: 34px 1fr; grid-template-rows: 1fr 1fr; align-items: center; }
  .theme-card > span { grid-row: 1 / 3; }
  .theme-card strong { margin: 0; }
  .form-actions { align-items: stretch; flex-direction: column-reverse; }
  .form-actions.end { align-items: stretch; }
  .form-actions .button { width: 100%; }
  .book-preview { display: block; padding: 23px 20px; }
  .book-stage { height: 430px; }
  .review-grid { grid-template-columns: 1fr; }
  .review-card.featured { transform: none; }
  /* one column: the two card columns stack in DOM order, so the questions still
     read 1-2-3-4 rather than 1-3-2-4. */
  .faq-card { padding: 15px 16px; }
  .faq-card[open] { padding: 16px 16px 18px; }
  .faq-card summary { gap: 10px; }
  .faq-card summary strong { font-size: 15px; }
  .faq-card p { padding-left: 0; }
  .faq-help { padding: 16px; }
  .faq-help a, .faq-help button { width: 100%; justify-content: center; }
  /* The grid is `auto-fit minmax(232px, 1fr)` and reflows on its own — the old
     two-column overrides (and the span rules that went with them) would fight it.
     This is the TABLET stage: one column each per row, everything still there. The
     phone stage is the ≤640 block below, which is where the stack stops being four
     screens tall. */
  .site-footer { padding: 72px 20px 0; }
  .footer-main { gap: 34px; }
  .footer-bottom { gap: 20px; padding: 15px 0 22px; flex-direction: column; align-items: flex-start; }
  .ft-legal { margin-left: 0; }
  .example-spread { grid-template-columns: 1fr; }
  .example-art { height: 330px; }
  .example-copy { padding: 35px 27px; }
  .order-dialog { padding: 40px 22px 28px; }
  .story-dialog { max-height: 95vh; }
  .story-dialog-header { align-items: flex-start; flex-direction: column; gap: 10px; padding: 24px 50px 18px 22px; }
  .story-dialog-header h2 { font-size: 23px; }
  .audio-player { flex-wrap: wrap; padding: 9px 15px; }.audio-player label { margin-left: 0; }.audio-player small { width: 100%; }
  .story-reader { min-height: auto; grid-template-columns: 1fr; padding: 15px; }
  .story-art-page { min-height: 300px; border-radius: 7px 7px 0 0; }
  .story-art-page::after { top: auto; left: 0; right: 0; bottom: -1px; width: auto; height: 10px; background: linear-gradient(180deg, transparent, rgba(18,31,34,.2)); }
  .story-copy-page { min-height: 350px; padding: 27px 25px 22px; border-radius: 0 0 7px 7px; box-shadow: inset 0 10px 18px rgba(43,42,35,.07); }
  .story-page-copy h3 { font-size: 28px; }
  .story-page-copy p { font-size: 16px; }
  .story-cover-copy { inset: 32px 25px auto; }
  .story-cover-copy strong { font-size: 35px; }
  .story-reader-footer { gap: 8px; padding-inline: 10px; flex-wrap: wrap; }
  .story-nav-button { min-width: 54px; }
  .story-nav-button span { display: none; }
  .story-approval-bar { grid-template-columns: 1fr; gap: 8px; padding: 14px 20px 18px; }
  .story-approval-bar p { display: none; }
  .story-approval-bar .button, .edit-story-button, .edit-page-button { width: 100%; justify-self: stretch; }
  .read-story-link { width: 100%; padding: 8px 0 0 32px; text-align: left; }
  .generated-story { flex-wrap: wrap; }
  .order-content { grid-template-columns: 140px 1fr; gap: 18px; }
  .order-thumb { height: 210px; }
  .account-dialog[open] { grid-template-columns: 1fr; }.account-form-wrap { padding: 38px 24px; }
  .portal-header, .portal-panel { padding-left: 22px; padding-right: 22px; }.portal-tabs { padding-left: 18px; overflow-x: auto; }.portal-books { grid-template-columns: 1fr; }
}

/* ---- the footer on a phone: the "Footer Mobile" design ------------------------
   The ≤760 stage above only pads and re-gaps, so `auto-fit minmax(232px, 1fr)` gave
   every block a full-width row of its own and the footer ran about four screens. Below
   640 it is: the brand strip, the two link columns SIDE BY SIDE, the sign-up, then the
   legal bar. Nothing is removed from the markup — the blurb and the ✦-note are the two
   things the design hides, and the placeholder carries the ask in their place.
   This block is a hand-copy on FOUR surfaces (styles.css, and the inline footer in
   shelf.html, account.html and checkout.html — the same four the base rules are copied
   across), so a change here is a change in all four. In the three inline copies the
   padding rule has to be written `footer.site-footer`: their ≤760 rule is, and a bare
   `.site-footer` loses to it on specificity whatever the source order.
   Three departures from the design, each because of something this page is. (1) The
   tracks are `minmax(0, 1fr)`, not the design's `1fr` — a bare `1fr` is `minmax(auto,
   1fr)` and the gift band has just paid for that (see `docs/claude/homepage.md`); the
   items all carry `min-width: 0` as well, but the track form is what does not depend on
   every one of them keeping it. (2) **The legal row is allowed to WRAP.** The design
   sets `flex-wrap: nowrap` and `white-space: nowrap` on `.footer-bottom`, and
   `.site-footer` is `overflow: hidden`, so at 360px — a real phone width, and where the
   gift band's crop actually showed — the third link is simply not on the page, with
   `document.scrollWidth === innerWidth` the whole time. The site speaks eleven
   languages and every one of those three labels is longer than the English literal, so
   this is a certainty and not a risk. The `nowrap` stays on the LINK alone, so a label
   never breaks mid-word and the row falls to two lines instead of vanishing. (3) The
   tagline keeps its `<br>` and stays two lines, where the design's flows as one. Hiding
   the break in CSS is what a stylesheet can reach — the markup is the part copied four
   times — and `br { display: none }` removes the box, so the two sentences run together
   as "Little people.Big adventures." with nothing to put the space back. */
@media (max-width: 640px) {
  .site-footer { padding: 56px 18px 0; }
  .ft-wave { height: 34px; }
  .ft-star-one, .ft-star-two, .ft-star-three { display: none; }

  .footer-main { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 26px 14px; padding-bottom: 24px; }

  /* brand: the wordmark and the socials share the top row, the tagline sits under both */
  .footer-brand { grid-column: 1 / -1; display: flex; align-items: center; gap: 10px 12px; flex-wrap: wrap; }
  .footer-brand p { flex: 1 0 100%; order: 3; margin: 0; font-size: 17px; line-height: 1.3; }
  .ft-socials { margin: 0 0 0 auto; gap: 7px; }
  .ft-social { width: 36px; height: 36px; border-radius: 11px; font-size: 14px; }

  /* the sign-up closes the footer, and it is two things rather than four */
  .newsletter { grid-column: 1 / -1; gap: 10px; padding-top: 22px; border-top: 1px solid rgba(253,246,230,.14); }
  .newsletter .ft-news-head { gap: 7px; }
  .newsletter strong { font-size: 17px; }
  .newsletter p { display: none; }
  .newsletter form { max-width: none; padding: 5px; }
  .newsletter input { padding-left: 12px; font-size: 16px; } /* 16px is what stops iOS zooming the page on focus */
  .newsletter button { width: 36px; height: 36px; font-size: 15px; }
  .ft-news-note { display: none; }

  /* two columns of links, tight rows — a label wraps rather than sets the track width */
  .footer-links { min-width: 0; }
  .ft-col-title { padding-bottom: 7px; font-size: 16px; }
  .ft-col-list { gap: 0; margin-top: 8px; }
  .footer-links a, .footer-links button { padding: 7px 0; font-size: 14px; gap: 7px; }
  .ft-link-label { white-space: normal; }

  /* one row: the copyright left, the three notices right. They only fit as dot-separated
     text, so the pill padding and the hover fill come off — and the row keeps the base
     `flex-wrap: wrap` for the reason the comment above states. */
  .footer-bottom { gap: 6px 10px; padding: 13px 0 18px; flex-direction: row; align-items: center; justify-content: space-between; font-size: 11.5px; }
  .ft-legal { margin-left: 0; gap: 4px 0; }
  .ft-legal .footer-legal + .footer-legal::before { content: "·"; margin: 0 7px; opacity: .45; }
  .site-footer .footer-legal { padding: 0; border-radius: 0; font-size: 11.5px; white-space: nowrap; }
  .site-footer .footer-legal:hover { background: transparent; }
}

/* The account dialog SCROLLS on a phone, and lifting `overflow: hidden` is what does it.
   That hidden (see the comment above `.account-dialog`) exists to stop a white hairline
   bleeding through the rounded clip down the NAVY panel's corners — and it also silently
   overrode the UA sheet's own `dialog { overflow: auto }`, so the register form, which is
   taller than a phone from the first field, was simply CLIPPED at the viewport with no way
   to reach the password box or the submit button. This band is exactly where
   `.account-visual` is `display: none`, so there is no navy panel here and no hairline to
   hide. Two things ride with it. The height is capped in `dvh` rather than left to the UA's
   own `100%`, which on a phone is the tall viewport and puts the last field under the URL
   bar. And the scroller has to be the DIALOG: `[open]` is `display: grid` and sizes its one
   row to the content, so an `overflow-y` on `.account-form-wrap` would have nothing to
   scroll and the overflow would land back on the dialog.
   The landscape band is in here for a reason of its own — `.account-visual`'s 620px
   min-height is more than a landscape phone HAS, so that panel and the second column have
   to go there too, and this pair of queries is the one the reader's own short-viewport
   rules already use. */
@media (max-width: 760px), (max-width: 900px) and (max-height: 500px) {
  .account-dialog[open] { grid-template-columns: 1fr; max-height: calc(100dvh - 24px); overflow-y: auto; overscroll-behavior: contain; }
  .account-visual { display: none; }
}
@media (max-width: 900px) and (max-height: 500px) {
  .account-form-wrap, .account-dialog.is-login .account-form-wrap { justify-content: flex-start; padding: 24px 24px 30px; }
}

/* ON A PHONE THE DIALOG IS THE SCREEN AND THE PANEL IS A BRAND RIBBON IN THE SITE HEADER'S
   OWN COLOUR ("Register Modal", 3a). It was NAVY here as well as on the desktop until
   2026-08-16: on a phone this dialog COVERS the site header and stands in for it, so a navy
   strip where the header is cream read as a different site's bar for as long as the modal
   was open. The colour is `var(--paper)` and not the header's own `rgba(255,253,247,.82)` —
   that alpha composites against whatever is behind it, and `.account-dialog` is
   `background: transparent`, so an rgba copied across would tint with the page rather than
   match it. The header is the FIRST element in the body and `.hero-section`'s warm gradient
   begins below it, so at rest it composites over `--paper` alone and renders exactly
   `#fffdf7` — sampled at 390 wide, not reasoned about — which IS `var(--paper)`. It does
   warm a shade once the hero scrolls under it; the ribbon is opaque and does not, and that
   is the right side to be on for a strip nothing scrolls behind. The desktop panel is untouched
   and stays navy: it sits BESIDE the form as artwork, not above it as a header.
   The band above takes `.account-visual` away below 760 outright, which left a
   phone with a cream form and no sign of whose site it is — the wordmark is in the header,
   and this dialog covers the header. It came back as a HEADER STRIP carrying the kicker and
   the greeting (2a), and on login as a 236px panel with the floating books; both are gone.
   The target is a 412 × 690 Chrome-on-Android viewport with the WHOLE register form —
   six fields, both consents and the CTA — landing in it, and every pixel spent on a greeting
   up here is a pixel of scrolling underneath. What is left is one row: the mark, the
   wordmark and the ✕. The wordmark COMES BACK because it is the only thing in the ribbon
   now, and the greeting, the kicker and the books go for the same reason, at BOTH tabs —
   the two states are the same ribbon, so a tab switch no longer resizes the shell.
   Seven things are load-bearing.

   (1) `min-height: 620px` on the QUERY is what leaves the landscape fix above intact. That
       band hides this panel because 620px is more than a landscape phone HAS; this query
       never fires there, so the `display: none` stands and nothing has to undo it. It is
       also why the ribbon may be re-shown by a plain `display: flex` rather than a revert.
   (2) The shell is FIXED-HEIGHT and the FORM COLUMN is the scroller. The dialog fills the
       viewport (`height: 100dvh`, no margin, no radius) and the grid is `auto minmax(0,1fr)`
       — a bare `1fr` is `minmax(auto,1fr)` and refuses to shrink under its content, so the
       wrap would overflow the dialog instead of scrolling inside it, silently and with the
       CTA under the address bar. This inverts what the strip band said: with a wrap that
       has a height of its own, `flex: 1` and `margin-top: auto` DO resolve, where in a box
       sized to its content they resolved to zero.
   (3) The scroller stays, whatever the design frame shows. The real form carries a referral
       note, an error node and a confirm box that the frame does not, and `.auth-pair` stacks
       under 560 where its rows did not — so it is a fit at 690 and a scroll at 640 (the
       marketing tick and the "no card needed" line were two more of these until 2026-08-17;
       the form is two rows shorter and every other reason still stands, so nothing here may
       be removed on the strength of that) — and `overscroll-behavior` moves WITH the
       scroller or a flick at the end of the form scrolls the page behind the modal.
   (4) `overflow: hidden` goes back on the dialog, which is what pins the ✕ to the ribbon
       instead of letting it scroll away with the form. It is safe here for the reason the
       radius rule below is unnecessary: at full bleed there is no rounded clip for the
       transparent background to read through as a hairline down the ribbon's corners.
   (5) `min-height: 0` has to be restated on the ribbon or it is 580px tall: `height` loses
       to a larger inherited `min-height`, silently and with nothing in the box model to
       blame. The wrap needs its own for the grid-row equivalent of the same reason.
   (6) The vertical padding on the fields is set with `padding-top`/`padding-bottom` and
       never the shorthand. `.auth-form select` needs its 44px of chevron room and
       `.pw-wrap input` its 48px of peek room, and a shorthand at equal specificity, later
       in the sheet, quietly takes both away — the chevron lands on the country name and the
       password runs under the eye.
   (7) The h3 is HIDDEN, not removed: it is a keyed string in `index.html` and the tabs
       right above it already say "Log in" / "Register", so on a phone it is a heading that
       repeats the control that got you here. Taking the node out is a translation sweep for
       nothing. */
@media (max-width: 760px) and (min-height: 620px) {
  .account-dialog[open] { width: 100%; max-width: none; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; grid-template-columns: 1fr; grid-template-rows: auto minmax(0,1fr); overflow: hidden; }

  /* The hairline is load-bearing, not trim: `.account-form-wrap`'s gradient OPENS on
     `#fffdf7`, which is this ribbon's colour exactly, so without a bottom border the strip
     has no edge at all and the ✕ floats over an unbroken sheet of cream. `#efe4cd` is the
     site header's own border colour, so the edge is the header's edge too. */
  .account-visual { display: flex; flex-direction: row; align-items: center; gap: 11px; min-height: 0; height: auto; padding: 12px 18px; justify-content: flex-start; border-radius: 0; color: var(--ink); background: var(--paper); border-bottom: 1px solid #efe4cd; }
  .account-visual .av-scene, .account-visual .av-greeting { display: none; }
  /* The anchor carries `.brand-light` (0,1,0) for the navy panel, and base `.brand` is
     (0,1,0) too and LOSES to it — so recolouring the ribbon alone leaves a `#fdf6e6`
     wordmark on cream: invisible, with no error and nothing to fail. This selector is
     (0,2,0) and is what actually beats it; `.brand-word` is `color: inherit` and follows,
     and the `em` and the three spines are already the site's own colours. */
  .account-visual .brand { color: var(--ink); }
  /* The mark is the site's, scaled down rather than redrawn. The base rule is on
     `.account-brand-mark`, which this markup does not carry — it is a plain `.brand-mark`
     inside `.account-visual` — so the origin and the 6px it inherits are restated here or
     the ribbon's mark sits at full size and one text-line low. The BOX is shrunk with it:
     a transform draws smaller and reserves the same 44px, which is 12px of ribbon — and a
     ribbon is 12px of the form's own height — bought for nothing. The three spines are
     absolutely positioned off the box's BOTTOM, so the origin has to be the bottom too or
     the scaled mark lifts off the row it is standing on. */
  /* It is the SITE HEADER's logo, at the site header's phone size — a 44px mark and a 20px
     wordmark, drawn by the base `.brand-*` rules with nothing restated (2026-08-14). It was
     `height: 32px` + `scale(.72)`, which bought ~12px of ribbon back off a form that was
     scrolling: a phone met a different, smaller logo the moment it opened the register
     dialog. The 12px is paid for several times over by the email + country pairing and the
     one-line consents below. The two COLOUR swaps that used to be excepted here are gone
     with it (2026-08-15) — see `.account-visual`'s own comment: it is the site's mark in the
     site's colours at every width, faint middle spine included. */
  .account-visual .brand-mark { margin-bottom: 0; }
  .account-visual .brand-word { display: block; font-size: 20px; }
  /* Only the POSITION is this band's now. The ✕ used to be lifted to a cream-on-navy puck
     here because the strip under it was dark; with the ribbon in the header's own cream
     (see this band's heading) that treatment is a `rgba(253,246,230,.9)` glyph on cream —
     the same invisible failure the wordmark would have been — so the base
     `.account-dialog .dialog-close` treatment stands and both overrides are gone. It keeps
     its 34px box: the design's 30px is a drawing, and that box is the tap target rather
     than the decoration. */
  .account-dialog .dialog-close { top: 10px; right: 12px; }
  /* No twinkles. The one that was left landed just right of the wordmark, where the site
     header has nothing at all — so the ribbon read as a second, gold-starred version of the
     logo rather than the logo (2026-08-14). The mark carries its own ✦ already. */
  .av-twinkle { display: none; }

  /* `.is-login` is restated at equal specificity to beat the base rule's `center` — a short
     login form centred in a full-height column sits under a third of a screen of cream. */
  .account-form-wrap, .account-dialog.is-login .account-form-wrap { justify-content: flex-start; align-items: stretch; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 14px 20px calc(20px + env(safe-area-inset-bottom)); }
  /* The tabs are a SEGMENTED SWITCHER here, not the desktop swash-underline pair
     ("Register Modal.dc.html", 3a — the picked direction). A dashed cream track with the
     active side raised as a tilted paper chip, so on a phone the pair never reads as two
     more fields above a form of them, which the underlined words did at 19px in a column
     of 47px boxes. Five things are load-bearing.

     (1) The underline is TURNED OFF, not restyled — `::after` is an absolutely positioned
         bar the base rule paints, and left on it draws a coral smear under a chip that is
         already saying which side is active.
     (2) The two sides tilt OPPOSITE ways and carry the colour of what they open: navy for
         the log-in side, coral for the register one, each with its own 4px paper-edge
         shadow. The design's own numbers (`tabStyle` in that file): ±2deg, -2px, a
         `0 4px 0 <edge>` sitting on `0 8px 15px rgba(13,47,67,.13)`.
     (3) The corner radii are asymmetric and MIRRORED between the two, which is what makes
         a chip read as torn paper rather than as a button; the track's own four corners
         are all different for the same reason.
     (4) `min-height: 42px` is a tap target, and the pair is `flex: 1` each — a switcher
         sized to its words puts "Register" and "Log in" at different widths and the raised
         chip then moves the track's centre when it changes side.
     (5) The ✦ marks the active side for anyone who cannot see the raise (it is `opacity`,
         not `display`, so nothing reflows on a tab switch) and is `aria-hidden`: the
         button's own text is what a screen reader reads, and `.active` is on the button. */
  .account-tabs { display: flex; gap: 5px; margin: 0 0 15px; padding: 4px; border-radius: 17px 21px 16px 20px; background: #f1e4c9; border: 1.5px dashed #dcc99f; box-shadow: inset 0 2px 4px rgba(120,90,20,.07); }
  .account-tabs button { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 42px; padding: 0; border-radius: 12px 19px 11px 18px; background: transparent; font-size: 16.5px; color: #a3906a; transition: transform .18s cubic-bezier(.2,1.3,.5,1), box-shadow .18s ease, background .18s ease, color .18s ease; }
  .account-tabs button:nth-child(2) { border-radius: 19px 12px 18px 11px; }
  .account-tabs button::after { display: none; }
  .account-tabs button.active { background: var(--paper); color: var(--blue); box-shadow: 0 4px 0 #9fc2c9, 0 8px 15px rgba(13,47,67,.13); transform: rotate(-2deg) translateY(-2px); }
  .account-tabs button:nth-child(2).active { color: var(--coral-dark); box-shadow: 0 4px 0 var(--coral-dark), 0 8px 15px rgba(13,47,67,.13); transform: rotate(2deg) translateY(-2px); }
  .account-tabs .tab-star { display: inline; font-size: 11px; font-style: normal; color: var(--gold); opacity: 0; transition: opacity .18s ease; }
  .account-tabs button.active .tab-star { opacity: 1; }
  .auth-form h3 { display: none; }
  .auth-form.active { gap: 11px; }
  .auth-form[data-auth-form="login"].active { gap: 13px; }
  .auth-form .lbl { font-size: 13px; }
  .auth-form .forgot { font-size: 12px; }
  .auth-form input, .auth-form select { min-height: 47px; padding-top: 12px; padding-bottom: 12px; border-radius: 12px; }
  /* The shorter box's number, restated for the trigger — it has no vertical padding to take
     the 4px off, so without this the two halves of the email + country row come out 47 and
     53, which is the mismatch a phone actually sees. */
  .cs-trigger { min-height: 47px; border-radius: 12px; }
  .auth-name-row { gap: 11px; }
  .auth-form .button { min-height: 48px; }
  .auth-form[data-auth-form="login"] .button { min-height: 50px; }
  .auth-form .foot { display: flex; flex-direction: column; gap: 7px; margin-top: 0; }
  .auth-form .foot .button { margin-top: 0; }
  .auth-switch { margin: 0; font-size: 12.5px; }
  /* There is ONE consent now (2026-08-17): the marketing tick went, and the `+` pull-up that
     bought the design's 7px between the two went with it — a rule matching a sibling that
     cannot exist is a rule nobody would notice had stopped applying.
     The sentence is short enough to sit BESIDE its box on one line here, so the row is
     centred on it rather than hung off its top — an `align-items: flex-start` with a 2px
     nudge is for text that wraps under itself, and it reads as a tick sitting high the
     moment it does not. A translation long enough to wrap still lays out, it just wraps
     around a centred box. */
  .auth-form .register-consent { gap: 9px; align-items: center; }
  .auth-form .register-consent input { width: 18px; height: 18px; min-height: 18px; margin-top: 0; }
  .auth-form .register-consent span { font-size: 12px; line-height: 1.4; }
}

@media (max-width: 600px) {
  .hero-visual { min-height: 332px; --cover-scale: .82; }
}

/* This band used to swap the country's NAME for its code — the squeeze that let a phone
   bar hold the wordmark, both account buttons and the country pill. Two things retired it:
   the account block lives in the drawer at every width below 1100 now, and the pill states
   the ISO code at every width anyway. Nothing here to undo further down the sheet. */

/* The smallest phones still in the wild (320px). The caret goes: the pill is plainly a
   control without it, and the row has no pixels to spare. */
@media (max-width: 360px) {
  .country-picker { padding: 6px 7px; gap: 3px; }
  #openLogin, #openRegister { padding: 9px 11px; }
}

@media (max-width: 470px) {
  .hero-actions { flex-direction: column; }
  .hero-actions .hero-cta, .hero-actions .hero-ghost { width: 100%; justify-content: center; }
  .hero-visual { min-height: 285px; --cover-scale: .7; }
  .upload-button { display: none; }
  .field-row { grid-template-columns: 1fr; gap: 0; }
  .format-options { grid-template-columns: 1fr; }
  .book-includes div { grid-template-columns: 1fr; }
  .book { width: 225px; height: 305px; }
  .book-stage { height: 385px; }
  .order-content { grid-template-columns: 1fr; }
  .order-thumb { width: 155px; height: 205px; margin: auto; }
  .order-heading h2 { font-size: 32px; }
}

@media (max-width: 400px) {
  .hero-visual { min-height: 238px; --cover-scale: .58; }
}

/* --- The header drawer ---------------------------------------------------------
   The bar below 1100px is the brand and the hamburger; everything else — the five
   nav links, the country pill and the account buttons — is one drop panel, which is
   `.header-tools` changing from `display: contents` into a real box. Nothing moves in
   the DOM, so every id app.js binds is exactly where it was.

   Five things are load-bearing. (1) This block sits at the FOOT of the sheet on
   purpose: the 1300/760/560/360 bands above it shrink the same controls to fit a bar
   they are no longer in, and equal-specificity rules are decided by file order, so the
   undo has to come last (the id-carrying ones are matched at the same weight through
   `.header-tools .header-account #openRegister`). (2) The panel is positioned against
   `.site-header`, which is `position: sticky` and therefore already a containing block
   — do not add a `position: relative` and do not make the bar `overflow: hidden`, or
   the panel is clipped to a 64px bar. (3) It is `display: none` when closed rather
   than an animated height: the reduced-motion block kills every transition on this
   site, so a panel whose closed state was only a collapsed height would sit open. (4)
   The hamburger's bars are transformed on the element, for the same reason — the ✕ is
   a state, not a frame. (5) `#countryPicker[hidden]` and `.account-link[hidden]` still
   have to bite inside the panel: both declare a `display`, so either the panel leaves
   the attribute rule winning or the sheet says so out loud — `.account-link[hidden]`
   is written in the base sheet for exactly that reason, because this block gives both
   logged-out pills a `display: flex`.

   The panel's CONTENTS are "Header Mobile.dc.html" (2026-08-09): the five nav links are
   tiles rather than a text column, and the logged-out pair is one row. Two of them have been
   redrawn since by "Country Selector Mobile.dc.html" (2026-08-17) — the country pill, which
   is fine print now, and `Create a book`, which is a green pressable rather than a coral
   tile — and each is stated where it is built, further down. Three departures
   from that design are deliberate. (1) **There is no gold "My books" tile.** The account
   chip's menu already carries that row and is admin-aware there (an admin reads "Admin
   console" and goes to `/admin`); a tile beside it is two routes to one place in a 390px
   panel, and it would need markup, a `hidden` of its own on every `updateAccountUI` and a
   close-on-click decision, against a drawer whose first invariant is that nothing moves in
   the DOM. (2) **The country pill stays, and it has a design of its own now.** "Header
   Mobile" drew no such control, and this comment read "though the design draws no such
   control" until 2026-08-17: "Country Selector Mobile.dc.html" is that drawing, and the
   pill is a line of fine print under the buttons here — see the block on the account row
   below. It stays because it is what prices the page and answers the printed-book shipping
   gate, and below 1100px this panel is the only place it exists. (3) **The menu rows keep their plain text** — the
   design gives My account and Log out an icon each, and those rows are shared with the
   desktop bar, where the design is the "Account Chip" one that has none. */
@media (max-width: 1100px) {
  /* A ROUND pill, and the bars are thin and evenly spaced (2026-08-20). It was a 42px
     square with a 14px radius, a 1.5px tan border and three 2.5px bars 4.75px apart —
     2.25px of paper between them — which rendered as a dense ≡ glyph in a hard-edged box
     beside a brand mark and a header that are all soft corners, the only right angle on
     the bar. Three things are load-bearing. (1) The radius is `999px`, the `.hero-ghost`
     pill's own figure, not a large fixed number — the button is square, so half its side
     is the circle and a fixed radius has to be re-picked if the size ever moves. (2) The
     bars are `currentColor` over the button's `color`, so the hover shifts all three with
     the border in ONE declaration; they were `var(--ink)` and could not follow it. (3)
     The 5.5px stride is what the ✕ transform translates by — the two figures are the same
     number in three places and moving one alone leaves the close state off-centre. */
  .nav-toggle { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 44px; height: 44px; padding: 0; border: 1.5px solid #e6d8bb; border-radius: 999px; background: rgba(255, 253, 247, .7); color: var(--blue); cursor: pointer; transition: border-color .18s, background .18s, transform .2s cubic-bezier(.34, 1.56, .64, 1); }
  .nav-toggle:hover { border-color: var(--blue); background: rgba(255, 255, 255, .92); }
  .nav-toggle:active { transform: scale(.93); }
  .nav-toggle:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
  .site-header.nav-open .nav-toggle { border-color: var(--blue); background: #fff; }
  .nav-toggle-bars { position: relative; display: block; width: 19px; height: 13px; }
  .nav-toggle-bars i { position: absolute; left: 0; width: 100%; height: 2px; border-radius: 999px; background: currentColor; transition: transform .2s, opacity .15s; }
  .nav-toggle-bars i:nth-child(1) { top: 0; }
  .nav-toggle-bars i:nth-child(2) { top: 5.5px; }
  .nav-toggle-bars i:nth-child(3) { top: 11px; }
  .site-header.nav-open .nav-toggle-bars i:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
  .site-header.nav-open .nav-toggle-bars i:nth-child(2) { opacity: 0; }
  .site-header.nav-open .nav-toggle-bars i:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

  /* Opaque, and carrying NO `backdrop-filter` of its own: the bar above it is already a
     blurred surface, and a second filtered layer nested inside the first renders the
     page straight through this panel — the menu reads as printed over the hero. */
  .header-tools { display: none; position: absolute; top: calc(100% + 10px); left: 0; right: 0; padding: 14px; border: 1px solid #efe4cd; border-radius: 20px; background: #fffdf7; box-shadow: 0 22px 50px -22px rgba(10, 42, 59, .38); }
  .site-header.nav-open .header-tools { display: block; }

  /* The links are TILES ("Header Mobile.dc.html"): "Create a book" across the top — a green
     button rather than a tile since "Country Selector Mobile.dc.html", see its own block
     below — and the other four as a 2×2 of tinted cards. They were a plain column of five
     text rows — five equal words where one of them is the thing the whole site is for.

     Five things are load-bearing. (1) **Nothing moves in the DOM, still.** The nav is a grid
     here and `Create a book` is pulled to the front with `order: -1` + `grid-column: 1 / -1`,
     so the desktop row keeps its source order (How it works, Examples, Create, Pricing, FAQ's)
     and the drawer reads the design's. (2) **The tiles are selected by their `href`**, never
     `:nth-child` — the nav is five links whose order is a marketing decision, and a positional
     selector re-tints the wrong card silently the day one is moved or a sixth is added.
     (3) **The icons are `::before` background images and CANNOT be markup.** Every one of
     these anchors carries `data-i18n`, and `applyI18n` writes `textContent` on a language
     switch — an <svg> child would survive exactly until somebody picked German. That is also
     why each icon's colour is baked into its data URI rather than inherited: a background
     image has no `currentColor`. (4) **The hover lift the design draws is deliberately not
     built** — the background change is kept, the `translateY(-2px)` is not: this panel is a
     thumb surface, `:hover` sticks on touch, and a row that tilts after the tap reads as the
     tap having missed. `:active` carries the press instead, and the CTA below says the same
     thing again about its own plinth, which its design also draws on a hover. (5) The `#` in every colour has
     to be written `%23` inside a data URI, or the rest of the URL is read as a fragment and
     the icon is simply absent. */
  .header-tools .desktop-nav { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 9px; flex: none; }
  .header-tools .desktop-nav a { display: flex; align-items: center; gap: 9px; min-width: 0; padding: 10px; border: 1.5px solid #efe3c9; border-radius: 15px 11px 16px 12px; background: #fffdf7; font-size: 14px; font-weight: 600; line-height: 1.2; color: var(--ink); white-space: normal; transition: background .16s, transform .12s; }
  .header-tools .desktop-nav a::after { display: none; }
  .header-tools .desktop-nav a::before { content: ""; flex: none; width: 32px; height: 32px; border-radius: 11px 9px 12px 10px; background-repeat: no-repeat; background-position: center; background-size: 17px 17px; }
  .header-tools .desktop-nav a:hover { transform: none; background: #fdf4e2; }
  .header-tools .desktop-nav a:active { transform: translateY(1px) scale(.985); }

  /* The one row that is a BUTTON rather than a tile ("Country Selector Mobile.dc.html",
     2026-08-17). It was the coral card "Header Mobile" drew, left-aligned behind a tinted
     pencil chip like the four rows under it — which made the thing the whole site is for
     the widest of five cards rather than the one control on the panel. It is the green
     pressable now: a 56px slab across the top, the display face at 19px, a ✦ either side,
     and a `0 6px 0` plinth, which is what says "press me" on a surface that has no hover.

     Four things are load-bearing. (1) **The press is `:active` and never `:hover`.** The
     design draws the plinth collapsing on hover; this panel exists only below 1100px, so it
     is a thumb surface by construction and a hovered state STICKS after the tap — item (4)
     of the block above, which this button is the reason for stating twice. The tiles'
     generic `:active` (a translate and a scale) would fight a plinth, so this one states
     its own: the box drops 3px and the shadow loses the same 3px, so the button's bottom
     edge does not move and only the slab does. `:hover` is still written, because without
     it the tile rule paints this button the cream every other row hovers to; it restates
     the green and changes nothing else, since the design gives the colour no hover state
     and an invented darker green is a token this site does not have. (2) **The stars are
     `::before` and `::after` glyphs and cannot be markup** — this anchor carries
     `data-i18n` and `applyI18n` writes `textContent` on a language switch, so a `<span>`
     star would survive exactly until somebody picked German, the same reason the tiles'
     icons are backgrounds. `::before` arrives from the tile rule as a 34px tinted box, so
     every one of its declarations is undone here rather than added to. `::after` arrives
     from TWO rules and both have to be undone: the drawer turns it off, so `display` is
     written out loud, and the base sheet's is the desktop link's hover underline — absolute,
     pinned to the bottom edge and at `opacity: 0` — so `position`, `inset` and `opacity` are
     restated or the star is drawn invisibly along the button's foot. The glyph is the site's own
     U+2726 `✦` (`.brand-mark b`), written down because a hand-picked star drifts silently —
     `EmailHtml` shipped U+2736 once for exactly that reason. (3) The `margin-bottom` is what
     makes the 18px the design puts above the 2×2 out of a 9px grid gap, and it is also the
     clearance the plinth needs: the shadow lies 6px BELOW the layout box, so a bare gap
     leaves it 3px into the tiles. (4) `box-shadow` is in the transition list — the tile
     rule's transition covers `background` and `transform` only, and a plinth that snaps
     while the box glides reads as the button breaking rather than being pressed. */
  .header-tools .desktop-nav a[href="#creator"] { order: -1; grid-column: 1 / -1; justify-content: center; min-height: 56px; margin-bottom: 9px; padding: 0 14px; border: 0; border-radius: 18px 22px 17px 21px; background: var(--green); color: #fdf6e6; font-family: var(--display); font-size: 19px; font-weight: 700; letter-spacing: .2px; box-shadow: inset 0 0 0 2px rgba(253, 246, 230, .28), 0 6px 0 #2f5244; transition: background .16s, transform .12s, box-shadow .12s; }
  .header-tools .desktop-nav a[href="#creator"]:hover { background: var(--green); transform: none; }
  .header-tools .desktop-nav a[href="#creator"]:active { transform: translateY(3px); box-shadow: inset 0 0 0 2px rgba(253, 246, 230, .28), 0 3px 0 #2f5244; }
  .header-tools .desktop-nav a[href="#creator"]::before,
  .header-tools .desktop-nav a[href="#creator"]::after { content: "✦"; display: block; position: static; inset: auto; opacity: 1; flex: none; width: auto; height: auto; border-radius: 0; background: none; transform: none; color: var(--gold-light); font-size: 14px; line-height: 1; }

  .header-tools .desktop-nav a[href="#how-it-works"]::before { background-color: rgba(233, 169, 43, .18); transform: rotate(-5deg);
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23b47f12" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z"/><path d="M18.5 15.5l.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7z"/></svg>'); }
  .header-tools .desktop-nav a[href="#examples"] { border-radius: 11px 15px 12px 16px; }
  .header-tools .desktop-nav a[href="#examples"]::before { border-radius: 9px 12px 10px 11px; background-color: rgba(14, 61, 86, .13); transform: rotate(4deg);
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%230e3d56" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 6.5C10.5 5 8.3 4.6 4 4.8v13c4.3-.2 6.5.2 8 1.7 1.5-1.5 3.7-1.9 8-1.7v-13c-4.3-.2-6.5.2-8 1.7z"/><path d="M12 6.5V19"/></svg>'); }
  .header-tools .desktop-nav a[href="#pricing"] { border-radius: 12px 16px 11px 15px; }
  .header-tools .desktop-nav a[href="#pricing"]::before { border-radius: 12px 10px 9px 11px; background-color: rgba(69, 111, 92, .16); transform: rotate(3deg);
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23456f5c" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12.8L12.7 20.6a1.8 1.8 0 01-2.5 0l-6.8-6.8a1.8 1.8 0 01-.5-1.3l.3-6a1.8 1.8 0 011.7-1.7l6-.3c.5 0 1 .2 1.3.5l6.8 6.8a1.8 1.8 0 010 2.5z"/><circle cx="8.4" cy="8.4" r="1.4"/></svg>'); }
  .header-tools .desktop-nav a[href="#faq"] { border-radius: 16px 12px 15px 11px; }
  .header-tools .desktop-nav a[href="#faq"]::before { border-radius: 10px 11px 12px 9px; background-color: rgba(235, 117, 91, .16); transform: rotate(-3deg);
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23d75b45" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.4 9.2a2.7 2.7 0 015.2.9c0 1.8-2.6 2.2-2.6 4"/><path d="M12 17.3h.01"/></svg>'); }

  /* The account row, with everything the bar had shortened put back: there is room for
     the portal button's words in a panel this wide. The country pill is a flag and an ISO
     code at every width now, so there is nothing left here to restore for it. */
  .header-tools .header-account { flex: none; flex-wrap: wrap; gap: 10px; margin-top: 12px; padding-top: 13px; border-top: 1px dashed #e0cfa8; }
  /* The SHELL is the flex item now, and the pill fills it — put `flex: 1 1 100%` on the
     button and the shell stays content-width, so the row breaks. The drop panel goes
     full-width with it: pinned to a right edge that is now the panel's own, a fixed
     352px would hang off a 320px screen.

     In here the pill is FINE PRINT ("Country Selector Mobile.dc.html", 1b): the same
     control with its border, its background and its caret off, sitting UNDER the two
     buttons and stating what the visitor is priced in, with a dashed `Change` beside it.
     It was the bar's pill stretched to the panel's full width, so a two-letter ISO code
     sat in an input-shaped box between the nav tiles and the account row — the one thing
     in a menu that looked like a form field, and the one control in it whose label said
     nothing about what it does. Six things are load-bearing.

     (1) **The design's native `<select>` overlay is deliberately NOT built.** The mock
     stretches a real select over the line ("so a phone still opens its own country
     sheet") because a design canvas has no `.cs-panel` to draw; on this site that overlay
     was REMOVED on 2026-08-07 so the two places a visitor names their country offer one
     control, and checkout-and-header.md states in as many words that putting it back is a
     trade, not a fix. The pill stays a <button> and the searchable panel stays the sheet.

     (2) **The sentence names the currency, which the panel's own hint used to.** That hint
     ("Prices & shipping follow your country.") was taken out on 2026-08-12 at the owner's
     request; this says the same kind of thing on a different surface and at the design's
     ask, so it is a decision rather than a revival — and it is a new key, never the
     deleted one.

     (3) **`order: 1` is what puts it under the buttons**, so nothing moves in the DOM —
     this drawer's first invariant, said for the third time. It lands under the logged-out
     pair and under the account chip alike, both of which sit at the default 0.

     (4) **Every state has to be flattened, not just the resting one.** `:hover` and
     `.is-open` each repaint the border and the background in the base sheet, and `:hover`
     sticks on touch — left alone, the line grows a box the moment it is tapped. The open
     feedback is the panel dropping full-width directly beneath it; `:focus-visible` keeps
     the base sheet's outline, which is the only ring left.

     (5) **`flex-wrap: wrap` with `justify-content: center`**: at 320px the sentence and
     `Change` do not fit on one line, and a nowrap row would push the panel open.

     (6) **The gap is what separates the sentence from `Change`** — never a trailing space
     in either string. A `.properties` value loses a leading one, so the two would run
     together in the ten translated catalogs and nowhere else. */
  .header-tools .header-account .country-picker-shell { flex: 1 1 100%; order: 1; }
  .header-tools .header-account .country-picker { width: 100%; min-height: 44px; justify-content: center; flex-wrap: wrap; gap: 6px; padding: 13px 0 11px; border: 0; border-radius: 0; background: none; }
  .header-tools .header-account .country-picker:hover,
  .header-tools .header-account .country-picker.is-open { border-color: transparent; background: none; box-shadow: none; }
  .header-tools .header-account .country-picker-flag { font-size: 14px; }
  .header-tools .header-account .country-picker-name,
  .header-tools .header-account .country-picker-caret { display: none; }
  .header-tools .header-account .country-picker-line { display: inline; color: var(--ink-soft); font-size: 12px; font-weight: 400; letter-spacing: 0; }
  .header-tools .header-account .country-picker-change { display: inline; color: var(--coral-dark); font-size: 12px; text-decoration: underline; text-decoration-style: dashed; text-underline-offset: 3px; }
  .header-tools .header-account #countryPicker .cs-panel { left: 0; right: 0; width: auto; }
  /* The logged-out pair is one row of two ("Header Mobile.dc.html"), Register the wider of
     the two because it is the one being asked for. `flex: 1.15 1 0` and `flex: 1 1 0` are the
     design's `grid-template-columns: 1fr 1.15fr` said in the flex row that is already here —
     a grid would need a box around the pair, and the country pill and the chip are flex items
     of this same row. Both keep their `[hidden]`: each declares a `display` now, and the rule
     that lets the attribute win is in the base sheet beside `#openRegister`, which has had
     one since long before this drawer existed. The icons are `::before` data URIs for the
     reason the nav tiles' are — both buttons carry `data-i18n`. */
  .header-tools .header-account .account-link { display: flex; align-items: center; justify-content: center; gap: 8px; flex: 1 1 0; min-width: 0; padding: 12px 10px; font-size: 15px; }
  .header-tools .header-account .account-link::before { content: ""; flex: none; width: 16px; height: 16px; background-repeat: no-repeat; background-position: center; background-size: 16px 16px; }
  .header-tools .header-account #openLogin { border-color: #e0d2b2; background: #fffdf7; }
  .header-tools .header-account #openLogin:hover { background: #f7eeda; }
  .header-tools .header-account #openLogin::before { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%230e3d56" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 21.5H5.5A1.5 1.5 0 014 20V4a1.5 1.5 0 011.5-1.5H10"/><path d="M15.5 15.7l3.2-3.2m0 0L15.5 9.3m3.2 3.2h-11"/></svg>'); }
  .header-tools .header-account #openRegister { flex: 1.15 1 0; box-shadow: 0 10px 22px -12px rgba(47, 82, 68, .95); }
  .header-tools .header-account #openRegister:hover { transform: none; }
  .header-tools .header-account #openRegister::before { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23fff8f1" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.6l2.2 4.5 4.9.7-3.5 3.5.8 4.9L12 15l-4.4 2.2.8-4.9L4.9 8.8l4.9-.7z"/></svg>'); }
  /* The chip is the panel's own account row: full width, with the name given its length
     back — this panel is where the country pill gets its whole name too, and for the same
     reason. The design's ≤720 monogram-only bar is deliberately NOT built: below 1100 the
     chip is in here, where there is room for all of it. The menu still drops from the chip
     (`position: absolute`) and draws over the panel, which is why the drawer's
     close-on-click exempts `.acc-chip` — closing the panel would take the menu with it. */
  /* `min-width: 0` is what lets the name ellipsise instead of pushing the panel open: a
     flex item's automatic minimum is its MIN-CONTENT, and an uncapped name here is 194px
     of unbreakable text — measured, "Priyanka Ramasubramanian" put the chip 23px past the
     panel's own edge at 320px. The stack's own `min-width: 0` cannot help, because the
     overflow is one box further out. */
    /* "My books" in the drawer is a ROW, not a 40px circle: there is width here for the words
     the bar has no room for, and a lone icon in a list of labelled rows is the one item a
     reader has to guess at. Same node, same id — this drawer's first invariant. It sits
     above the chip on the default `order: 0`, which is where the markup already puts it. */
  .header-tools .header-account .header-books { flex: 1 1 100%; width: auto; height: auto; justify-content: flex-start; padding: 12px 16px; border-radius: 16px 13px 15px 12px; }
  .header-tools .header-account .header-books:hover { transform: none; }
  .header-tools .header-account .header-books .hb-label { display: inline; }
  .header-tools .header-account .acc { flex: 1 1 100%; min-width: 0; }
  .header-tools .header-account .acc-chip { width: 100%; padding: 6px 14px 6px 6px; }
  .header-tools .header-account .acc-name { max-width: none; }
  .header-tools .header-account .acc-caret { margin-left: auto; }
  .header-tools .header-account .acc-menu { left: 0; right: 0; min-width: 0; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
}

/* --- Full-screen generation overlay (collapse to a single progress bar) --- */
.gen-overlay { position: fixed; inset: 0; z-index: 120; display: none; align-items: center; justify-content: center; padding: 26px; overflow-y: auto;
  background: radial-gradient(circle at 28% 18%, #fdf3dd, #f3e7cf 52%, #ecd9b6); }
.gen-overlay.show { display: flex; }
.gen-overlay[hidden] { display: none; }
/* margin:auto, not the flex centering alone: once the sheet-approval panel makes the
   content taller than a short viewport, auto margins collapse to 0 and the overlay
   scrolls to everything — flex centering alone clips both ends unreachably. */
.gen-overlay-inner { width: min(640px, 92vw); text-align: center; margin: auto; }
.gen-eyebrow { display: inline-block; color: #9a6a12; font-weight: 800; font-size: 12px; letter-spacing: .16em; text-transform: uppercase; margin-bottom: 14px; }
.gen-stage { display: block; font-family: var(--display); font-size: clamp(24px, 4.6vw, 38px); color: var(--ink); margin-bottom: 30px; line-height: 1.15; }
.gen-track { position: relative; height: 52px; border-radius: 999px; background: #fff7e6;
  box-shadow: inset 0 3px 10px rgba(0,0,0,.13), 0 1px 0 #fff; }
.gen-fill { height: 100%; width: 3%; border-radius: 999px; background: linear-gradient(90deg, var(--coral), var(--gold));
  box-shadow: 0 2px 10px rgba(233,169,43,.5); transition: width .45s ease; }
/* The icon travels LEFT TO RIGHT along the track, so it has to face right. Most animal
   and vehicle emoji are drawn in left profile and are mirrored (.is-mirrored, set by
   setGenPusherIcon in app.js); everything else — 🚀 already points up-right, ✨/🏰/👑
   have no facing at all — is left upright, which is why the mirror is the VARIANT and not
   the base rule. The bob keyframes carry the transform, so a flip applied to the base rule
   alone would be overwritten every frame: there are two animations, named apart because a
   duplicate @keyframes name in this sheet is last-wins with no warning. */
.gen-pusher { position: absolute; top: 50%; left: 3%; font-size: 54px; line-height: 1; pointer-events: none;
  transform: translate(-45%, -58%); transition: left .45s ease; filter: drop-shadow(0 3px 3px rgba(0,0,0,.22));
  animation: genBob .8s ease-in-out infinite alternate; }
.gen-pusher.is-mirrored { transform: translate(-45%, -58%) scaleX(-1); animation-name: genBobMirrored; }
@keyframes genBob { from { transform: translate(-45%, -64%) rotate(5deg); } to { transform: translate(-45%, -50%) rotate(-5deg); } }
@keyframes genBobMirrored { from { transform: translate(-45%, -64%) scaleX(-1) rotate(-5deg); } to { transform: translate(-45%, -50%) scaleX(-1) rotate(5deg); } }
/* The wrapper is inert until the cast pause — see .is-approving below, which is the only
   thing that gives it a box of its own. */
.gen-progress { display: contents; }
.gen-meta { display: flex; justify-content: space-between; align-items: center; margin: 18px 6px 10px; color: #7a6a4a; font-weight: 800; font-size: 15px; }
.gen-meta #genPercent { font-size: clamp(30px, 4.4vw, 44px); font-family: var(--display); color: var(--ink); }
.gen-caption { display: none; }
/* Leaving the overlay hides it; the run keeps going in this tab. */
.gen-minimise { border: 2px solid #e4d6b6; background: rgba(255, 255, 255, .7); color: #7a6a4a; font-weight: 800; font-size: 14px; border-radius: 999px; padding: 10px 20px; cursor: pointer; }
.gen-minimise:hover { background: #fff; color: var(--ink); }
.gen-minimise[hidden] { display: none; }
/* The guardian's sheet-approval pause — "Character Check Options" 1a, "the cast call".
   Every child here carries its own top margin instead of the grid gap, because the
   question and its footnote are a pair (22px above, 7px between) and a single gap cannot
   say that. */
.gen-approve { margin-top: 22px; display: grid; justify-items: center; }
.gen-approve[hidden] { display: none; }
/* The sheet is a photo taped into an album: the white border is the plate's own padding,
   so the frame belongs to the paper rather than to the image. The DESIGN's window
   geometry is deliberately not ported — it crops a fixed 330px window out of a known
   demo image, and a real sheet is whatever aspect the model returned with one to six
   figures on it. This pause exists so the guardian sees the WHOLE cast, so the image is
   contained, never cropped, and the vh cap is what keeps the actions on a short laptop
   viewport (the reason .gen-overlay-inner takes margin: auto). */
.gen-plate { position: relative; width: min(520px, 88vw); background: #fffdf7; border-radius: 6px; padding: 14px 14px 12px;
  box-shadow: 0 18px 40px rgba(61, 42, 12, .18); transform: rotate(-.5deg); }
/* width: 100% with height: auto takes the height from the sheet's own aspect ratio, and
   the vh cap then clamps a TALL one — object-fit: contain is what letterboxes it inside
   that clamped box instead of squashing the cast. The letterbox is the paper's own
   colour, so a tall sheet reads as a smaller photo on the same page. */
.gen-plate img { display: block; width: 100%; height: auto; max-height: 42vh; object-fit: contain; border-radius: 3px; background: transparent; }
.gen-tape { position: absolute; top: -11px; width: 74px; height: 22px; background: rgba(247, 217, 125, .62); border: 1px solid rgba(210, 175, 80, .35); }
.gen-tape-l { left: 34px; transform: rotate(-5deg); }
.gen-tape-r { right: 34px; transform: rotate(4deg); }
/* Four people were just invented for this family; these are what introduce them. Empty
   when nothing on the sheet could be named, and an empty chip row is a stray gap. */
.gen-cast { display: flex; justify-content: center; flex-wrap: wrap; gap: 7px; margin-top: 11px; }
.gen-cast:empty { display: none; }
.gen-cast-name { display: inline-flex; align-items: center; gap: 6px; border: 1.5px solid #efe0c2; border-radius: 999px; padding: 5px 12px;
  background: #fdf8ec; font: inherit; font-size: 12.5px; font-weight: 700; color: #7a6a4a; cursor: pointer; }
.gen-cast-name.is-target { border-color: var(--coral); background: #fbe3dd; color: var(--ink); box-shadow: 0 0 0 2px rgba(215,91,69,.12); }
.gen-cast-dot { width: 7px; height: 7px; border-radius: 50%; }
.gen-version { display: flex; align-items: center; gap: 10px; margin-top: 17px; color: #7a6a4a; }
.gen-version[hidden] { display: none; }
.gen-version button { width: 31px; height: 31px; border: 1.5px solid #e4d6b6; border-radius: 50%; background: #fffdf7; color: var(--ink); font: inherit; font-size: 20px; cursor: pointer; }
.gen-version button:disabled { opacity: .35; cursor: default; }
.gen-version strong { min-width: 112px; font-size: 12px; }
/* A question and its footnote, not two shouts: the ask is the display face at 20px and
   the consequence is small and grey under it. Nothing here is set in bold body copy. */
.gen-approve-ask { margin: 22px auto 0; max-width: 420px; font-family: var(--display); font-size: 20px; line-height: 1.35; color: var(--ink); }
.gen-approve-note { margin: 7px auto 0; max-width: 420px; font-size: 13px; font-weight: 600; line-height: 1.5; color: #8a7a5f; }
/* The two actions are NOT twins — a filled yes, and under it a quiet link. Stacked, so the
   pair cannot read as one grey-and-colour block. (It said "a warm coral yes" until
   2026-08-17; the yes is green now with every other filled CTA — see the note over
   `.button-coral`. `family.html`'s `.drawn-keep` is this same button redrawn and moved
   with it.) */
.gen-approve-actions { display: flex; flex-direction: column; align-items: center; gap: 12px; margin-top: 20px; }
/* The name fields on #jobClaim. It borrows .gen-approve wholesale — it is the same panel in
   the same overlay, shown by the same .is-approving switch — and adds only the rows, because
   a second full panel style would be one more thing to keep in step with the first. */
.gen-claim-fields { display: flex; flex-direction: column; gap: 10px; margin: 18px auto 0; max-width: 320px; text-align: left; }
.gen-claim-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; font-weight: 600; color: #8a7a5f; }
.gen-claim-field input { padding: 10px 12px; border: 1px solid #e3d9c6; border-radius: 10px; font: inherit; font-weight: 600; color: var(--ink); background: #fff; }
.gen-claim-field input:focus-visible { outline: 2px solid var(--coral); outline-offset: 1px; }
.gen-keep { min-height: 53px; display: inline-flex; align-items: center; gap: 14px; padding: 0 26px 0 12px; border: 0;
  border-radius: 7px 12px 8px 11px; background: var(--green); color: #fff; font: inherit; font-size: 16px; font-weight: 800; cursor: pointer;
  box-shadow: 0 14px 26px -12px rgba(47, 82, 68, .85); transition: transform .16s, box-shadow .16s; }
.gen-keep:hover { transform: translateY(-2px) rotate(-.6deg); box-shadow: 0 18px 30px -12px rgba(47, 82, 68, .9); }
.gen-keep-tick { width: 34px; height: 34px; flex: none; border-radius: 50%; background: rgba(255, 255, 255, .22); display: grid; place-items: center; font-size: 17px; }
/* redraws >= MAX_REDRAWS hides this, and a flex display beats the UA sheet's [hidden]. */
.gen-redo { display: inline-flex; flex-direction: column; align-items: center; gap: 1px; padding: 4px 8px; border: 0; background: none;
  color: #7a6a4a; font: inherit; cursor: pointer; }
.gen-redo[hidden] { display: none; }
.gen-redo-word { font-size: 14px; font-weight: 800; text-decoration: underline; text-decoration-color: #d9c9a6; text-underline-offset: 4px; }
.gen-redo:hover .gen-redo-word { color: var(--ink); text-decoration-color: var(--gold); }
.gen-redo small { font-size: 11.5px; font-weight: 600; color: #a4906f; }
.gen-redo-one { color: var(--coral-dark); }
.gen-redo-photo { color: #4a6a7a; }
/* Spent, not gone. A character with no redraws left keeps their button in place and greyed,
   because the alternative — three links that appear and vanish as the chips are clicked —
   reads as a broken panel rather than as a budget. The sub-line says which it is. */
.gen-redo:disabled { cursor: default; opacity: .55; }
.gen-redo:disabled .gen-redo-word,
.gen-redo:disabled:hover .gen-redo-word { color: inherit; text-decoration-color: #e2d7bf; }
/* While the guardian is judging, the DRAWING is the biggest thing on screen: the 52px
   trough becomes a 22px line sharing one row with a caption that says what the number
   measures — "15% of your book drawn", never "your approval — 15%", which labelled the
   book's progress with the guardian's decision. */
.gen-overlay.is-approving .gen-stage { margin-bottom: 0; }
.gen-overlay.is-approving .gen-progress { display: flex; align-items: center; gap: 14px; margin-top: 20px; }
.gen-overlay.is-approving .gen-track { flex: 1 1 auto; height: 22px; }
.gen-overlay.is-approving .gen-pusher { font-size: 34px; }
.gen-overlay.is-approving .gen-meta { flex: none; margin: 0; }
.gen-overlay.is-approving #genCount, .gen-overlay.is-approving #genPercent { display: none; }
.gen-overlay.is-approving .gen-caption { display: block; font-size: 13px; font-weight: 800; color: #7a6a4a; white-space: nowrap; }
/* The caption does not wrap, so on a narrow phone it eats the bar it is captioning —
   under 420px the two take a line each rather than the trough shrinking to a stub. */
@media (max-width: 420px) {
  .gen-overlay.is-approving .gen-track { flex: 1 0 100%; }
  .gen-overlay.is-approving .gen-progress { flex-wrap: wrap; }
  .gen-overlay.is-approving .gen-meta { flex: 1 0 100%; justify-content: center; margin-top: 8px; }
}
@media (max-width: 640px) { .gen-track { height: 36px; } .gen-pusher { font-size: 38px; } .gen-meta #genPercent { font-size: 28px; } }

/* The minimised run. z-index sits just UNDER .gen-overlay (120) so restoring buries it,
   and the keyframe is gen-mini-prefixed — a duplicate @keyframes name in this sheet is
   last-wins with no warning. */
.gen-mini { position: fixed; left: 18px; bottom: 18px; z-index: 119; display: flex; align-items: center; gap: 12px; width: min(340px, calc(100vw - 36px));
  padding: 12px 16px 16px; border: 2px solid #f0e0bd; border-radius: 18px; background: #fffaf0; box-shadow: 0 16px 38px rgba(61, 42, 12, .2);
  color: var(--ink); text-align: left; cursor: pointer; font: inherit; }
.gen-mini[hidden] { display: none; }
.gen-mini:hover { border-color: var(--gold); transform: translateY(-2px); }
.gen-mini-icon { font-size: 26px; animation: gen-mini-bob 2.4s ease-in-out infinite; }
.gen-mini-copy { display: grid; gap: 2px; min-width: 0; flex: 1; }
.gen-mini-copy strong { font-size: 14px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gen-mini-copy small { color: #8a7a5c; font-size: 12px; font-weight: 700; }
.gen-mini-percent { font-family: var(--display); font-size: 22px; color: #9a6a12; }
.gen-mini-track { position: absolute; left: 16px; right: 16px; bottom: 9px; height: 5px; border-radius: 999px; background: #f0e4c8; overflow: hidden; }
.gen-mini-fill { display: block; height: 100%; width: 3%; border-radius: 999px; background: linear-gradient(90deg, var(--gold), var(--coral)); transition: width .4s ease; }
@keyframes gen-mini-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }

/* The book still being made, shown in the portal beside the finished ones. */
.portal-book.is-generating { background: #fff6e2; border: 1px solid #f0dfb4; }
.portal-book.is-generating button { background: var(--gold); color: #4a3208; }
/* Two classes deep on purpose: .portal-book div is a flex row with margin-top:auto,
   and it outruns a bare .portal-progress. */
.portal-book .portal-progress { display: block; position: relative; height: 6px; margin: 0 0 10px; border-radius: 999px; background: #f0e4c8; overflow: hidden; }
.portal-book .portal-progress span { position: absolute; inset: 0 auto 0 0; display: block; font-size: 0; border-radius: 999px; background: linear-gradient(90deg, var(--gold), var(--coral)); transition: width .4s ease; }

/* Test-only shortcut: unlock the full book without paying (admin or ?test) */
.test-unlock { position: fixed; left: 16px; bottom: 16px; z-index: 130; padding: 10px 15px; border: 1px dashed var(--coral-dark); border-radius: 12px; background: #fff5f1; color: var(--coral-dark); font-family: var(--sans); font-weight: 800; font-size: 13px; cursor: pointer; box-shadow: 0 8px 22px rgba(215,91,69,.2); }
.test-unlock:hover { background: #fbe3dd; }

/* Read-aloud button states (loading / playing) */
.audio-player button[disabled] { opacity: .6; cursor: default; }
.audio-player button.is-loading { cursor: progress; }
.audio-player button.is-loading .audio-icon { display: inline-block; animation: spin 1s linear infinite; }
.audio-player button.is-playing { background: var(--coral); color: #fff; border-color: var(--coral); }

/* ---- Guest gate + example (sample) books ---- */
/* Ensure the hidden attribute always wins over author display rules
   (e.g. read-only sample reader must not show Approve/Edit buttons). */
[hidden] { display: none !important; }
.gated-hidden { display: none !important; }

.creator-gate {
  display: grid;
  grid-template-columns: 1fr;
  gap: 26px;
  align-items: start;
  max-width: 560px;
  margin: 0 auto;
}
@media (max-width: 820px) { .creator-gate { grid-template-columns: 1fr; } }

.gate-lock {
  background: var(--paper);
  border: 1px solid var(--border);
  border-radius: 22px;
  padding: 30px 28px;
  box-shadow: var(--shadow);
  text-align: center;
}
.gate-lock .gate-emoji { font-size: 34px; }
.gate-lock h3 { font-family: var(--display); color: var(--ink); font-size: 24px; margin: 12px 0 8px; }
.gate-lock p { color: var(--ink-soft); font-size: 15px; line-height: 1.65; margin: 0 0 20px; }
.gate-actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
.gate-actions .cta-button {
  border: 0; cursor: pointer; border-radius: 999px; padding: 13px 22px;
  font-size: 14px; font-weight: 800; color: #fff; background: var(--ink);
  box-shadow: 0 10px 22px rgba(13, 47, 67, .18);
}
.gate-actions .ghost-button {
  border: 1.5px solid var(--border); cursor: pointer; border-radius: 999px;
  padding: 13px 22px; font-size: 14px; font-weight: 800; color: var(--ink); background: transparent;
}
.gate-actions .cta-button:hover { background: var(--blue); }
.gate-actions .ghost-button:hover { border-color: var(--ink); }

.gate-examples > h4 { font-family: var(--display); color: var(--ink); font-size: 20px; margin: 4px 0 4px; }
.gate-examples > p { color: var(--ink-soft); font-size: 14px; margin: 0 0 16px; }

.examples-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
@media (max-width: 620px) { .examples-grid { grid-template-columns: repeat(2, 1fr); } }

.example-card {
  background: var(--paper);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 10px 10px 16px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 8px;
  box-shadow: 0 12px 30px rgba(10, 42, 59, .08);
}
.example-cover {
  aspect-ratio: 3 / 4;
  border-radius: 12px;
  overflow: hidden;
  background: var(--cream);
}
.example-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.example-card h4 { font-family: var(--display); color: var(--ink); font-size: 16px; margin: 4px 6px 0; line-height: 1.25; }
.example-card p { color: var(--ink-soft); font-size: 12px; margin: 0 6px; }
.example-card button {
  margin: 6px 8px 0; border: 0; cursor: pointer; border-radius: 999px;
  padding: 9px 12px; font-size: 12.5px; font-weight: 800; color: var(--ink);
  background: var(--gold-light);
}
.example-card button:hover { background: var(--gold); }

.example-gallery-head { text-align: center; margin-bottom: 20px; }
.example-gallery-head h2 { font-family: var(--display); color: var(--ink); margin: 6px 0 6px; }
.example-gallery-head p { color: var(--ink-soft); font-size: 14px; margin: 0; }
.example-dialog .examples-grid { max-width: 720px; }

/* ---- Page-flip (turning leaf) for the story reader ---- */
/* Perspective governs the flip's foreshortening. Too tight and the turning
   leaf looms big mid-flip (free edge swings ~1 page-width toward the camera)
   then snaps to true size on landing. 5200px keeps a subtle 3D arc without
   the oversized bulge. */
.story-reader { perspective: 5200px; }
.page-leaf {
  position: absolute;
  z-index: 9;
  pointer-events: none;
  transform-style: preserve-3d;
  transition: transform .62s cubic-bezier(.36, .1, .22, 1);
  /* Promote to its own compositor layer for the whole turn — without this the
     leaf re-rasterises each frame of the 3D rotate and flickers. */
  will-change: transform;
}
.page-leaf.next { transform-origin: left center; }
.page-leaf.prev { transform-origin: right center; }
/* Land perfectly flat (±180°, not ±178°) so the leaf ends coplanar with the
   real page beneath it — identical content, so removing it is invisible. A 2°
   short landing left a tilt that snapped flat on removal (the end flicker). */
.page-leaf.next.turning { transform: rotateY(-180deg); }
.page-leaf.prev.turning { transform: rotateY(180deg); }
/* NOTE: never put filter/opacity/overflow on .page-leaf — they flatten
   preserve-3d and break backface-visibility (mirrored text shows through).
   The shadow lives on the faces instead. */
.page-leaf.turning .leaf-face { box-shadow: 0 22px 34px rgba(30, 25, 15, .3); }
/* On landing, fade the lift shadow to nothing BEFORE the leaf is removed — else
   its removal pops the shadow off in one frame and reads as a flicker. */
.page-leaf.landed .leaf-face { box-shadow: none !important; }
/* Backface culling can leak a hairline of the (light) outgoing front face at
   the rest frame, showing a white corner. The back + real page already carry
   the correct content, so hide the front once the leaf has landed. */
.page-leaf.landed .leaf-front { visibility: hidden; }
.leaf-face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transition: box-shadow .16s ease-out;
}
.leaf-face > * { width: 100%; height: 100%; }
.leaf-back {
  /* No opaque backing: the incoming page clone fills the back, and the real
     page beneath the leaf is already the correct new page. A light paper
     gradient here peeked white at the corners as the leaf landed on the dark
     art page — transparent lets any sub-pixel edge reveal the matching page. */
  background: transparent;
  /* translateZ lifts the back face off the front face's plane — without it the
     two z-fight and the front's text shows sharply mirrored through the back. */
  transform: rotateY(180deg) translateZ(1px);
  box-shadow: inset -14px 0 26px rgba(43, 42, 35, .12);
  border-radius: 0;
}
.page-leaf.prev .leaf-back { box-shadow: inset 14px 0 26px rgba(43, 42, 35, .12); border-radius: 0; }
/* Holds the OLD content on the non-turning half until the leaf passes vertical. */
.page-stay-overlay { position: absolute; z-index: 8; overflow: hidden; pointer-events: none; }
.page-stay-overlay > * { width: 100%; height: 100%; }

/* ================================================================
   REDESIGN LAYER — imported from Claude Design "Kids book app redesign"
   Additive presentation classes; uses the same :root tokens.
   ================================================================ */

/* ---- illustrated book cover (hero fan, samples showcase, quality) ---- */
.bca { position: relative; aspect-ratio: 3 / 4; border-radius: 3px 13px 13px 3px; overflow: hidden; background: var(--cov, linear-gradient(160deg, #2a4a73, #12294a)); box-shadow: 10px 14px 30px rgba(13,47,67,.22), inset 0 0 0 1px rgba(255,255,255,.06); display: grid; grid-template-rows: 1fr auto; isolation: isolate; }
.bca::before { content: ""; position: absolute; z-index: 3; left: 0; top: 0; bottom: 0; width: 9px; background: linear-gradient(90deg, rgba(0,0,0,.28), rgba(255,255,255,.14) 60%, rgba(0,0,0,.12)); }
.bca::after { content: ""; position: absolute; inset: 0; z-index: 1; background: radial-gradient(120% 80% at 70% 12%, rgba(255,255,255,.16), transparent 55%), linear-gradient(180deg, transparent 45%, rgba(3,20,33,.42)); pointer-events: none; }
.bca .bca-art { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; object-fit: cover; }
.bca .bca-emoji { grid-row: 1; align-self: center; justify-self: center; z-index: 2; font-size: clamp(46px, 8vw, 92px); line-height: 1; filter: drop-shadow(0 10px 16px rgba(0,0,0,.35)); transform: rotate(-3deg); }
.bca .bca-plate { grid-row: 2; z-index: 2; padding: 12px 14px 15px; text-align: center; }
.bca .bca-kicker { color: var(--gold-light); font-size: 8px; font-weight: 900; letter-spacing: 1.6px; text-transform: uppercase; }
.bca .bca-title { margin: 6px 8px 0; color: #fff; font-family: var(--display); font-weight: 500; font-size: clamp(15px, 2.1vw, 22px); line-height: 1.02; letter-spacing: -.4px; text-shadow: 0 2px 10px rgba(0,0,0,.5); }
.bca .bca-author { margin-top: 6px; color: rgba(255,255,255,.72); font-family: var(--display); font-style: italic; font-size: 9px; }
.bca .bca-sparkle { position: absolute; z-index: 2; color: var(--gold-light); opacity: .9; filter: drop-shadow(0 1px 3px rgba(0,0,0,.4)); }
.bca .s1 { top: 14px; right: 14px; font-size: 15px; } .bca .s2 { bottom: 46%; left: 12px; font-size: 10px; opacity: .7; }
/* Real artwork and nothing over it: no lettered plate, no vignette. The library's own
   covers render this way — the plate stays the fallback for a book with no art yet. */
.bca-bare::after { content: none; }
.bca-bare .bca-plate, .bca-bare .bca-sparkle { display: none; }

/* cover themes */
.t-bedtime  { --cov: radial-gradient(120% 90% at 30% 22%, #34568a, #142a4c 68%); }
.t-adventure{ --cov: linear-gradient(160deg, #f0965f, #d1573f); }
.t-animals  { --cov: linear-gradient(160deg, #5c9b74, #2f6b4e); }
.t-space    { --cov: radial-gradient(120% 90% at 72% 18%, #3f2f74, #0f1230 70%); }
.t-ocean    { --cov: linear-gradient(160deg, #38a7b2, #0e5b6b); }
.t-learning { --cov: linear-gradient(160deg, #f2c451, #dd971c); }
.t-castle   { --cov: linear-gradient(160deg, #7c6bc4, #43307e); }
.t-dino     { --cov: linear-gradient(160deg, #8aa25a, #4b6b39); }
.t-snow     { --cov: linear-gradient(160deg, #b7dce6, #6aa6bd); }
.t-candy    { --cov: linear-gradient(160deg, #f3a1c0, #db5f8b); }

/* The old hero cover fan (.cover-fan/.fan-cover/.fc-*) went with the hero it
   styled — the "Hero Playful" covers are .hero-cover and carry their own art. */

/* ---- examples showcase (real sample books) ---- */
/* The Claude Design "Storybook Showcase" page: this week's book in a dashed panel, a
   counted "Some more examples" rule, a grid of cards, and a gold strip at the foot. It
   replaced the "Storybook Library" rail, so `.feature*`, `.shelf*`, `.lib-*`, `.card-*`
   and `.tag*` went with the markup they styled. The cover itself is still `.bca` — one
   cover component for the quality section, the panel and every card — with the design's
   own radius, spine and tilt layered on here.
   Everything is `sb-`prefixed and so are the keyframes: this sheet already carries
   hero-/faq-/ft-/sub-/hm-/cs-/kc- twinkle and bob variants, and a duplicate
   `@keyframes` name is last-wins with no warning. */
/* Flat paper. It used to grow extra bottom padding and pull it back with a negative
   margin, which was the arch debt for the navy band below — that band crests into this
   one now (`.fg-seam`, drawn in ITS colour above ITS own edge), so this section owes it
   nothing and keeps the shared padding. `overflow-x: clip` and not `hidden` — the
   straddling badge below sits above this section's own top edge, and `hidden` clips it
   away; the axis the rail actually needs clipped is still clipped (see the rail's own
   note). It is also what lets the navy crest and its twinkles paint over this paper. */
.examples-section { position: relative; overflow-x: clip; background: var(--band-examples);
  padding: var(--band-pad) 0; }
.sb-spark { position: absolute; color: var(--gold); animation: sb-twinkle 3.4s ease-in-out infinite; pointer-events: none; }
.sb-spark.s1 { top: 7%; left: 6%; font-size: 20px; }
.sb-spark.s2 { top: 10%; right: 6%; font-size: 15px; color: #eb9d8a; animation-duration: 4.2s; animation-delay: .6s; }
.sb-spark.s3 { bottom: 12%; left: 9%; font-size: 17px; color: #9fc7d4; animation-duration: 3.8s; animation-delay: 1.1s; }
/* The rhythm's THIRD seam device: this section's kicker is a badge that straddles the
   join, sitting 32px above its own band so the sand above and the paper below read as
   stitched rather than stacked. The whole heading is pulled up by the section's top
   padding plus that 32px — the badge is the heading's first child, so pulling the
   heading is what puts the badge over the seam and keeps the h2 the same distance
   under it at every width. It is `.sb-kicker` over the shared `.kicker`, the sidestep
   `.hero-eyebrow` and `.how-kicker` both make — `.kicker` dresses every other heading.
   The section's own bottom padding still carries the arch debt, so only the TOP of the
   padding is spent here. */
.examples-section .section-heading { margin-top: calc(-1 * var(--band-pad) - 32px); }
.sb-kicker.kicker { display: inline-block; margin-bottom: 22px; padding: 11px 22px; border: 2.5px solid var(--blue); border-radius: 999px; background: var(--gold-light); color: var(--blue); font-size: 12px; letter-spacing: 1.6px; font-weight: 800; transform: rotate(-2deg); box-shadow: 0 14px 26px -14px rgba(14,61,86,.8); }

@keyframes sb-twinkle { 0%, 100% { opacity: .28; transform: scale(.8) rotate(0deg); } 50% { opacity: 1; transform: scale(1.15) rotate(14deg); } }
/* The tilt is stated on the element as well as inside the keyframe: with animations
   suppressed the keyframes never run, and a tile whose only tilt lived in the keyframe
   would sit straight. Same rule the mobile rail plate and `hm-bob` already keep. */
@keyframes sb-bob { 0%, 100% { transform: translateY(0) rotate(var(--sb-r, 0deg)); } 50% { transform: translateY(-7px) rotate(var(--sb-r, 0deg)); } }

.sb-featured { position: relative; margin: 38px 0 0; padding: 30px 30px 28px; background: #fffdf7; border: 2px dashed #e2d3b4; border-radius: 30px; }
.sb-ribbon { position: absolute; top: -13px; left: 32px; padding: 6px 13px; border-radius: 999px; background: var(--gold); color: #3b2c08; font-size: 10.5px; font-weight: 800; letter-spacing: 1.6px; transform: rotate(-2deg); }
.sb-feature-grid { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 32px; align-items: center; }
.sb-feature-cover { --sb-r: -4deg; position: relative; cursor: pointer; transform: rotate(var(--sb-r)); animation: sb-bob 5.4s ease-in-out infinite; }
.sb-feature-cover .bca { border-radius: 10px 20px 20px 10px; box-shadow: 0 34px 50px -28px rgba(13,47,67,.9); }
.sb-feature-copy { min-width: 0; }
.sb-eyebrow { display: block; color: #a4906f; font-size: 10.5px; font-weight: 800; letter-spacing: 1.7px; text-transform: uppercase; }
.sb-feature-title { display: block; margin-top: 8px; font-family: var(--display); font-weight: 500; font-size: clamp(28px, 3.2vw, 36px); line-height: 1.08; text-wrap: balance; }
.sb-feature-blurb { max-width: 440px; margin: 10px 0 0; font-family: var(--display); font-style: italic; font-size: 16.5px; line-height: 1.55; color: #8a7a5f; text-wrap: pretty; }
.sb-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.sb-chip { display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; border: 1.5px solid #ece0c6; border-radius: 999px; background: #faf3e2; color: #6b5c45; font-size: 13px; }
.sb-feature-actions { display: flex; flex-wrap: wrap; gap: 11px; margin-top: 22px; }

.sb-cta { display: inline-flex; align-items: center; gap: 9px; padding: 15px 24px; border: 0; border-radius: 16px; background: var(--green); color: #fff; font-family: var(--sans); font-size: 15.5px; font-weight: 800; cursor: pointer; box-shadow: 0 20px 30px -18px rgba(47,82,68,.95); transition: transform .16s, background .16s; }
.sb-cta:hover { background: var(--green-dark); transform: translateY(-2px) rotate(-.8deg); }
.sb-ghost { display: inline-flex; align-items: center; gap: 9px; padding: 15px 22px; border: 2px solid #dfd0ae; border-radius: 16px; background: #fffdf7; color: var(--ink); font-family: var(--sans); font-size: 15.5px; font-weight: 800; cursor: pointer; transition: transform .16s, border-color .16s; }
.sb-ghost:hover { border-color: var(--ink); transform: translateY(-2px); }

.sb-more-head { display: flex; align-items: baseline; gap: 14px; margin-top: 46px; }
.sb-more-head strong { font-family: var(--display); font-weight: 500; font-size: clamp(24px, 2.6vw, 30px); }
.sb-rule { flex: 1; height: 2px; background: repeating-linear-gradient(90deg, #e0cfa8 0 7px, transparent 7px 15px); }
.sb-count { flex: none; font-family: var(--display); font-style: italic; font-size: 15px; color: #8a7a5f; }

/* The grid is a RAIL: four cards to a view, scrolled a page at a time by the two arrows
   in the head row above it. A card's flex-basis is FIXED — a quarter of the track less
   the gaps — which is what the old `auto-fill, never auto-fit` rule bought and for the
   same reason: with one card in the rail (two static samples, one of them featured
   above, and nothing flagged) a `1fr` track would stretch that card across the page, and
   a fixed basis cannot. The arrows live in `.sb-more-head` rather than over the track's
   own edges because `.examples-section` clips its X axis (`overflow-x: clip`, so the
   straddling badge can still hang above it) and anything hung outside the shell is
   clipped at every width that has no gutter.
   Snapping is `proximity`, never `mandatory`: the ≤560 breakpoint shows 1.25 cards so the
   next one peeks, and a mandatory snap on a fractional last item can leave the true end
   of the scroll unreachable. */
.sb-rail-nav { flex: none; display: flex; gap: 8px; }
.sb-rail-nav[hidden] { display: none; }
.sb-rail-btn { width: 38px; height: 38px; display: grid; place-items: center; border: 2px solid #e2d3b4; border-radius: 999px; background: #fffdf7; color: var(--ink); font-size: 19px; line-height: 1; cursor: pointer; transition: transform .16s, border-color .16s, opacity .16s; }
.sb-rail-btn:hover:not(:disabled) { border-color: var(--coral); transform: translateY(-2px); }
.sb-rail-btn:disabled { opacity: .35; cursor: default; }
/* A hovered card's lift and drop shadow are INSIDE the scroll box — `overflow-x: auto`
   computes `overflow-y` to `auto` as well, so the track's own padding is what keeps them
   from being clipped; an `overflow: visible` cannot exist on this element. */
.sb-track { --sb-per: 4; --sb-gap: 18px; display: flex; gap: var(--sb-gap); margin-top: 22px; padding: 10px 2px 32px; overflow-x: auto; scroll-snap-type: x proximity; scroll-behavior: smooth; scrollbar-width: none; }
.sb-track::-webkit-scrollbar { display: none; }
.sb-track > .sb-card { flex: 0 0 calc((100% - (var(--sb-per) - 1) * var(--sb-gap)) / var(--sb-per)); scroll-snap-align: start; }
@media (max-width: 1040px) { .sb-track { --sb-per: 3; } }
@media (max-width: 820px) { .sb-track { --sb-per: 2; } }
@media (max-width: 560px) { .sb-track { --sb-per: 1.25; --sb-gap: 14px; } }
.sb-card { display: flex; flex-direction: column; padding: 13px; border: 2px solid #ece0c6; border-radius: 20px; background: #fffdf7; cursor: pointer; transition: transform .2s, border-color .2s, box-shadow .2s; }
.sb-card:hover, .sb-card:focus-visible { transform: translateY(-6px) rotate(-.7deg); border-color: #eb9d8a; box-shadow: 0 30px 46px -34px rgba(60,42,14,.95); outline: none; }
/* The design's card cover is 4/3; a book cover is painted 2:3, and a landscape crop cuts
   a third off every one of them — the ratio is the book object's, as everywhere else. */
.sb-card-cover { position: relative; overflow: hidden; border-radius: 13px; background: #e8dcc2; }
.sb-card-cover .bca { border-radius: 0; box-shadow: none; }
/* A book with no artwork yet keeps the lettered plate; the corner labels sit over it. */
.sb-card-cover.is-plate .bca-plate, .sb-feature-cover.is-plate .bca-plate { padding-bottom: 34px; }
.sb-badge { position: absolute; z-index: 3; top: 9px; left: 9px; padding: 6px 10px; border-radius: 999px; background: var(--coral); color: #fff; font-size: 10px; font-weight: 800; letter-spacing: 1.3px; text-transform: uppercase; transform: rotate(-2deg); box-shadow: 0 8px 14px -8px rgba(60,42,14,.9); }
.sb-lang { position: absolute; z-index: 3; right: 9px; bottom: 9px; display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 999px; background: rgba(13,47,67,.82); color: #fdf6e6; font-size: 11px; font-weight: 700; backdrop-filter: blur(3px); }
/* Theme tints, from the design's own tint table — one per story feeling the creator
   offers. Solid, because the badge sits over artwork. */
.sb-tint-fairy { background: #8e6bb0; } .sb-tint-bible { background: #456f5c; }
.sb-tint-royal { background: #a07d3c; } .sb-tint-farm { background: #e9a92b; }
.sb-tint-brave { background: #eb755b; } .sb-tint-bedtime { background: #5f7fa8; }
.sb-tint-silly { background: #c2578f; } .sb-tint-ocean { background: #4c8ba0; }

.sb-card-title { display: block; margin-top: 11px; font-family: var(--display); font-weight: 500; font-size: 19px; line-height: 1.15; text-wrap: balance; }
.sb-card-blurb { display: block; margin-top: 5px; font-family: var(--display); font-style: italic; font-size: 13px; line-height: 1.5; color: #8a7a5f; text-wrap: pretty; }
.sb-card-foot { display: flex; align-items: center; gap: 6px; margin-top: auto; padding-top: 13px; }
.sb-foot-chip { flex: none; white-space: nowrap; display: inline-flex; align-items: center; gap: 5px; padding: 5px 9px; border-radius: 999px; background: #faf3e2; color: #6b5c45; font-size: 11.5px; font-weight: 700; }
.sb-foot-chip.sb-foot-family { background: #e6efe9; color: #456f5c; }
.sb-foot-gap { flex: 1; }
.sb-read { flex: none; white-space: nowrap; color: var(--coral-dark); font-size: 12.5px; font-weight: 800; }

/* The shop window's offer — the dashed end-of-rail slot's replacement. Its CTA carries a
   bare `data-open-creator` and rides the delegate at the foot of app.js, so a logged-out
   visitor gets the register modal like every other "Create a book". */
.sb-final { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 20px; max-width: 840px; margin: 36px auto 0; padding: 20px 26px; border: 2.5px solid var(--gold); border-radius: 24px; background: #fffdf7; box-shadow: 0 30px 52px -46px rgba(191,146,20,.95); }
.sb-final-ico { --sb-r: -5deg; flex: none; width: 48px; height: 48px; display: grid; place-items: center; border-radius: 16px; background: #faf3e2; font-size: 24px; line-height: 1; transform: rotate(var(--sb-r)); animation: sb-bob 3.8s ease-in-out infinite; }
.sb-final-copy { flex: 1; min-width: 240px; }
.sb-final-copy strong { display: block; font-family: var(--display); font-weight: 500; font-size: 21px; }
.sb-final-copy span { display: block; margin-top: 3px; font-family: var(--display); font-style: italic; font-size: 14.5px; color: #8a7a5f; }
.sb-final .sb-cta { flex: none; }

/* ---- redesign responsive ---- */
@media (max-width: 900px) {
  .sb-featured { padding: 26px 22px 24px; border-radius: 26px; }
  .sb-feature-grid { grid-template-columns: 1fr; gap: 26px; justify-items: center; text-align: center; }
  /* Stacked, the cover is capped rather than filled: full width it is over 400px tall and
     pushes the copy — and the two buttons — off the first screen. */
  .sb-feature-cover { width: 220px; }
  .sb-feature-blurb { margin-inline: auto; }
  .sb-chips, .sb-feature-actions { justify-content: center; }
}
@media (max-width: 640px) {
  .sb-more-head { margin-top: 34px; flex-wrap: wrap; }
  /* The rule needs a row of its own once the count wraps, or it collapses to nothing. */
  .sb-rule { flex-basis: 100%; }
  .sb-final { flex-direction: column; text-align: center; }
  .sb-final-copy { min-width: 0; }
}

/* ================================================================
   KID STUDIO — full-screen, picture-first creator overlay
   (from Claude Design redesign; wired to the real creator flow)
   ================================================================ */
/* The site header is sticky at z-index 100 — above the studio overlay — so it
   would float over the studio's top row (Close / progress stars / sound).
   Hide it outright while the studio is open; the studio has its own top bar. */
body.kc-open .site-header { display: none; }
.kc { position: fixed; inset: 0; z-index: 70; display: none; flex-direction: column; background: radial-gradient(130% 100% at 18% 6%, #fff5da 0%, #fbeccd 48%, #f6e2bd 100%); }
.kc.active { display: flex; }
.kc-confetti { position: fixed; inset: 0; z-index: 90; pointer-events: none; }

.kc-top { display: flex; align-items: center; gap: 18px; padding: 16px clamp(16px, 4vw, 44px); }
.kc-exit { display: inline-flex; align-items: center; gap: 9px; min-height: 46px; padding: 0 18px; border: 0; border-radius: 24px; background: #fff; color: var(--ink); font-size: 14px; font-weight: 900; cursor: pointer; box-shadow: 0 6px 16px rgba(13,47,67,.1); }
.kc-exit:hover { transform: translateY(-1px); }
/* The steps are the square stamps alone — no dashed card around them and no counted
   foot under them. The row is the top bar's middle child rather than a row of its own:
   the studio is a fixed-height column (.kc-top / #kc-main / .kc-nav) and a second row
   is height taken off the step body, which is where "Make my book!" has to stay
   reachable. */
.kc-progress { flex: 1; min-width: 0; display: flex; align-items: flex-start; justify-content: center; gap: clamp(6px, 1.8vw, 18px); }
/* The step name and counter over the pips: the PHONE header only ("Book Steps Mobile"),
   where the stamps are 6px bars and no longer say which step this is. It sits inside
   .kc-progress so the middle column is one element at both widths — the row's click and
   keydown handlers read `closest(".kc-star")`, so it is inert. */
.kc-mtitle { display: none; }
.kc-star { position: relative; display: flex; flex-direction: column; align-items: center; }
/* The stars are built once, so a step that is skipped (adventure, once a Bible account
   or a fairy tale fixed the plot) hides its own — `display: flex` above would otherwise
   beat the hidden attribute and leave a star that can never be reached. */
.kc-star[hidden] { display: none; }
/* A stamp, not the design's fixed per-step glyph: kcStepBadge fills it with what was
   actually PICKED (the language flag, the child's photo, a face per joiner), which is
   the one thing a paper stamp card cannot do and the reason the tile stays a container
   rather than a single character. */
.kc-star-icon { position: relative; width: clamp(48px, 6.4vw, 76px); height: clamp(48px, 6.4vw, 76px); display: grid; place-items: center; border-radius: clamp(12px, 1.4vw, 16px); background: #fffdf7; border: 2px solid #ecdfc4; font-size: clamp(21px, 2.8vw, 30px); transition: transform .2s, border-color .2s, box-shadow .2s; }
.kc-star:nth-child(1) .kc-star-icon { transform: rotate(-4deg); }
.kc-star:nth-child(2) .kc-star-icon { transform: rotate(3deg); }
.kc-star:nth-child(3) .kc-star-icon { transform: rotate(-2deg); }
.kc-star:nth-child(4) .kc-star-icon { transform: rotate(4deg); }
.kc-star:nth-child(5) .kc-star-icon { transform: rotate(-3deg); }
/* Quiet the CONTENTS of an unstamped tile, never the tile: guarded on both state
   classes so the wash can't reach a stamp that was earned or is being filled in. */
.kc-star:not(.done):not(.current) .kc-star-icon { opacity: .5; filter: grayscale(.6); }
.kc-star-label { margin-top: 9px; max-width: 84px; font-size: clamp(11px, 1.2vw, 13.5px); font-weight: 700; line-height: 1.15; text-align: center; color: #b3a486; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kc-star.done .kc-star-icon { background: #fff; border: 2.5px solid var(--green); box-shadow: 0 10px 18px -12px rgba(69,111,92,.8); }
/* The tick is a pseudo-element because kcRefreshBadges rewrites the star's innerHTML
   whenever the icon or label is missing — a real element would be wiped by it. */
.kc-star.done .kc-star-icon::after { content: "✓"; position: absolute; right: 4px; bottom: 4px; width: 19px; height: 19px; display: grid; place-items: center; border-radius: 99px; background: var(--green); color: #fff; font-size: 11px; font-weight: 900; }
.kc-star.done .kc-star-label { color: var(--green); }
.kc-star.current .kc-star-icon { background: #fff; border: 3px dashed var(--coral); box-shadow: 0 14px 24px -12px rgba(215,91,69,.9); transform: rotate(0) scale(1.06); animation: kcStampPulse 2.2s ease-in-out infinite; }
.kc-star.current .kc-star-label { font-family: var(--display); font-style: italic; font-weight: 700; font-size: clamp(12px, 1.35vw, 15.5px); color: var(--coral); }
.kc-star.reachable { cursor: pointer; }
.kc-star.reachable:hover .kc-star-icon { border-color: var(--coral); transform: translateY(-2px) rotate(0); }
.kc-star:focus-visible { outline: none; }
.kc-star:focus-visible .kc-star-icon { border-color: var(--coral); box-shadow: 0 0 0 3px rgba(235,117,91,.32); }
@keyframes kcStampPulse { 0%, 100% { transform: rotate(0) scale(1.06); } 50% { transform: rotate(0) scale(1.13); } }

/* Who-joins carries a face per joiner inside the same stamp: one fills it, two
   sit side by side, three or four make a 2x2 (the third centred under the pair),
   and a fifth turns the last cell into "+N". The faces stay round on the square
   tile — a photo cropped to the tile's own corners reads as the stamp itself. */
/* A lone photo sits inside the stamp, not over its border. */
.kc-star-icon:not(.multi) .kc-star-face { width: calc(100% - 14px); height: calc(100% - 14px); }
.kc-star-icon.multi { padding: 7px; gap: 2px; grid-template-columns: repeat(2, 1fr); place-content: center; font-size: clamp(10px, 1.3vw, 14px); }
.kc-star-icon[data-count="2"] { padding: 6px; gap: 1px; font-size: clamp(12px, 1.6vw, 17px); }
.kc-star-face { display: grid; place-items: center; width: 100%; aspect-ratio: 1; border-radius: 50%; overflow: hidden; line-height: 1; }
.kc-star-photo { background-size: cover; background-position: center; }
.kc-star-more { background: #fff6dc; box-shadow: inset 0 0 0 1px #eee3c9; font-size: .8em; font-weight: 800; color: var(--ink-soft); }
.kc-star-icon[data-count="3"] .kc-star-face:nth-child(3) { grid-column: 1 / -1; justify-self: center; width: 50%; }

/* The nine drawn family faces (`KC_DRAWN_FACES` in app.js) — a Fitzpatrick modifier only
   ever RECOLOURS the same white face, so the studio draws its people instead of toning
   them. One rule for all twenty-odd surfaces that print a person: they are inline SVG at
   `width: 1em`, so every one of them sizes off the `font-size` that already sized the
   emoji and none of them needs a rule of its own. */
.kc-face { width: 1em; height: 1em; display: inline-block; vertical-align: -.15em; flex: none; }
/* The top row's right-hand end: the HEADER's own book meter and account chip, moved here
   by `moveAccountToStudio` (the site header is hidden while the studio is open) in place of
   the navy "📚 My books ↗" button that stood here — "my books" is a row of the chip's menu.
   Three things are load-bearing. (1) The slot is `flex: none` and the progress row keeps
   `flex: 1`, or a long member name eats the stamps' middle column. (2) The chip is restyled
   to the studio's own furniture — white, no hairline border, the same 46px and the same
   soft shadow `.kc-exit` carries on the left of the row — rather than arriving with the
   header's paper-and-hairline look on a gold studio. (3) The menu is `position: absolute`
   against `.acc`, so it drops over `#kc-main` on its own `z-index: 20`; nothing between
   here and `.kc` may take `overflow: hidden`. */
.kc-account { flex: none; display: flex; align-items: center; gap: 10px; }
.kc-top .acc-chip { min-height: 46px; border-color: transparent; background: #fff; box-shadow: 0 6px 16px rgba(13,47,67,.1); }
.kc-top .acc-chip:hover { border-color: #e6bd3f; }

.kc-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 1fr; padding: 4px clamp(16px, 4vw, 44px) clamp(14px, 2vw, 26px); }
.kc-stage { display: flex; flex-direction: column; min-height: 0; }

.kc-ask { display: flex; align-items: center; gap: 16px; margin-bottom: clamp(12px, 2vw, 22px); }
/* The stamp holds the SITE MARK now, not a mascot emoji. Three things follow.
   (1) The blob turns pale (it was a gold gradient) — `.brand-mark i:nth-child(3)`
   is `var(--gold)` and a gold spine on a gold disc is an invisible spine.
   (2) `.brand-mark` is a fixed 44px drawing, so it is SCALED to the stamp rather
   than sized by it; the stamp itself stays the clamped 64–92px the layout expects.
   (3) The whole stamp no longer rocks (`kcBob` is gone) — only the star bobs, and
   it is `.brand-mark b`, so a `transform` on the mark itself stays free for the
   scale above. */
.kc-mascot { flex: 0 0 auto; width: clamp(64px, 8vw, 92px); height: clamp(64px, 8vw, 92px); display: grid; place-items: center; border-radius: 46% 54% 52% 48%; background: linear-gradient(160deg, #fffdf6, #ffeec2); box-shadow: 0 12px 24px rgba(233,169,43,.26); }
.kc-mascot .brand-mark { transform: scale(1.4); transform-origin: center; }
.kc-mascot .brand-mark b { animation: kcMarkStar 1.9s ease-in-out infinite; }
@keyframes kcMarkStar { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-6px) scale(1.22); } }
.kc-bubble { position: relative; flex: 1; background: #fff; border-radius: 20px 22px 22px 6px; padding: 16px 20px; box-shadow: 0 10px 26px rgba(13,47,67,.12); }
.kc-bubble::before { content: ""; position: absolute; left: -12px; bottom: 12px; border: 8px solid transparent; border-right-color: #fff; border-bottom: 0; }
.kc-bubble h2 { margin: 0; font-family: var(--display); font-weight: 500; font-size: clamp(22px, 3.2vw, 36px); line-height: 1.05; letter-spacing: -.8px; color: var(--ink); }
.kc-bubble p { margin: 6px 0 0; color: var(--ink-soft); font-size: clamp(12px, 1.4vw, 15px); }

.kc-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 4px 8px; }
.kc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(clamp(120px, 14vw, 160px), 1fr)); gap: clamp(12px, 1.6vw, 18px); }
.kc-choice { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: clamp(14px, 2vw, 22px) 10px; background: #fff; border: 3px solid transparent; border-radius: 20px 24px 20px 24px; box-shadow: 0 8px 20px rgba(13,47,67,.08); cursor: pointer; transition: transform .16s, box-shadow .16s, border-color .16s, background .16s; font: inherit; }
.kc-choice:hover { transform: translateY(-5px) rotate(-1deg); box-shadow: 0 16px 30px rgba(13,47,67,.16); }
.kc-choice:active { transform: translateY(-1px) scale(.98); }
.kc-choice .kc-emoji { font-size: clamp(40px, 5.4vw, 62px); line-height: 1; filter: drop-shadow(0 6px 8px rgba(0,0,0,.18)); transition: transform .2s; }
.kc-choice:hover .kc-emoji { transform: scale(1.08) rotate(4deg); }
.kc-choice .kc-clabel { font-size: clamp(13px, 1.5vw, 16px); font-weight: 800; color: var(--ink); text-align: center; }
.kc-choice.selected { border-color: var(--gold); background: #fffaef; box-shadow: 0 14px 30px rgba(233,169,43,.28); }
.kc-choice.selected .kc-emoji { animation: kcPop .4s ease; }
@keyframes kcPop { 0% { transform: scale(1); } 45% { transform: scale(1.3) rotate(-8deg); } 100% { transform: scale(1.08); } }
.kc-check { position: absolute; top: -10px; right: -10px; width: 34px; height: 34px; display: none; place-items: center; border-radius: 50%; background: var(--coral); color: #fff; font-size: 17px; box-shadow: 0 6px 14px rgba(235,117,91,.4); }
.kc-count-badge { position: absolute; top: -10px; left: -10px; min-width: 30px; height: 30px; padding: 0 7px; display: grid; place-items: center; border-radius: 999px; background: var(--gold); color: #fff; font-size: 15px; font-weight: 900; box-shadow: 0 6px 14px rgba(233,169,43,.4); }
.kc-choice.selected .kc-check { display: grid; animation: kcPop .4s ease; }

.kc-preview { display: flex; flex-direction: column; align-items: center; align-self: center; gap: 14px; padding: clamp(16px, 2vw, 26px); background: rgba(255,255,255,.55); border: 1px solid rgba(255,255,255,.7); border-radius: 22px 28px 22px 28px; box-shadow: 0 16px 40px rgba(13,47,67,.1); }
.kc-preview-label { display: inline-flex; align-items: center; gap: 7px; color: var(--coral-dark); text-transform: uppercase; letter-spacing: 1.6px; font-size: 11px; font-weight: 900; }
.kc-book { width: min(230px, 60vw); transition: transform .3s; }
.kc-book .bca { transform: rotate(-2deg); }
.kc-book:hover .bca { transform: rotate(0) translateY(-3px); }
.kc-preview-hint { color: var(--ink-soft); font-size: 12px; text-align: center; line-height: 1.5; }
.kc-collected { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 800; color: #9a7a2a; }
.kc-collected span { font-size: 15px; }

.kc-nav { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: clamp(12px, 1.6vw, 18px) clamp(16px, 4vw, 44px) clamp(16px, 2vw, 24px); }
.kc-back { min-height: 54px; padding: 0 22px; border: 0; border-radius: 26px; background: transparent; color: #6f7e84; font-size: 15px; font-weight: 900; cursor: pointer; }
.kc-back:hover { color: var(--coral); }
.kc-back:disabled { opacity: 0; pointer-events: none; }
.kc-next { min-height: 60px; display: inline-flex; align-items: center; gap: 14px; padding: 0 34px; border: 0; border-radius: 14px 20px 14px 20px; color: #fff; background: var(--green); font-size: 19px; font-weight: 900; cursor: pointer; box-shadow: 0 14px 30px rgba(69,111,92,.34); transition: transform .16s, background .16s, opacity .16s; }
.kc-next .arrow { font-size: 24px; }
.kc-next:hover { transform: translateY(-2px); background: var(--green-dark); }
/* The disabled fill is a WASHED version of the live one and has to move with it — it was
   `#d9b8ad`, which is washed coral, and left the studio's own Next reading pink in the one
   state a family sees before they have answered anything. */
.kc-next:disabled { opacity: .4; cursor: default; transform: none; box-shadow: none; background: #a9bdb1; }

/* ── the studio's last screen: "Book Ready.dc.html" ─────────────────────────────────────
   The book on the left, what it is and what unlocking it buys on the right — the same
   give-vs-show split the checkout page makes, one step earlier. Four things are load-bearing.
   (1) The grid is centred with `margin: auto` on the CHILD, never `align-items: center` on
   the scroll box: this panel is the studio's fixed-height body (`#kc-nav` is hidden here, so
   it owns the height), and a centred flex item taller than its container is clipped at the
   TOP with no way to scroll back to it. (2) The classes and keyframes are `cel-`prefixed —
   the design's own are the bare `twinkle`/`bob`/`glow`, and this sheet already carries
   hero-/hm-/fg-/faq-/ft-/sub-/sb- variants of two of those, where a duplicate `@keyframes`
   name is last-wins with no warning. (3) The cover's tilt is stated on the element as well as
   inside `celWobble`, and the glow's centring transform is repeated in every frame of
   `celGlow`: the reduced-motion block below kills both animations, and a rule whose only
   transform lived in the keyframe would sit straight (or jump a quarter-screen). (4) The
   ribbon is a SIBLING of `.bca`, which is `overflow: hidden` — inside it, it would be cut
   off at the spine. */
.kc-celebrate { position: relative; flex: 1; min-height: 0; display: flex; padding: clamp(18px, 3vw, 34px) clamp(16px, 4vw, 40px) clamp(26px, 4vw, 54px); overflow-y: auto; }
/* The design's own tracks: the cover column is CAPPED and the copy takes the rest, where this
   was two equal `1fr`s. Equal columns are what wrapped the action row onto two lines — the two
   buttons and their price come to ~600px and an even split of 1020 gave the copy 470. */
.cel-grid { position: relative; z-index: 1; margin: auto; width: 100%; max-width: 1180px; display: grid; grid-template-columns: minmax(0, 1fr); gap: clamp(24px, 4vw, 64px); align-items: center; justify-items: center; }
@media (min-width: 900px) { .cel-grid { grid-template-columns: minmax(260px, 400px) minmax(0, 1fr); justify-items: stretch; } }

.cel-stage { position: relative; display: flex; justify-content: center; padding: 18px 0; }
.cel-glow { position: absolute; top: 50%; left: 50%; width: min(420px, 100%); aspect-ratio: 1; transform: translate(-50%, -50%); border-radius: 50%; background: radial-gradient(circle, rgba(247,217,125,.75), rgba(247,217,125,0) 68%); animation: celGlow 6s ease-in-out infinite; pointer-events: none; }
.kc-celebrate .cel-book { position: relative; width: min(290px, 66vw); animation: celRise .7s cubic-bezier(.2,.9,.3,1.2) both; }
/* The design's ground shadow is a blurred blob behind the cover; here it is a second stop on
   the cover's own box-shadow. The cover WOBBLES, so a separate blob would have to be tilted
   with it or sit still while the book moved off it — a shadow on the box does neither. */
.kc-celebrate .cel-book .bca { transform: rotate(-2deg); border-radius: 6px 17px 17px 6px; box-shadow: 0 34px 56px -26px rgba(84,58,12,.85), 0 10px 26px -14px rgba(84,58,12,.45); animation: celWobble 3.2s ease-in-out infinite; }
.cel-ribbon { position: absolute; z-index: 2; top: 26px; left: -14px; padding: 7px 15px; border-radius: 8px 14px 9px 13px; background: var(--coral); color: #fffdf7; font-size: 11.5px; font-weight: 900; letter-spacing: 1.5px; text-transform: uppercase; transform: rotate(-6deg); box-shadow: 0 10px 20px -12px rgba(13,47,67,.7); }
@keyframes celRise { from { opacity: 0; transform: translateY(40px) scale(.8); } to { opacity: 1; transform: none; } }
@keyframes celWobble { 0%,100% { transform: rotate(-2deg); } 50% { transform: rotate(2deg); } }
@keyframes celGlow { 0%,100% { opacity: .55; transform: translate(-50%, -50%) scale(.96); } 50% { opacity: .9; transform: translate(-50%, -50%) scale(1.04); } }

.cel-tw { position: absolute; z-index: 0; pointer-events: none; animation: celTwinkle 5s ease-in-out infinite; }
.cel-tw.t1 { top: 12%; left: 6%; font-size: 20px; color: rgba(233,169,43,.55); }
.cel-tw.t2 { top: 28%; right: 8%; font-size: 15px; color: rgba(235,117,91,.5); animation-duration: 4.2s; animation-delay: .7s; }
.cel-tw.t3 { bottom: 14%; left: 11%; font-size: 13px; color: rgba(125,90,14,.35); animation-duration: 6s; animation-delay: 1.4s; }
.cel-tw.t4 { bottom: 24%; right: 13%; font-size: 18px; color: rgba(233,169,43,.4); animation-duration: 5.6s; animation-delay: 2s; }
@keyframes celTwinkle { 0%,100% { opacity: .25; transform: scale(.8) rotate(0deg); } 50% { opacity: 1; transform: scale(1.15) rotate(14deg); } }

.cel-copy { min-width: 0; text-align: left; }
/* The design's chip is FILLED gold and tilted, where it was a pale outline pill — it is the
   first thing on the copy side and it now reads as a stamp on the page rather than a caption.
   The cut-corner radius is the studio's own (`10px 14px 11px 13px` and its cousins). */
.cel-eyebrow {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 14px;
  border: 0; border-radius: 9px 15px 10px 14px; background: var(--gold); color: #3a2a06;
  font-size: 10.5px; font-weight: 900; letter-spacing: 1.8px;
  transform: rotate(-2.4deg); box-shadow: 0 8px 16px -12px rgba(13,47,67,.8);
}
.kc-celebrate h1 { position: relative; margin: 15px 0 0; font-family: var(--display); font-weight: 400; font-size: clamp(31px, 4.4vw, 47px); line-height: 1.08; letter-spacing: -1px; color: var(--ink); text-wrap: balance; }
/* The design sets the headline `white-space: nowrap`; this one deliberately WRAPS. The panel
   is `overflow-y: auto`, which computes `overflow-x` to `auto` as well — the trap `.kc-stage`
   documents — so a 47px unbreakable line in Polish or Romanian would not clip, it would pan the
   whole studio sideways. The highlight is on the last chunk, which is where the wrap leaves it. */
.cel-hl { box-shadow: inset 0 -.22em 0 rgba(235,117,91,.45); }
.cel-h1-star { position: absolute; top: -.24em; left: -.36em; font-size: .3em; color: var(--gold); transform: rotate(14deg); }
.cel-h1-party { display: inline-block; font-size: .72em; transform: rotate(-6deg); }
.kc-celebrate .cel-sub { margin: 14px 0 0; max-width: 26em; color: var(--ink-soft); font-size: clamp(14px, 1.8vw, 16.5px); line-height: 1.55; text-wrap: pretty; }
.kc-celebrate .cel-sub b { color: var(--ink); }
/* The one line on this screen a grown-up has to read: the book is finished, and payment is
   what opens the rest of it. It was a gold pill; the design's revision makes it ordinary sub-
   copy, because the buy button under it now carries the price and IS the loud thing — two
   competing gold objects one above the other was the reason the pill stopped working. The
   class name stays `.cel-locked`: the sentence still names the lock and the payment, which is
   the invariant the root `CLAUDE.md` records under that name. */
.kc-celebrate .cel-locked {
  display: block; margin: 14px 0 0; max-width: 46ch; padding: 0; border: 0; background: none;
  color: var(--ink-soft); font-size: clamp(14px, 1.8vw, 16px); font-weight: 400; line-height: 1.6;
}
/* `.btn-lg` came in with the old examples showcase and outlived it — the
   studio's handoff card and its celebration screen are the only users now, so the base
   rules live here with them rather than in a section that no longer renders one.
   (`.btn-ghost-l`, the pale outline variant beside them, went with that showcase.) */
.btn-lg { min-height: 54px; display: inline-flex; align-items: center; gap: 12px; padding: 0 26px; border: 0; border-radius: 10px 14px 11px 13px; font-size: 15px; font-weight: 900; cursor: pointer; transition: transform .18s, box-shadow .18s, background .18s; }
.btn-lg:hover { transform: translateY(-2px); }
/* Green, not gold (2026-08-17's "every filled call to action is green", applied to the
   handoff's own Create an account on 2026-08-18): the tint under it moves with the fill,
   or a gold glow under a green button reads as a mis-render rather than as a shadow. */
.btn-green { color: #fff; background: var(--green); box-shadow: 0 12px 24px rgba(47,82,68,.34); }
.btn-green:hover { background: var(--green-dark); }
/* `.cel-grownup` is the handoff screen's "Log in" and NOTHING else now — the celebration's
   own two buttons are `.cel-buy` and `.cel-preview` below. It kept a set of `.cel-actions`
   overrides (a pill radius, a paper fill) until 2026-08-12; those went with the buttons they
   restyled, and the base rule is left exactly as `.ho-account` needs it. */
.cel-grownup { display: inline-flex; align-items: center; gap: 8px; min-height: 58px; padding: 0 26px; border: 1.5px solid #d9cfb8; border-radius: 12px 16px 12px 16px; background: #fff; color: var(--ink); font-size: 15px; font-weight: 900; cursor: pointer; }
.cel-grownup:hover { border-color: var(--coral); }

/* The buy step and the free read, in the design's own order and weighting: the navy button
   leads and carries the price behind a hairline divider, the preview is paper under a dashed
   gold border. Both are tilted a degree the opposite way, which is what makes them read as two
   things stuck on the page rather than a toolbar. */
.cel-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 22px; margin-top: 26px; }
.cel-buy, .cel-preview {
  display: inline-flex; align-items: center; gap: 12px; min-height: 58px; padding: 0 26px;
  font-family: inherit; font-size: 16px; font-weight: 800; cursor: pointer;
  transition: transform .18s, box-shadow .18s, background .18s;
}
.cel-buy {
  border: 0; border-radius: 12px 20px 13px 19px; background: var(--blue); color: #fffdf7;
  transform: rotate(-1.2deg); box-shadow: 0 16px 30px -18px rgba(13,47,67,.9);
}
.cel-buy:hover { background: #0a3247; transform: rotate(-1.2deg) translateY(-2px); }
.cel-preview {
  border: 2px dashed #d9b968; border-radius: 19px 12px 20px 13px; background: #fffdf7;
  color: var(--blue); font-size: 15.5px; transform: rotate(1.4deg);
  box-shadow: 0 12px 24px -20px rgba(13,47,67,.8);
}
.cel-preview:hover { background: #fff8e8; transform: rotate(1.4deg) translateY(-2px); }
.cel-ico { flex: none; }
/* The price rides INSIDE the button, in the display face, behind a hairline. It is written by
   `kcCelebrate` and repainted by `renderPrices` — see `[data-cel-price]` there. */
.cel-price { padding-left: 12px; margin-left: 2px; border-left: 1px solid rgba(255,253,247,.28); font-family: var(--display); font-size: 17px; }

.cel-unlock { width: 100%; max-width: 480px; margin-top: 20px; padding-top: 22px; border-top: 1.5px dashed #dcc99f; }
.cel-unlock-label { display: block; margin-bottom: 14px; font-size: 11.5px; font-weight: 900; letter-spacing: 1.8px; text-transform: uppercase; color: #9a7a3c; }
/* Two across, each column only as wide as its own line — `minmax(0, auto)` with the track
   pack pushed to the start, so a long translation grows its own cell instead of two half-empty
   columns opening a gutter down the middle. The narration line is gated, so a one-perk list is
   normal and reads as one entry rather than an orphan in a two-column grid. */
.cel-perks { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, auto)); justify-content: start; gap: 14px 30px; }
.cel-perks li { position: relative; display: flex; align-items: center; gap: 10px; }
.cel-perk-text { min-width: 0; color: var(--ink-soft); font-size: 14.5px; font-weight: 700; line-height: 1.3; }
.cel-tick { flex: none; width: 34px; height: 34px; display: grid; place-items: center; border-radius: 50%; background: #dcecd6; border: 1.5px solid #bcd7bf; color: var(--green); }
.cel-tick .cel-ico { width: 15px; height: 15px; }
.cel-perk-star { position: absolute; top: -6px; right: 6px; font-size: 13px; color: var(--gold); transform: rotate(12deg); }
/* The phone's short labels. Above 640 each button is its icon, `.cel-btn-word` and (on the buy
   button) the price — the three flex items the design draws. */
.cel-btn-short { display: none; }

/* ── the celebration on a phone: "Book Ready Mobile" ───────────────────────────────────
   The same markup `kcCelebrate` writes, restyled — no mobile branch in app.js, the rule the
   whole phone studio keeps. The layout is: the cover under the studio's own `.kc-top`, the
   chip and the headline under it, the lock note, a wavy rule, the perks as paper cards, and a
   footer of two buttons that is always on screen. Eight things are load-bearing.
   (1) THE FOOTER IS STICKY, NOT FIXED. `.kc-celebrate` is the scroller, so `bottom: 0` pins
   `.cel-actions` to its bottom edge while the body scrolls under it; a `position: fixed` would
   be measured against the viewport and would sit over the `env(safe-area-inset-bottom)` of a
   studio that is itself a fixed panel. It is the LAST child of `.cel-copy` here — `order` is
   what moves it past `.cel-unlock` — because a sticky element can only stick inside its own
   parent's box, and with the perks after it it would unstick halfway down.
   (2) Nothing pushes it down, because THE STAGE ABSORBS THE SLACK. `.cel-stage` is the
   flexible child and `.cel-copy` is content-sized, so on a short book (one perk on a tall
   phone) the cover grows and the footer is still the last thing on the screen — the design's
   own structure. A `margin-top: auto` on the footer would do the same job by leaving a gap
   under the perks instead, which is the emptier-looking half of the same fix.
   (3) THE SIDE PADDING MOVES OFF THE SCROLLER. `.kc-celebrate` is `overflow-y: auto`, which
   computes `overflow-x` to `auto` as well — the trap `.kc-stage` documents — so the footer's
   full-bleed `margin-inline: -16px` against the scroller's own padding would pan the studio
   32px sideways. The inline padding sits on `.cel-copy` instead, and the bleed never leaves
   the scroller's content box.
   (4) `.cel-grid` becomes a flex COLUMN and has to say `align-items: stretch`: its desktop
   `align-items: center` means "centre on the cross axis", which in a column is the horizontal
   one — inherited, `.cel-copy` would shrink to its text and the full-bleed footer with it.
   This also replaces the desktop grid's `margin: auto` centring, which is the invariant that
   stops a taller-than-the-box child being clipped at the TOP: the column starts at the top and
   grows (`flex: 1 0 auto`), so there is nothing to clip.
   (5) THE BUY BUTTON IS NAVY, and it said CORAL here until 2026-08-12. The old reasoning was
   that coral is this site's buy colour and navy is the public library's, so a button that was
   coral on a laptop and navy on a phone was the worse outcome — which is still true, and is now
   satisfied the other way round: the design's revision paints it navy on BOTH viewports, so
   this screen agrees with itself and it is `/shelf`'s coral `.btn-unlock` that is the odd one
   out. The weighting is no longer a departure either: the buy button leads on the phone AND on
   the desktop now.
   (6) NO `:hover` TRANSFORM in the footer. `.cel-buy:hover` lifts 2px and a tap leaves `:hover`
   stuck on a touch browser — the rule `.kc-pm-cta` and `.kc-pm-age` already keep.
   (7) THE FOOTER SURVIVED THE REDESIGN, deliberately. The design's own mobile frame puts the
   two buttons in the flow under the copy, which fits its 393×852 mock with two English perks —
   but the perks are gated and the copy is translated, so a German sub-line wrapping to four
   lines on a 640px phone puts the buy step below the fold with nothing on screen saying it is
   there. What is taken from the design is the LOOK of the pair (the lock, the price behind its
   hairline, the cut corners, one line each); where they sit is this sheet's own answer.
   (8) The twinkles' `bottom:` percentages are against the SCROLLED CONTENT, not the viewport,
   so two of them would land under the sticky footer; they are re-pinned from the top. */
@media (max-width: 640px) {
  .kc-celebrate { padding: 12px 0 0; }
  .cel-grid { flex: 1 0 auto; margin: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0; max-width: none; }
  /* THE COVER IS SIZED OFF WHAT IS LEFT, not off the width. The design does the same
     (`.rd-coverwrap { flex: 1 1 auto; min-height: 150px }`, `.rd-cover { height: 100% }`) and
     it is what fits the whole screen — headline, note, perks and footer — on a 700px phone: a
     width-driven cover puts its overflow on the PERKS, which are the promises the buy step is
     made of, and pushes them under the sticky footer. `.bca` is `aspect-ratio: 3/4` with no
     width of its own, so a height on `.cel-book` is all it takes. The `min-height` is the
     floor: past it (three perks on a 360×720) the stage stops giving and the screen scrolls,
     which is the right way round. */
  .cel-stage { flex: 1 1 auto; align-items: center; min-height: 190px; padding: 4px 0 0; }
  /* `.bca` is what carries the 3/4, so the HEIGHT goes on it and its width follows — an
     `aspect-ratio` on the wrapper as well fights the `min-height` floor above (the wrapper
     stops at the floor, the cover inside keeps its ratio, and the difference hangs over the
     chip below it). `max-height` is the width cap restated in the other axis: 80vw is 60vw
     × 4/3, so a tall phone stops growing the cover instead of squaring off its box. */
  .kc-celebrate .cel-book { height: 100%; width: auto; max-width: 60vw; max-height: 80vw; }
  .kc-celebrate .cel-book .bca { height: 100%; width: auto; }
  .cel-copy { flex: none; }
  .cel-glow { width: min(300px, 82vw); }
  .cel-ribbon { top: 11px; left: -6px; padding: 5px 11px; font-size: 9.5px; letter-spacing: 1.1px; }
  .cel-tw.t1 { top: 7%; left: 8%; font-size: 15px; }
  .cel-tw.t2 { top: 17%; right: 7%; font-size: 18px; }
  .cel-tw.t3 { top: 33%; bottom: auto; left: 5%; font-size: 12px; }
  .cel-tw.t4 { top: 4%; bottom: auto; right: 22%; font-size: 12px; }

  .cel-copy { display: flex; flex-direction: column; padding: 0 16px; }
  .cel-eyebrow { align-self: center; margin-top: 12px; padding: 5px 12px; font-size: 10px; letter-spacing: 1.3px; transform: rotate(-1.6deg); }
  .kc-celebrate h1 { margin-top: 10px; font-weight: 500; font-size: clamp(26px, 8vw, 32px); letter-spacing: -.8px; text-align: center; }
  .kc-celebrate .cel-sub { align-self: center; margin-top: 9px; max-width: 30ch; font-size: 13px; line-height: 1.5; text-align: center; }
  .kc-celebrate .cel-locked { margin-top: 12px; max-width: 34ch; align-self: center; font-size: 13.5px; line-height: 1.5; text-align: center; }
  /* The dashed border becomes the design's hand-drawn squiggle, and the label takes its ✦. */
  .cel-unlock { max-width: none; margin-top: 18px; padding-top: 0; border-top: 0; }
  .cel-unlock::before { content: ""; display: block; height: 9px; margin-bottom: 15px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 9' preserveAspectRatio='none'%3E%3Cpath d='M0 5c10-5 20 5 30 0s20-5 30 0 20 5 30 0 20-5 30 0' fill='none' stroke='%23dcc99f' stroke-width='2'/%3E%3C/svg%3E") center/120px 9px repeat-x; opacity: .9; }
  .cel-unlock-label { display: flex; align-items: center; gap: 7px; margin-bottom: 11px; font-size: 10.5px; letter-spacing: 1.4px; color: #6c604a; }
  .cel-unlock-label::before { content: "✦"; color: var(--gold); font-size: 12px; }
  /* The perks go back to a COLUMN of paper cards. The design's phone frame runs them along one
     row, which fits its two short English strings and nothing longer — a translated
     "Read-aloud in Portuguese" beside "All 24 pages" wraps into two ragged blocks. One per row
     is the same information and cannot wrap wrong. */
  .cel-perks { display: flex; flex-direction: column; gap: 8px; }
  .cel-perks li { gap: 11px; padding: 9px 12px; border: 1.5px solid #ece0c6; border-radius: 9px 14px 10px 13px; background: var(--paper); }
  .cel-perks li:nth-child(1) { transform: rotate(-.5deg); }
  .cel-perks li:nth-child(2) { transform: rotate(.6deg); }
  .cel-perk-text { font-size: 13px; line-height: 1.35; color: #33454f; }
  .cel-tick { width: 28px; height: 28px; }
  .cel-tick .cel-ico { width: 14px; height: 14px; }
  /* The ✦ hangs off the card's own corner here, not off a line of text. */
  .cel-perk-star { top: -7px; right: -4px; font-size: 12px; }

  .cel-actions {
    order: 9; position: sticky; bottom: 0; z-index: 4; margin: 18px -16px 0 -16px;
    display: grid; grid-template-columns: 1.25fr 1fr; gap: 10px;
    padding: 12px 16px calc(14px + env(safe-area-inset-bottom));
    background: rgba(255,253,247,.96); border-top: 1.5px solid var(--border);
    box-shadow: 0 -12px 26px -20px rgba(40,25,5,.8);
  }
  /* One line each and the price still on the buy button, so the phone says the same thing the
     desktop does. `min-width: 0` is what lets a grid track actually shrink — without it the
     button's own content sets a floor and the pair overflows the footer on a 360px screen. */
  .cel-buy, .cel-preview {
    min-width: 0; min-height: 54px; padding: 0 10px; gap: 7px;
    justify-content: center; font-size: 13px; line-height: 1.2; white-space: nowrap;
  }
  .cel-buy { border-radius: 12px 20px 13px 19px; box-shadow: 0 12px 22px -13px rgba(13,47,67,.95); animation: celBob 3.1s ease-in-out infinite; }
  .cel-preview { border-radius: 19px 12px 20px 13px; font-size: 13px; box-shadow: none; }
  /* A tap leaves `:hover` stuck on a touch browser, and the tilt would fight `celBob`. */
  .cel-buy, .cel-buy:hover, .cel-preview, .cel-preview:hover { transform: none; }
  .cel-buy .cel-ico, .cel-preview .cel-ico { width: 17px; height: 17px; }
  .cel-price { padding-left: 8px; margin-left: 0; font-size: 15px; }
  .cel-btn-word { display: none; }
  .cel-btn-short { display: block; font-weight: inherit; }
}
@keyframes celBob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }

/* ── the studio's grown-up gate: "Confirm And Make.dc.html" ────────────────────────────
   The picks read back as a paper docket on the left, the consent and the button on the
   right — the same give-vs-show split the celebration screen below it makes. Four things
   are load-bearing. (1) The grid is centred with `margin: auto` on the CHILD, never
   `align-items: center` on the scroll box: this is the studio's fixed-height body
   (`#kc-nav` is hidden here, so it owns the height), and a centred flex item taller than
   its container is clipped at the TOP with no way to scroll back to it — and the tallest
   state, logged out with the terms unticked, carries an account row AND a consent card AND
   the chips. (2) The classes and keyframes are `ho-`prefixed; the design's own are the bare
   `twinkle`/`tilt`, and this sheet already carries hero-/hm-/fg-/faq-/ft-/sub-/sb-/cel-
   variants where a duplicate `@keyframes` name is last-wins with no warning. (3) The tilt is
   stated on `.ho-stage` as well as inside `hoTilt`, because the reduced-motion block below
   kills the animation and a card whose only rotation lived in the frame would sit straight.
   It is on the STAGE and not the docket so the ribbon tilts with the paper it is stuck to —
   the docket is `overflow: hidden` (the reason the ribbon is a sibling, as `.cel-ribbon` is
   to `.bca`). (4) The consent's checkbox is CLIPPED, never `display: none` — the label is
   what is styled, but the input has to stay focusable or the card is unreachable by
   keyboard; `:has()` is what dresses the ticked and focused states. */
.kc-handoff { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; padding: clamp(14px, 2vw, 20px) clamp(16px, 4vw, 34px) clamp(24px, 4vw, 46px); overflow-y: auto; }

.ho-grid { position: relative; z-index: 1; margin: auto; width: 100%; max-width: 1010px; display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: clamp(24px, 4vw, 44px); align-items: center; padding-top: clamp(18px, 3vw, 30px); }

.ho-stage { position: relative; width: min(352px, 100%); margin: 0 auto; transform: rotate(-1.6deg); animation: hoTilt 7s ease-in-out infinite; }
.ho-docket { position: relative; background: var(--paper); border: 1.5px solid #efe0c0; border-radius: 20px; overflow: hidden; box-shadow: 0 30px 54px -34px rgba(13,47,67,.8), 0 14px 30px -20px rgba(84,58,12,.4); }
.ho-docket-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 15px 20px; background: var(--ink); color: #fdf6e6; font-size: 10.5px; font-weight: 800; letter-spacing: 1.8px; }
.ho-docket-head span + span { letter-spacing: 1.2px; opacity: .72; }
.ho-docket-title { padding: 19px 20px 8px; }
.ho-book { margin: 0; font-family: var(--display); font-size: 23px; line-height: 1.2; color: var(--ink); text-wrap: pretty; }
.ho-book-sub { margin: 5px 0 0; color: #8a7a5f; font-size: 13px; }
.ho-rows { display: flex; flex-direction: column; padding: 4px 20px 18px; }
.ho-row { display: flex; align-items: center; gap: 11px; padding: 10px 0; border-top: 1px dashed #ece0c6; }
.ho-row-icon { flex: none; width: 30px; height: 30px; display: grid; place-items: center; border-radius: 10px; background: #fff6de; font-size: 15px; line-height: 1; }
.ho-row-icon.is-code { font-size: 11px; font-weight: 900; letter-spacing: .5px; color: #a07d3c; }
.ho-row-label { flex: 1; min-width: 0; font-size: 11px; font-weight: 800; letter-spacing: 1.3px; text-transform: uppercase; color: #a4906f; }
.ho-row-value { min-width: 0; color: var(--ink); font-size: 13.5px; text-align: right; }
.ho-tear { height: 18px; border-top: 2px dashed #e6d6b4; background: var(--paper); }
.ho-docket-foot { display: flex; align-items: center; gap: 9px; padding: 13px 20px; background: #fff6de; color: #7d6a4d; font-size: 12.5px; line-height: 1.45; }
.ho-ribbon { position: absolute; z-index: 2; top: -13px; right: -12px; padding: 7px 13px; border-radius: 8px; background: var(--coral); color: #fff; font-size: 10px; font-weight: 900; letter-spacing: 1.4px; transform: rotate(7deg); box-shadow: 0 12px 22px -14px rgba(180,60,40,.95); }

.ho-copy { min-width: 0; text-align: left; }
.kc-handoff h2 { margin: 0; font-family: var(--display); font-weight: 400; font-size: clamp(30px, 4.2vw, 46px); line-height: 1.05; letter-spacing: -1px; color: var(--ink); text-wrap: pretty; }
.ho-lede { margin: 14px 0 0; max-width: 27em; color: #5b6f7c; font-size: clamp(14px, 1.7vw, 16.5px); line-height: 1.55; text-wrap: pretty; }
.ho-account { display: flex; flex-wrap: wrap; gap: 11px; margin-top: 22px; }
.ho-account .btn-lg, .ho-account .cel-grownup { min-height: 54px; border-radius: 999px; }
.ho-consent { position: relative; display: flex; align-items: flex-start; gap: 13px; width: 100%; max-width: 30em; margin-top: 22px; padding: 16px 17px; border: 2px solid #e6d6b4; border-radius: 18px; background: rgba(255,253,247,.6); box-shadow: 0 14px 26px -24px rgba(13,47,67,.6); cursor: pointer; transition: border-color .18s, background .18s, box-shadow .18s; }
.ho-consent input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.ho-consent-box { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 2px solid #d9c9a8; border-radius: 9px; background: #fff; color: transparent; font-size: 14px; font-weight: 900; line-height: 1; transition: background .18s, border-color .18s, color .18s, transform .2s cubic-bezier(.34,1.56,.64,1); }
.ho-consent-copy { flex: 1; min-width: 0; color: #3f5867; font-size: 13.5px; line-height: 1.5; }
.ho-consent-copy a { color: var(--coral-dark); font-weight: 800; }
.ho-consent:has(input:checked) { border-color: #456f5c; background: var(--paper); box-shadow: 0 18px 32px -26px rgba(69,111,92,.9); }
.ho-consent:has(input:checked) .ho-consent-box { border-color: #456f5c; background: #456f5c; color: #fff; transform: rotate(-7deg) scale(1.05); }
.ho-consent:has(input:focus-visible) { border-color: var(--gold); box-shadow: 0 0 0 3px rgba(233,169,43,.22); }

.ho-cta { display: inline-flex; align-items: center; justify-content: center; gap: 9px; width: 100%; max-width: 30em; margin-top: 14px; padding: 18px 28px; border: 0; border-radius: 999px; background: var(--green); color: #fff; font-size: 17.5px; font-weight: 800; cursor: pointer; box-shadow: 0 20px 36px -16px rgba(47,82,68,.95); transition: transform .2s cubic-bezier(.34,1.56,.64,1), background .18s, box-shadow .18s; }
.ho-cta:hover { transform: translateY(-2px); background: var(--green-dark); }
/* Muted, never `disabled`: the click is what says WHICH gate is still shut (the toast, or
   the register dialog), and a disabled button says nothing at all. */
.ho-cta.is-waiting { background: #f0e2c4; color: #b6a382; box-shadow: none; }
.ho-cta.is-waiting:hover { transform: none; background: #ecdcbb; }

.ho-tw { position: absolute; z-index: 0; pointer-events: none; animation: hoTwinkle 5s ease-in-out infinite; }
.ho-tw.t1 { top: 13%; left: 8%; font-size: 20px; color: rgba(233,169,43,.55); }
.ho-tw.t2 { top: 27%; right: 10%; font-size: 15px; color: rgba(235,117,91,.5); animation-duration: 4.2s; animation-delay: .7s; }
.ho-tw.t3 { bottom: 14%; left: 14%; font-size: 13px; color: rgba(125,90,14,.35); animation-duration: 6s; animation-delay: 1.4s; }
.ho-tw.t4 { bottom: 24%; right: 16%; font-size: 18px; color: rgba(233,169,43,.4); animation-duration: 5.6s; animation-delay: 2s; }
@keyframes hoTwinkle { 0%,100% { opacity: .25; transform: scale(.8) rotate(0deg); } 50% { opacity: 1; transform: scale(1.15) rotate(14deg); } }
@keyframes hoTilt { 0%,100% { transform: rotate(-1.6deg); } 50% { transform: rotate(-.4deg) translateY(-6px); } }

/* The labels carry most of the row's identity (the current step's is the coral italic),
   so they survive far lower than the old bar's did — they only go when five of them
   stop fitting side by side. */
@media (max-width: 560px) { .kc-star-label { display: none; } }
/* Under 640 the header is the "Book Steps Mobile" design's: ONE row of ✕ · step line over
   pips · 📚, all of it 44px tall. The row used to wrap the stamps onto a line of their own,
   because Close and "My books" both had a min-content width the middle column could not
   squeeze past — both are icon squares now (the word is a `<b>` this hides, and the glyph
   is redrawn by ::before), so the row fits and the studio keeps the vertical space the
   wrap cost it. The stamps become PIPS: a 6px bar each, coloured by the same .done /
   .current classes kcRefreshBadges already writes, so nothing in the JS knows the
   difference. The focus ring moves onto .kc-star, since the ring hung on .kc-star-icon
   and that is display: none here. */
@media (max-width: 640px) {
  .kc-top { gap: 10px; padding: 10px 14px 8px; background: rgba(253,241,214,.94); border-bottom: 1.5px solid rgba(236,220,187,.9); }
  /* The ✕ is the row's LAST item on a phone (2026-08-17, at the owner's request) — it was
     the first, on the left, where the design put it. Three things are load-bearing.
     (1) It is `order`, in this block only, and never a move in `renderKcStep`: the desktop
     row is `✕ Close` · stamps · account chip and is unmoved, and the studio's whole phone
     layout is CSS over the markup the one renderer already writes.
     (2) `order: 3` puts it past `.kc-account` (order 0), so on a logged-in phone the row
     reads stamps · monogram · ✕ and the close is the corner. `.kc-mtitle` is a child of
     `.kc-progress`, not of `.kc-top`, so its `flex: 1 0 100%` wrap is unaffected by this.
     (3) Tab order still reaches Close FIRST, because `order` moves the box and not the
     DOM. That is the right way round for a close button and is deliberate — this sheet
     argues about tab stops elsewhere (`.kc-exit-word`, the pip's `role="button"`), so it
     is written down rather than left to be re-discovered. */
  .kc-exit { order: 3; width: 44px; height: 44px; min-height: 0; flex: none; padding: 0; gap: 0; border-radius: 14px; justify-content: center; font-size: 17px; box-shadow: none; overflow: hidden; background: var(--paper); border: 1.5px solid #ecdcbb; color: var(--blue); }
  .kc-exit-word { display: none; }
  .kc-exit::before { content: "✕"; }
  /* The account end goes to the monogram alone and a bare count — the `/shelf` bar's own
     ≤720 answer. The chip keeps its 44px through PADDING, never a smaller monogram, and
     the caret goes with the name: a caret over nothing points at a menu the row no longer
     explains. */
  .kc-account { gap: 6px; }
  /* "My books" is moved onto this row with the meter and the chip (2026-08-25) and is the
     one of the three that GOES on a phone. The block above already documents that a fourth
     item starves `.kc-progress`; this would be a fourth, and at 320px the ✕, the
     monogram and a 40px circle leave the pips nothing to shrink into (it was a fifth while
     the plan meter was loose on this row; the meter is inside the chip's menu since
     2026-08-25 and costs the row nothing). It is the right one to
     drop because it is the only one that is a way SOMEWHERE — the meter states the month and
     the chip is the session, both facts about a walk in progress, while the shelf is one ✕
     away and on the header the ✕ lands on. Hidden here only, so `renderHeaderBooks`'s
     two-directional `hidden` is untouched and nothing in the JS knows this block exists.
     The drawer used to make the same call about the plan meter; that rule is gone with the
     meter's move into the account menu, so this is the last hide of its kind on this row. */
  .kc-top .header-books { display: none; }
  .kc-top .acc-chip { min-height: 44px; padding: 4px; gap: 0; }
  .kc-top .acc-stack, .kc-top .acc-caret { display: none; }
  .kc-progress { flex: 1; min-width: 0; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 4px; row-gap: 5px; }
  /* A fourth item on this row starves `.kc-progress`, and `.kc-mtitle` is `flex: 1 0 100%`
     with `min-width: auto` — so the wrapped title row spills out of its column onto whatever
     sits beside it. Its `<strong>` already ellipsises, so letting the row shrink is the fix. */
  .kc-top .kc-mtitle { min-width: 0; }
  .kc-mtitle { flex: 1 0 100%; display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
  .kc-mtitle strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; font-weight: 800; letter-spacing: .2px; color: var(--blue); }
  .kc-mtitle span { flex: none; font-size: 11px; font-weight: 800; letter-spacing: 1px; color: #a4906f; }
  /* The BAR is a ::before and the star itself is padding: a pip is 6px tall, and a 6px
     `role="button"` with a tabindex is a target no thumb can hit — the star-jump the
     desktop studio documents has to survive the restyle, so the tappable box stays 24px
     and only the mark inside it shrinks. (The design's own pips are not interactive.) */
  .kc-star { flex: 1 1 0; min-width: 0; height: auto; padding: 9px 0; background: none; }
  .kc-star::before { content: ""; width: 100%; height: 6px; border-radius: 999px; background: #ecdcbb; transition: background .2s; }
  .kc-star-icon, .kc-star-label { display: none; }
  .kc-star.done::before { background: var(--green); }
  .kc-star.current::before { background: var(--coral); }
  .kc-star:focus-visible { outline: 2px solid var(--coral); outline-offset: 3px; }
}
/* The account end goes compact well before the row stacks: the stamps are a wide middle
   column, and the old shelf button wrapped onto two lines to make room for them — a chip
   carrying a member's own name is wider still, so the name is capped here. It stops at
   641px: below that the chip is the phone header's monogram square, and this block sits
   AFTER that one in the sheet, so an open-ended max-width would win the padding back. The
   meter no longer has a rule here — it collapses at one width for both bands, in the block
   below this one. */
@media (min-width: 641px) and (max-width: 860px) {
  .kc-account { gap: 8px; }
  .kc-top .acc-name { max-width: 92px; }
  .kc-top .acc-chip { gap: 8px; padding: 5px 10px 5px 5px; }
}

@media (prefers-reduced-motion: reduce) {
  .kc-mascot .brand-mark b, .kc-star.current .kc-star-icon, .kc-celebrate .cel-book, .kc-celebrate .cel-book .bca { animation: none !important; }
  .cel-glow, .cel-tw, .ho-stage, .ho-tw { animation: none !important; }
  /* The phone footer's buy button (`celBob`, on a control the thumb is aimed at, so it goes
     with the rest). Its ✦ went with the redesign — `.cel-buy` is the button now. */
  .cel-buy { animation: none !important; }
  .kc-pm-spark, .kc-pm-avatar, .kc-pm-age.selected .kc-pm-age-icon { animation: none !important; }
}

/* kid studio — family picks name inputs */
.kc-family-names { margin-top: 14px; }
.kc-fam-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.kc-fam-name { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; background: #fff; border-radius: 14px 18px 14px 18px; box-shadow: 0 8px 20px rgba(13,47,67,.08); }
.kc-fam-name span { color: var(--ink-soft); font-size: 12px; font-weight: 800; }
.kc-fam-name input { min-height: 42px; padding: 0 12px; border: 2px solid var(--gold-light); border-radius: 10px; background: #fffdf7; color: var(--ink); font-family: var(--display); font-size: 17px; outline: 0; }
.kc-fam-name input:focus { border-color: var(--gold); box-shadow: 0 0 0 3px rgba(233,169,43,.16); }

/* kid studio — merged join step: subheads + photo cards */
.kc-subhead { margin: 14px 2px 10px; color: var(--ink-soft); font-size: 12px; font-weight: 900; letter-spacing: 1px; text-transform: uppercase; }
.kc-subhead:first-child { margin-top: 0; }
.kc-fam-card { display: flex; flex-direction: column; gap: 10px; padding: 14px 16px; background: #fff; border-radius: 16px 20px 16px 20px; box-shadow: 0 8px 20px rgba(13,47,67,.08); }
.kc-you-card { background: #fffaef; box-shadow: inset 0 0 0 2px var(--gold-light), 0 8px 20px rgba(13,47,67,.08); }
.kc-fam-top { display: flex; align-items: center; gap: 10px; }
.kc-fam-emoji { font-size: 30px; line-height: 1; }
.kc-fam-top strong { font-family: var(--display); font-size: 16px; color: var(--ink); }

/* kid studio — the stage area must flex and scroll INSIDE the fixed overlay,
   never grow past it (tall steps were pushing the Back/Next bar off-screen) */
/* `position: relative` is the containing block for the phone's drawn scrollbar, which is
   appended here as a SIBLING of whichever screen is rendered (the handoff, the celebration)
   — inside the scroller it would scroll away with the content. It takes no `overflow`, the
   rule the account menu's `z-index: 20` drop depends on. */
#kc-main { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
#kc-main > .kc-body { flex: 1; min-height: 0; }
.kc-scroll { overflow-x: hidden; scrollbar-width: thin; scrollbar-color: #d7cfbd transparent; }
.kc-fam-card { min-width: 0; }

/* kid studio — per-person pop-up modal */
.kc-modal-back { position: fixed; inset: 0; z-index: 85; display: none; align-items: center; justify-content: center; padding: 20px; background: rgba(43,34,10,.45); backdrop-filter: blur(4px); }
.kc-modal-back.open { display: flex; }
.kc-modal { position: relative; width: min(440px, 100%); max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; padding: 28px 26px 24px; background: var(--paper); border-radius: 22px 26px 22px 26px; box-shadow: 0 30px 70px rgba(13,47,67,.35); text-align: center; animation: celRise .35s cubic-bezier(.2,.9,.3,1.2) both; }
/* Centred the way `.dialog-close` is, and for the reason written there: the grid is what
   places the line box, and the `padding-top` is what corrects the × inside it. It had
   neither and leaned on the UA button's own centring, which carries the glyph's offset. */
.kc-modal-close { position: absolute; top: 12px; right: 12px; width: 36px; height: 36px; display: grid; place-items: center; padding: .13em 0 0; border: 0; border-radius: 50%; background: #f0e9d8; color: var(--ink); font-size: 20px; line-height: 1; cursor: pointer; }
.kc-modal-close:hover { background: #e4dac2; }
.kc-modal-emoji { font-size: 52px; line-height: 1; }
.kc-modal h3 { margin: 0; font-family: var(--display); font-weight: 500; font-size: 24px; color: var(--ink); letter-spacing: -.5px; }
.kc-chip-photo { display: block; width: clamp(40px, 5.4vw, 62px); height: clamp(40px, 5.4vw, 62px); border-radius: 50%; background: center/cover no-repeat; box-shadow: 0 4px 10px rgba(13,47,67,.25); }

/* kid studio — the person modal ("Add Child Modal" Claude Design). Header, a body that
   is the only part that scrolls, and a footer that always states what is still missing.
   Nothing here is preselected: see renderKcPersonModal. */
/* The card carries no overflow of its own: the three ✦ sparkles are pinned OUTSIDE its
   edges, so the head and the foot round their own corners instead and .kc-pm-body is
   the only scroller (it is inset from the radius and needs none). */
.kc-pmodal { position: relative; width: min(560px, 100%); max-height: 90vh; display: flex; flex-direction: column; background: var(--paper); border-radius: 28px; box-shadow: 0 40px 80px -50px rgba(13,47,67,.7), 0 30px 70px rgba(13,47,67,.25); text-align: left; animation: celRise .35s cubic-bezier(.2,.9,.3,1.2) both; }
/* kcPm-prefixed: the sheet already carries several twinkle/bob keyframes (the reader,
   the FAQ, the footer, the studio mascot) and a duplicate @keyframes name is last-wins
   with no warning. --r keeps each bobbing element's own resting tilt. */
@keyframes kcPmTwinkle { 0%, 100% { opacity: .3; transform: scale(.8) rotate(0deg); } 50% { opacity: 1; transform: scale(1.15) rotate(14deg); } }
@keyframes kcPmBob { 0%, 100% { transform: translateY(0) rotate(var(--r, 0deg)); } 50% { transform: translateY(-4px) rotate(var(--r, 0deg)); } }
.kc-pm-spark { position: absolute; line-height: 1; pointer-events: none; animation: kcPmTwinkle 3.2s ease-in-out infinite; }
.kc-pm-spark-a { top: -14px; left: 34px; color: var(--gold); font-size: 18px; }
.kc-pm-spark-b { top: 24px; right: -14px; color: #eb9d8a; font-size: 13px; animation-duration: 4s; animation-delay: .6s; }
.kc-pm-spark-c { bottom: 52px; left: -16px; color: #9fc7d4; font-size: 15px; animation-duration: 3.6s; animation-delay: 1s; }
.kc-pm-head { display: flex; align-items: flex-start; gap: 14px; padding: 26px 26px 20px; border-bottom: 1px solid #f2e8d4; border-radius: 28px 28px 0 0; }
.kc-pm-avatar { --r: -7deg; width: 52px; height: 52px; flex: none; border-radius: 17px; border: 2.5px solid #e6bd3f; background: var(--gold-light) center/cover no-repeat; display: grid; place-items: center; font-size: 26px; line-height: 1; box-shadow: 0 12px 20px -12px rgba(191,146,20,.9); transform: rotate(var(--r)); animation: kcPmBob 3.4s ease-in-out infinite; }
.kc-pm-heading { flex: 1; min-width: 0; }
.kc-pm-heading strong { display: block; font-family: var(--display); font-size: 27px; line-height: 1.15; font-weight: 400; color: var(--ink); }
.kc-pm-heading span { display: block; margin-top: 5px; color: #8a7a5f; font-size: 13.5px; }
.kc-pm-close { width: 34px; height: 34px; flex: none; border: 0; border-radius: 50%; background: #f3e9d5; color: #6b5c45; font-size: 15px; line-height: 1; display: grid; place-items: center; cursor: pointer; transition: transform .15s, background .15s; }
.kc-pm-close:hover { background: #ecdfc4; transform: rotate(90deg); }
.kc-pm-body { flex: 1; min-height: 0; overflow-y: auto; padding: 22px 26px 24px; display: flex; flex-direction: column; gap: 22px; }
.kc-pm-field { display: block; min-width: 0; }
.kc-pm-row { display: flex; gap: 22px; flex-wrap: wrap; }
.kc-pm-row > .kc-pm-field { flex: 1; min-width: 190px; }
.kc-pm-legend { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
/* Each numbered badge has its own colour and its own tilt — the design's 1-2-3 strip
   reads as three hand-placed stickers rather than a numbered list. A companion's
   fields are unnumbered, so they render no badge and none of this applies. */
.kc-pm-step { width: 22px; height: 22px; flex: none; border-radius: 50%; font-size: 11.5px; font-weight: 800; display: grid; place-items: center; }
.kc-pm-step-1 { background: var(--coral); color: #fff; transform: rotate(-6deg); box-shadow: 0 6px 12px -8px rgba(215,91,69,.95); }
.kc-pm-step-2 { background: var(--gold); color: #3b2c08; transform: rotate(5deg); box-shadow: 0 6px 12px -8px rgba(191,146,20,.95); }
.kc-pm-step-3 { background: var(--green); color: #fff; transform: rotate(-4deg); box-shadow: 0 6px 12px -8px rgba(69,111,92,.95); }
.kc-pm-label { font-family: var(--display); font-style: italic; font-size: 16.5px; color: var(--ink); }
.kc-pm-hint { color: #a4906f; font-size: 12.5px; }
.kc-pm-opt { padding: 3px 9px; border-radius: 99px; background: #f3e9d5; color: #a4906f; font-size: 11.5px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; }
.kc-pm-input { width: 100%; margin-top: 10px; padding: 14px 16px; border: 1.5px solid #e6d8ba; border-radius: 14px; background: var(--paper); color: var(--ink); font-family: var(--display); font-size: 19px; outline: 0; }
.kc-pm-input:focus { border-color: var(--blue); }
.kc-pm-input-year { max-width: 190px; }
.kc-pm-ages { margin-top: 11px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; }
.kc-pm-age { display: flex; flex-direction: column; align-items: center; gap: 7px; padding: 15px 8px; border: 2px solid #ece0c6; border-radius: 18px; background: var(--paper); color: var(--ink); cursor: pointer; transition: transform .16s, border-color .16s, background .16s, box-shadow .16s; }
.kc-pm-age:hover { transform: translateY(-3px) rotate(-1.5deg); }
.kc-pm-age.selected { border-color: var(--coral); background: #fff6ec; transform: translateY(-3px) rotate(-1.5deg); box-shadow: 0 16px 26px -18px rgba(215,91,69,.95); }
.kc-pm-age-icon { font-size: 26px; line-height: 1; opacity: .75; }
.kc-pm-age.selected .kc-pm-age-icon { opacity: 1; animation: kcPmBob 2.4s ease-in-out infinite; }
.kc-pm-age-range { font-family: var(--display); font-size: 20px; line-height: 1; font-weight: 400; }
.kc-pm-age-sub { font-family: var(--display); font-style: italic; color: #8a7a5f; font-size: 13.5px; text-align: center; }
.kc-pm-seg { margin-top: 11px; display: flex; gap: 4px; padding: 4px; border-radius: 13px; background: #f3e9d5; }
.kc-pm-seg-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; border: 0; padding: 12px 10px; border-radius: 11px; background: transparent; color: #8a7a5f; font-size: 14.5px; font-weight: 700; cursor: pointer; transition: background .16s, color .16s, transform .16s; }
.kc-pm-seg-btn.selected { background: var(--paper); color: var(--ink); transform: rotate(-1deg); box-shadow: 0 6px 12px -6px rgba(13,47,67,.5); }
.kc-pm-seg-icon { font-size: 17px; line-height: 1; }
/* "Add Child Modal v2" turns the photo field from a dashed box the picture fills into a
   ROW: a round face on the left and two lines of text beside it — a title that names the
   state and a hint under it. The row is what the EMPTY state is, and it is the design as
   drawn (49px, the drawn camera in a cream disc, the dashed border warming on hover). */
.kc-pm-photo { width: 100%; margin-top: 11px; display: flex; align-items: center; gap: 11px; padding: 3px 14px 3px 3px; border: 2px dashed #e2d3b4; border-radius: 15px; background: var(--paper); color: var(--ink); text-align: left; cursor: pointer; transition: border-color .15s, background .15s; }
.kc-pm-photo:hover { border-color: #eb9d8a; background: #fffaf5; }
.kc-pm-face { width: 41px; height: 41px; flex: none; display: grid; place-items: center; border: 1.5px solid #ecd39a; border-radius: 50%; background: #fdf0cf center/cover no-repeat; }
/* The drawn camera, not 📸 — the design draws it, and at 41px an emoji's own baseline
   and padding leave it sitting off-centre in the disc. */
.kc-pm-cam { position: relative; display: block; width: 19px; height: 15px; border-radius: 4px; background: #a86a1c; }
.kc-pm-cam::before { content: ""; position: absolute; left: 5.5px; top: -3px; width: 8px; height: 3px; border-radius: 2px 2px 0 0; background: #a86a1c; }
.kc-pm-cam::after { content: ""; position: absolute; left: 50%; top: 50%; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%; background: #fdf0cf; }
.kc-pm-photo-copy { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.kc-pm-photo-copy strong { font-size: 13.5px; font-weight: 700; line-height: 1.2; }
.kc-pm-photo-copy span { color: #a4906f; font-size: 11px; font-weight: 600; line-height: 1.25; }
/* A FILLED field is the SAME 49px row, and that is the point: this field shares its row
   with the gender segment, so a preview that takes its own size makes the photo column
   taller than the control beside it and the modal grows a dead half-column of paper. The
   photo goes into the disc it already occupies — solid green border and tinted paper are
   what say "filled" — and nothing about the row's geometry moves. */
.kc-pm-photo.has-photo { border-style: solid; border-color: var(--green); background: #fffaef; }
.kc-pm-photo.has-photo .kc-pm-face { border-color: var(--green); background-color: var(--paper); background-size: cover; }
/* Removing the photo is a real need now that consent gates the button: without it a
   grown-up who will not tick has no way out but closing the modal, which throws away
   the name they typed. It rides the LEGEND line, not a line of its own under the button —
   a row added below is the same expansion the preview was, one line further down — and it
   is a SIBLING of the photo button either way, since a button nested in a button never
   gets the click (the rule the consent tick already keeps). */
.kc-pm-photo-clear { margin-left: auto; border: 0; padding: 0; background: transparent; color: #a4906f; font-size: 11.5px; font-weight: 800; text-decoration: underline; cursor: pointer; }
.kc-pm-photo-clear:hover { color: var(--coral-dark); }
/* The consent card is v2's, and it only exists once a photo does — a permission box with
   nothing to permit is furniture. The input is clipped rather than display:none so it
   keeps its place in the tab order and the label's click target, and the drawn box beside
   it carries the focus ring the hidden input would have shown (the .ho-consent idiom). */
.kc-pm-consent { position: relative; display: flex; align-items: flex-start; gap: 13px; padding: 14px 16px; border: 1.5px solid #f0e4cb; border-radius: 18px; background: #fdf8ec; cursor: pointer; transition: border-color .15s, background .15s; }
.kc-pm-consent:hover { border-color: #e6bd3f; background: #fdf3e0; }
.kc-pm-consent input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.kc-pm-consent-box { flex: none; width: 24px; height: 24px; margin-top: 1px; display: grid; place-items: center; border: 2px solid #d9c9a6; border-radius: 8px; background: var(--paper); transition: background .16s, border-color .16s, box-shadow .16s; }
/* A drawn tick, not ✓ — the glyph falls out of the text font on some platforms, and this
   one has to pop, which a character cannot do. */
.kc-pm-consent-tick { width: 11px; height: 6px; margin-top: -3px; border-left: 2.5px solid #fff; border-bottom: 2.5px solid #fff; transform: rotate(-45deg) scale(.4); opacity: 0; transition: transform .18s cubic-bezier(.2,1.6,.4,1), opacity .12s; }
.kc-pm-consent:has(input:checked) .kc-pm-consent-box { border-color: var(--green); background: var(--green); }
.kc-pm-consent:has(input:checked) .kc-pm-consent-tick { transform: rotate(-45deg) scale(1); opacity: 1; }
.kc-pm-consent:has(input:focus-visible) .kc-pm-consent-box { box-shadow: 0 0 0 3px rgba(69,111,92,.28); }
.kc-pm-consent-copy { flex: 1; min-width: 0; }
.kc-pm-consent-copy strong { display: block; color: var(--ink); font-size: 14px; font-weight: 800; line-height: 1.3; }
.kc-pm-consent-copy span { display: block; margin-top: 3px; color: #8a7a5f; font-size: 12.5px; line-height: 1.5; }
.kc-pm-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 16px 26px; border-top: 1px solid #f2e8d4; border-radius: 0 0 28px 28px; background: #fffaef; }
.kc-pm-summary { flex: 1; min-width: 180px; font-family: var(--display); font-style: italic; color: #6b5c45; font-size: 15px; }
.kc-pm-remove { border: 0; padding: 8px 4px; background: transparent; color: #a4906f; font-size: 12px; font-weight: 800; text-decoration: underline; cursor: pointer; }
.kc-pm-remove:hover { color: var(--coral-dark); }
.kc-pm-cta { border: 0; border-radius: 999px; padding: 14px 28px; background: var(--green); color: #fff; font-size: 15px; font-weight: 800; box-shadow: 0 14px 26px -14px rgba(47,82,68,.95); cursor: pointer; transition: transform .16s, background .16s; }
.kc-pm-cta:hover { background: var(--green-dark); transform: translateY(-2px) rotate(-1deg); }
.kc-pm-cta:disabled { background: #f2e8d4; color: #b3a081; box-shadow: none; transform: none; cursor: not-allowed; }
/* On a phone the card is the "Child Modal Mobile" design: a bottom SHEET, and the same
   markup renderKcPersonModal already writes — the rule the whole phone studio keeps. The
   breakpoint is the studio's own 640 (it was 520, which left a 520–640 band showing a
   centred 560px card floating in a phone-shaped viewport). The three-up age cards become
   full-width rows with a tick, the segmented gender track becomes two cards, and the foot
   stacks its line over a full-width button.
   Scoped to `.kc-modal-back:has(.kc-pmodal)` so the Bible and fairy-tale pickers — which
   ride the OLD .kc-modal shell in the same backdrop — keep centring.
   The grab handle is decoration: it drags nothing, and the dismissals are the ones this
   backdrop already has (its own click, Escape, the ✕). */
@media (max-width: 640px) {
  .kc-modal-back:has(.kc-pmodal) { align-items: flex-end; padding: 0; }
  /* dvh, not vh: the phone's URL bar is inside vh, and the foot is what states what is
     still missing. Still no `overflow` on the card — .kc-pm-body has to stay the ONE
     scroller, or photoJustAdded's scrollIntoView starts moving the wrong box. */
  .kc-pmodal { width: 100%; max-height: 90dvh; border-radius: 22px 22px 0 0; box-shadow: 0 -14px 40px rgba(13,47,67,.3); animation: kcPmSheet .32s cubic-bezier(.2,.9,.3,1) both; }
  .kc-pmodal::before { content: ""; flex: none; width: 44px; height: 4px; margin: 8px auto 0; border-radius: 99px; background: #ded6c2; }
  @keyframes kcPmSheet { from { transform: translateY(100%); } to { transform: none; } }
  /* The sparks are pinned OUTSIDE the card's edges; against the screen's own edges two of
     the three are off it. */
  .kc-pm-spark { display: none; }
  .kc-pm-head { align-items: center; gap: 11px; padding: 12px 16px; border-radius: 0; border-bottom: 0; }
  .kc-pm-avatar { width: 44px; height: 44px; border-radius: 12px 15px 11px 14px; font-size: 24px; }
  .kc-pm-heading strong { font-size: 21px; }
  .kc-pm-heading span { margin-top: 2px; font-size: 12.5px; }
  .kc-pm-close { width: 32px; height: 32px; }
  .kc-pm-body { gap: 16px; padding: 16px; border-top: 1px solid var(--border); }
  .kc-pm-row { gap: 16px; }
  /* The design puts the age hint on its own indented line. The legend is already
     flex-wrap, so a full basis is the whole of it — no copy change, which would show up
     on the desktop card too. */
  .kc-pm-hint { flex-basis: 100%; margin: -2px 0 0 30px; }
  /* Three 1/3-width cards at 390px are three cramped columns, so the band becomes a row
     per band: emoji, range, what it means, and a tick that says which one is chosen. The
     tick is an ::after and therefore the LAST grid item — a ::before would take column
     one from the emoji. */
  .kc-pm-ages { margin-top: 10px; grid-template-columns: 1fr; gap: 7px; }
  .kc-pm-age { display: grid; grid-template-columns: 30px 46px 1fr 18px; align-items: center; justify-items: start; gap: 10px; min-height: 48px; padding: 8px 12px; border-width: 1.5px; border-radius: 9px 13px 10px 12px; }
  /* The tilt goes on BOTH states, not just .selected: a tap leaves :hover stuck on a
     touch browser, and a rotated full-width row hangs off the side of the sheet. */
  .kc-pm-age:hover, .kc-pm-age.selected { transform: none; box-shadow: none; }
  .kc-pm-age-icon { font-size: 21px; opacity: 1; }
  .kc-pm-age-range { font-size: 18px; }
  .kc-pm-age-sub { font-family: var(--sans); font-style: normal; font-size: 12.5px; color: #5f5644; text-align: left; }
  .kc-pm-age::after { content: "✓"; width: 18px; height: 18px; display: grid; place-items: center; border: 1.5px solid #cfc7b2; border-radius: 50%; font-size: 11px; color: transparent; }
  .kc-pm-age.selected::after { border-color: var(--coral); background: var(--coral); color: #fff; }
  /* The gender track loses its trough and becomes the design's two cards. It keeps the
     modal's CORAL selection rather than the design's blue: blue-light selection appears
     nowhere else in the studio (.kc-choice.selected is a gold border and a coral tick),
     and one control that is coral on a laptop and blue on a phone is the worse outcome. */
  .kc-pm-seg { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 9px; padding: 0; border-radius: 0; background: transparent; }
  .kc-pm-seg-btn { min-height: 48px; border: 1.5px solid #e6d8ba; border-radius: 9px 13px 10px 12px; background: var(--paper); color: var(--ink); }
  .kc-pm-seg-btn.selected { border-color: var(--coral); background: #fff6ec; color: var(--ink); transform: none; box-shadow: none; }
  .kc-pm-photo { border-radius: 9px 13px 10px 12px; }
  /* The foot stacks: the line that says what is missing sits ABOVE a full-width button,
     centred, and the sheet pads past the home indicator. The button keeps its coral and
     takes the design's hand-drawn radius. */
  .kc-pm-foot { flex-direction: column; align-items: stretch; gap: 8px; padding: 12px 16px calc(14px + env(safe-area-inset-bottom)); border-radius: 0; background: var(--cream); }
  .kc-pm-summary { order: 1; min-width: 0; font-family: var(--sans); font-style: normal; font-size: 12.5px; color: #6c604a; text-align: center; }
  .kc-pm-cta { order: 2; width: 100%; min-height: 50px; padding: 14px 20px; border-radius: 12px 16px 13px 15px; }
  .kc-pm-cta:hover { transform: none; }
  /* The companion card's Remove: a stretched flex column would make this underlined link
     a full-bleed bar under the button, reading as a second action of equal weight. */
  .kc-pm-remove { order: 3; align-self: center; }
}
/* The sheet slides up; reduced motion takes that and nothing else — the desktop card's
   own celRise is older than this port and is left alone. */
@media (prefers-reduced-motion: reduce) and (max-width: 640px) {
  .kc-pmodal { animation: none !important; }
}

/* classic (grown-up) builder hidden until toggled — kid studio is the default */
/* The creator section paints nothing: the teaser band that opened the studio (and
   the toggle that was the only way into the classic builder) is gone, so the whole
   section is `hidden` in the markup. The form itself stays in the DOM — app.js reads
   and writes it — and `hidden` needs this rule to beat the section's own display. */
.creator-section[hidden] { display: none; }

/* bible picker modal */
.kc-modal-wide { width: min(620px, 100%); }
.kc-bible-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.kc-bible-grid .kc-emoji { font-size: 38px; }
.kc-bible-grid .kc-clabel { font-size: 12px; }
.kc-bible-note { margin: 2px 0 0; color: #98a2a4; font-size: 11px; }
.kc-bible-search { width: min(360px, 100%); margin: 0 auto 14px; display: block; padding: 10px 14px; border: 2px solid var(--gold-light); border-radius: 14px; font: inherit; font-size: 15px; color: var(--ink); outline: 0; }
.kc-bible-search:focus { border-color: var(--gold); box-shadow: 0 0 0 4px rgba(233,169,43,.15); }

/* Both wide pickers (the fairy tales and the Bible accounts) are a long grid inside a
   `max-height: 90vh` card, so the SCROLLER is `.kc-modal` itself — 39 tales cannot be
   reached any other way. It gets the studio's own playful bar: a cream trough and a gold
   barber's stripe, the `.kc-sb-gold` thumb the picker panes ride, drawn here as the native
   bar rather than as a mounted track because there is no positioned pane to hang one in.
   Three things follow. It is `::-webkit-scrollbar*` and NOTHING else — declaring
   `scrollbar-color` or `scrollbar-width` on this element makes Blink drop every rule below
   it (the note on `.kc-sb-pane` states this in full), so Firefox keeps its native bar, the
   same trade the picker's phone bar already accepts. The trough is `background-clip` with a
   transparent border so it reads as an inset track rather than a full-bleed column, the
   `.cs-list` precedent. And it is scoped to `.kc-modal-wide`: the person modal is a short
   form that must not grow a decorated bar the moment a name wraps. */
.kc-modal-wide::-webkit-scrollbar, .story-copy-page::-webkit-scrollbar { width: 14px; }
.kc-modal-wide::-webkit-scrollbar-track, .story-copy-page::-webkit-scrollbar-track { background: #fdeecb; border: 4px solid var(--paper); border-radius: 999px; background-clip: padding-box; }
.kc-modal-wide::-webkit-scrollbar-thumb, .story-copy-page::-webkit-scrollbar-thumb { border: 3px solid var(--paper); border-radius: 999px; background: repeating-linear-gradient(135deg, #e9a92b 0 7px, #f7d97d 7px 14px); background-clip: padding-box; }
.kc-modal-wide::-webkit-scrollbar-thumb:hover, .story-copy-page::-webkit-scrollbar-thumb:hover { background: repeating-linear-gradient(135deg, #dd9a1c 0 7px, #f3d068 7px 14px); background-clip: padding-box; }

/* The contact dialog (V42). It is the old feature-request dialog grown a subject and an
   address: "Request a feature" is one of the subjects now, so there is one form for every
   way of writing to us rather than a dialog beside two mailto links. The field rules cover
   the select, the input and the textarea together — three controls that must not look like
   three different kinds of thing on one short form. */
.contact-dialog { width: min(480px, calc(100% - 28px)); border: 0; border-radius: 20px; padding: 30px 28px; background: var(--paper); box-shadow: 0 30px 80px rgba(10,42,59,.3); }
.contact-dialog::backdrop { background: rgba(13,47,67,.55); }
.contact-dialog h3 { margin: 0 0 6px; font-family: var(--display); font-size: 24px; color: var(--ink); }
.contact-dialog p { margin: 0 0 16px; color: var(--ink-soft); font-size: 14px; line-height: 1.5; }
.contact-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.contact-field > span { font-size: 13px; font-weight: 700; color: var(--ink); }
.contact-dialog select,
.contact-dialog input,
.contact-dialog textarea { width: 100%; box-sizing: border-box; padding: 12px 14px; border: 1px solid #d5d1c7; border-radius: 12px; font: inherit; font-size: 15px; color: var(--ink); background: #fff; }
.contact-dialog textarea { resize: vertical; }
.contact-dialog select:focus,
.contact-dialog input:focus,
.contact-dialog textarea:focus { outline: 0; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(233,169,43,.15); }
.contact-dialog .button { width: 100%; }

/* The public-library confirmation, the portal's twin of the one hand-written into
   shelf.html (that page loads no app.js and no styles.css, so the two are a deliberate
   copy of each other and move together). It is dressed as the contact dialog rather than
   as a panel: it is a question with an answer, not a surface. */
.lib-dialog { position: relative; width: min(480px, calc(100% - 28px)); border: 0; border-radius: 20px; padding: 30px 28px; background: var(--paper); box-shadow: 0 30px 80px rgba(10,42,59,.3); }
.lib-dialog::backdrop { background: rgba(13,47,67,.55); }
.lib-dialog h3 { margin: 0 0 6px; font-family: var(--display); font-size: 24px; color: var(--ink); }
.lib-dialog > p { margin: 0 0 14px; color: var(--ink-soft); font-size: 14px; line-height: 1.5; }
.lib-points { margin: 0 0 16px; padding: 0; list-style: none; display: grid; gap: 9px; }
.lib-points li { display: grid; grid-template-columns: 18px 1fr; gap: 9px; align-items: start; font-size: 14px; line-height: 1.45; color: var(--ink); }
.lib-points li > span:first-child { text-align: center; }
/* The reward row is hidden by `hidden` when the admin's amount is zero, and `hidden`
   cannot beat the `display: grid` this rule sets — the same trap `.addon-card[hidden]`
   and `.kc-star[hidden]` exist for. */
.lib-points li[hidden] { display: none; }
.lib-consent { display: flex; gap: 10px; align-items: flex-start; margin: 0 0 18px; padding: 12px 13px; border: 1px solid #ded8ca; border-radius: 13px; background: #fbf7ec; font-size: 14px; line-height: 1.4; color: var(--ink); cursor: pointer; }
.lib-consent input { margin: 2px 0 0; width: 17px; height: 17px; flex: none; accent-color: var(--blue); }
.lib-actions { display: flex; gap: 10px; justify-content: flex-end; }
.lib-actions button { border-radius: 999px; padding: 11px 18px; font: inherit; font-weight: 700; font-size: 14.5px; cursor: pointer; }
.lib-cancel { border: 1px solid #d5d1c7; background: #fff; color: var(--ink); }
.lib-confirm { border: 0; background: var(--blue); color: #fdf6e6; }
.lib-confirm:disabled { opacity: .45; cursor: not-allowed; }

/* kid studio — step 1 (the "Story Studio - Country Language Accent" design). Three
   numbered COLUMNS standing side by side in one panel: the country we already guessed,
   the languages of that country, and the narrator's accent. It was three sections stacked
   in one card with the country list folded away behind "Change country"; the columns give
   the list a column's worth of height with nothing below it to push off the screen, so the
   fold is gone and the guess is a banner above the list instead.

   The country list is the ONLY scroller — 55 countries never fit and the other two columns
   always do. Its rows stay the compact .kc-ls-copt they already were rather than becoming
   .kc-choice tiles: a 160px tile each would put the list straight back into the vertical
   scroll this layout exists to remove.

   Deliberately quieter than the rest of the studio: no .kc-choice tiles, no bounce. This
   is the step a grown-up answers, and it sits between the question and the picker steps that
   the child drives.

   It is FULL WIDTH — the stage's own box, exactly as the three rail-and-grid steps are, which
   have no cap of their own. It carried the design's `min(1120px, 100%)` and on a wide screen
   that read as a step that had shrunk beside the one after it. The country list absorbs the
   extra width as MORE COLUMNS (its `auto-fit, minmax(196px, 1fr)`), so the panel gets shorter
   rather than sparser, which is the direction the max-height cap below already pulls in. */
.kc-ls-panel { width: 100%; display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr) minmax(0, 1fr); background: var(--paper); border: 1px solid #ece3d0; border-radius: 20px; box-shadow: 0 14px 30px -26px rgba(10,42,59,.4); overflow: hidden; }
.kc-ls-col { min-width: 0; display: flex; flex-direction: column; padding: 19px 20px 20px; border-left: 1px solid #f2ebd9; }
.kc-ls-col:first-child { border-left: 0; }
.kc-ls-col-accent { background: #fffaef; }
/* The phone accordion's own two pieces are inert up here ("Story Studio - Language Mobile",
   the .kc-exit-word / .kc-back-word rule): the collapsed row is not rendered at all — a
   display:none button is not a tab stop, which is why this needs no `disabled` and no
   matchMedia listener — and the body wrapper is `display: contents`, so the column's own
   flex children are exactly the children they were. */
.kc-ls-body { display: contents; }
.kc-ls-row { display: none; }
/* The accent column stays on the page for a non-English book, greyed rather than gone:
   the panel is a fixed three-column grid, and a column that vanished on a language click
   would jump the other two sideways under the pointer that just clicked. */
.kc-ls-col.off .kc-ls-num { background: #e2dccd; color: #8f8878; }
.kc-ls-col.off .kc-ls-legend { color: #9b8f7d; }

.kc-ls-empty { margin: 0; padding: 15px 16px; border: 1px dashed #e2d6bb; border-radius: 13px; background: #fdf8ec; font-size: 12.5px; line-height: 1.6; color: #7d6f57; text-wrap: pretty; }

/* Still the finish step's shell — see the "Finish Step" block below. */
.kc-lang-step { width: min(760px, 100%); margin-inline: auto; display: flex; flex-direction: column; gap: 14px; padding: 2px 0 6px; }
.kc-ls-card { background: var(--paper); border: 1px solid #ece3d0; border-radius: 20px; box-shadow: 0 14px 30px -26px rgba(10,42,59,.4); overflow: hidden; }
.kc-ls-sec { padding: 20px 22px; }
.kc-ls-rule { height: 1px; background: #f2ebd9; }
.kc-ls-sec-accent { border-top: 1px solid #f2ebd9; background: #fffaef; }

.kc-ls-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.kc-ls-col .kc-ls-head { margin-bottom: 14px; }
.kc-ls-num { width: 21px; height: 21px; flex: none; display: grid; place-items: center; border-radius: 50%; background: var(--blue); color: #fdf6e6; font-size: 11.5px; font-weight: 900; }
.kc-ls-num-coral { background: var(--coral); color: #fff; }
.kc-ls-legend { font-size: 11px; font-weight: 900; letter-spacing: 1.2px; text-transform: uppercase; color: #8a7a5f; }
.kc-ls-aside { font-size: 12.5px; color: #a4906f; }

/* the fifty-five-country list, always open — the one scroller on the panel */
.kc-ls-search { display: flex; align-items: center; gap: 9px; margin-bottom: 11px; padding: 10px 14px; border: 1.5px solid #ece3d0; border-radius: 12px; background: #fffdf7; }
.kc-ls-glass { color: #a4906f; }
.kc-ls-search input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; font-size: 14px; color: var(--ink); }
.kc-ls-search input::placeholder { color: #c9bda3; }
/* The design's 392px, but capped against the viewport as well: the studio is a
   fixed-height column and the panel is the tallest thing on the step, so on a laptop a
   flat 392px pushes the whole panel into the stage's scroll — which is what the
   side-by-side layout was built to avoid. Nothing is CLIPPED either way; past the floor
   the stage scrolls, exactly as the picker steps' do. */
.kc-ls-clist { max-height: min(392px, 46vh); overflow-y: auto; padding-right: 4px; display: grid; grid-template-columns: repeat(auto-fit, minmax(196px, 1fr)); gap: 8px; align-content: start; }
.kc-ls-copt { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border: 1px solid #f2ebd9; border-radius: 11px; background: var(--paper); color: var(--ink); font-size: 13.5px; font-weight: 800; text-align: left; cursor: pointer; }
.kc-ls-copt:hover { border-color: #dccfb4; }
.kc-ls-copt.selected { border-color: var(--blue); background: var(--cream); }
/* a filtered-out country is hidden with the attribute, and this is what makes it bite */
.kc-ls-copt[hidden] { display: none; }
.kc-ls-cflag { font-size: 17px; line-height: 1; }
.kc-ls-cname { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kc-ls-clangs { margin-left: auto; font-size: 11.5px; font-weight: 700; color: #a4906f; white-space: nowrap; }

/* the languages of that country — ONE per row. The column is a third of the panel, so the
   old `auto-fill, minmax(216px, 1fr)` would sit at one column anyway on every viewport it
   can reach; saying it outright stops a wide screen putting two half-width cards in a
   column narrower than the country list beside it. */
.kc-ls-langs { display: grid; gap: 9px; align-content: start; }
.kc-ls-lang { display: flex; align-items: center; gap: 11px; padding: 11px 13px; border: 1.5px solid #ece3d0; border-radius: 15px; background: var(--paper); cursor: pointer; text-align: left; }
.kc-ls-lang:hover { border-color: #dccfb4; }
.kc-ls-lang.selected { border-color: var(--blue); background: var(--cream); box-shadow: 0 6px 14px -12px rgba(10,42,59,.7); }
.kc-ls-plate { width: 34px; height: 34px; flex: none; display: grid; place-items: center; border-radius: 11px; background: var(--cream); color: #a4906f; font-family: var(--display); font-size: 14px; font-weight: 700; }
.kc-ls-lang.selected .kc-ls-plate { background: var(--blue-light); color: var(--blue); }
.kc-ls-lang.selected.is-en .kc-ls-plate { background: var(--gold-light); color: #7d5a0e; }
.kc-ls-ltext { flex: 1; min-width: 0; }
.kc-ls-ltext strong { display: block; font-size: 14.5px; line-height: 1.2; color: var(--ink); }
/* wraps rather than ellipsing: the only long one is English's "always available · picks
   an accent", and a truncated "Always available · pi…" says less than nothing */
.kc-ls-native { display: block; margin-top: 1px; font-size: 12px; line-height: 1.35; color: #8a7a5f; }
/* a language the narrator cannot speak says so on its own card, not only in the note. It
   rides UNDER the native name inside .kc-ls-ltext rather than as a third flex child of the
   card: beside the tick it had nowhere to shrink to and sat on top of the language's name. */
.kc-ls-mute { display: inline-block; margin-top: 5px; white-space: nowrap; padding: 3px 8px; border-radius: 999px; background: #f2ebd9; color: #8a7a5f; font-size: 10px; font-weight: 900; letter-spacing: .4px; text-transform: uppercase; }
.kc-ls-tick { margin-left: auto; width: 22px; height: 22px; flex: none; display: grid; place-items: center; border: 1.5px solid #e0d3b4; border-radius: 50%; background: transparent; color: transparent; font-size: 12px; font-weight: 900; }
.kc-ls-lang.selected .kc-ls-tick { border-color: var(--blue); background: var(--blue); color: #fdf6e6; }
.kc-ls-mutenote { margin: 11px 0 0; color: var(--ink-soft); font-size: 12px; line-height: 1.5; }

/* the narrator's accent — English only */
.kc-ls-accents { display: flex; gap: 8px; flex-wrap: wrap; align-content: start; }
.kc-ls-accent-chip { display: flex; align-items: center; gap: 8px; padding: 10px 15px; border: 1.5px solid #ece3d0; border-radius: 999px; background: var(--paper); color: var(--ink-soft); font-size: 13.5px; font-weight: 900; cursor: pointer; }
.kc-ls-accent-chip:hover { border-color: #dccfb4; }
.kc-ls-accent-chip span { font-size: 16px; line-height: 1; }
.kc-ls-accent-chip.selected { border-color: var(--coral); background: #fdece8; color: #c94a2c; }

/* Under 1080 the three columns stack: a 1fr country list beside two 1fr columns is a
   two-word ellipsis each. The seam between them turns from a left border into a top one,
   and the country list gives back the height the stack now needs. */
@media (max-width: 1080px) {
  .kc-ls-panel { grid-template-columns: 1fr; }
  .kc-ls-col { border-left: 0; border-top: 1px solid #f2ebd9; }
  .kc-ls-col:first-child { border-top: 0; }
  .kc-ls-clist { max-height: 300px; }
}

/* kid studio — the last step (the "Finish Step" design). It borrows step 1's card and
   numbered sections wholesale (.kc-lang-step / .kc-ls-*), because it asks the same kind
   of question: a grown-up's setting, quieter than the picker steps the child drives.
   Only the 3-up option buttons and the tinted sample foot are its own. */
.kc-fs-opts { margin-top: 12px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; }
.kc-fs-opt { display: flex; flex-direction: column; gap: 5px; padding: 13px 14px; border: 1.5px solid #ece3d0; border-radius: 15px; background: var(--paper); color: var(--ink); text-align: left; cursor: pointer; }
.kc-fs-opt:hover { border-color: #dccfb4; }
.kc-fs-opt.selected { border-color: var(--gold); background: #fffaef; box-shadow: 0 10px 20px -16px rgba(125,90,14,.9); }
.kc-fs-top { display: flex; align-items: center; gap: 9px; }
.kc-fs-top strong { font-size: 14.5px; }
.kc-fs-emoji { font-size: 20px; line-height: 1; }
/* Its own tick, never .kc-check — that one is absolutely positioned for the big tiles. */
.kc-fs-tick { margin-left: auto; width: 20px; height: 20px; flex: none; display: grid; place-items: center; border: 1.5px solid #e6d8ba; border-radius: 50%; background: transparent; color: transparent; font-size: 11px; font-weight: 900; }
.kc-fs-opt.selected .kc-fs-tick { border-color: var(--gold); background: var(--gold); color: #fffdf7; }
.kc-fs-sub { font-size: 12.5px; font-weight: 800; color: #8a7a5f; }
.kc-fs-opt.selected .kc-fs-sub { color: #a07d3c; }

.kc-fs-sample { margin: 9px 0 0; font-family: var(--display); font-size: 19px; line-height: 1.5; color: #2c4a5c; text-wrap: pretty; }
/* The pill is a real button now (V54): it plays the baked clip of the sentence above it.
   It was a label, and kept the same shape deliberately — the family has already learnt what
   it says — so what marks it as pressable is the pointer, the lift on hover and the coral it
   takes while a clip is playing. The tap target is 44px through PADDING rather than a taller
   pill, the rule the studio's header buttons keep. */
.kc-fs-voice { display: inline-flex; align-items: center; gap: 8px; margin-top: 12px; padding: 11px 16px; border: 1px solid #ece0c6; border-radius: 999px; background: var(--paper); font-family: inherit; font-size: 12.5px; font-weight: 800; color: var(--blue); cursor: pointer; transition: transform .12s ease, border-color .12s ease, color .12s ease; }
.kc-fs-voice:hover { border-color: var(--gold); transform: translateY(-1px); }
.kc-fs-voice:active { transform: translateY(0); }
.kc-fs-voice.is-playing { border-color: var(--coral); color: var(--coral); }
.kc-fs-voice.is-loading { color: #8a7a5f; }
.kc-fs-voice span { font-size: 15px; line-height: 1; }
.kc-fs-voice b { font-weight: 800; }
/* A combination nobody has baked yet. It is a note and not an error state: the book itself
   is still read aloud, which is what the sentence says. */
.kc-fs-voice-note { margin: 9px 0 0; font-size: 12.5px; line-height: 1.5; color: #8a7a5f; text-wrap: pretty; }

/* On a phone the ONE card of numbered sections becomes one card PER section
   ("Book Steps Mobile.dc.html"): a 430px column cannot show a rule between two sections
   as a rule — it reads as a card that lost its border — so the card gives up its own
   chrome, the rules go, and each section wears the chrome instead. `.kc-ls-card` is a
   block container, so it has to become a flex column before a `gap` means anything.
   The accent section's `border-top` is replaced outright: as a standalone card it needs
   a border on all four sides, not a seam to the section above it.
   Step 1's three-column panel takes the same treatment for the same reason — it is
   already stacked by the 1080 block above, and a seam between stacked columns reads as a
   card that lost its border exactly as the rule did. The summary line leaves the bottom
   bar entirely: it is a 52px Back beside a full-width CTA down here and there is no room
   for a third child.
   Raised from the old 560 to 640 so it matches the rest of the phone layout — between
   561 and 640 the country list was still three columns wide inside a stacked step. */
@media (max-width: 640px) {
  .kc-ls-card { display: flex; flex-direction: column; gap: 12px; background: none; border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
  .kc-ls-rule { display: none; }
  .kc-ls-sec { padding: 14px; background: var(--paper); border: 1.5px solid #efe0c0; border-radius: 20px; box-shadow: 0 20px 34px -32px rgba(13,47,67,.8); }
  .kc-ls-sec-accent { border: 1.5px solid #f2e6cd; background: #fffaef; }
  .kc-ls-panel { display: flex; flex-direction: column; gap: 9px; background: none; border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
  .kc-ls-col { padding: 0; overflow: hidden; background: var(--paper); border: 1.5px solid #ecdcbb; border-top: 1.5px solid #ecdcbb; border-radius: 15px 19px 15px 19px; box-shadow: none; }
  /* The accent card is not tinted apart down here any more: it is REMOVED for a
     non-English book (see .kc-ls-col.off below), so the only accent card a phone ever
     shows is a live one, and a second paper colour among three cards reads as a state. */
  .kc-ls-col-accent { border: 1.5px solid #ecdcbb; background: var(--paper); }
  .kc-ls-search { padding: 0 15px; min-height: 46px; border: 2px solid #f1dca6; border-radius: 999px; background: var(--paper); }
  .kc-ls-clist { grid-template-columns: 1fr; padding-right: 0; max-height: 46vh; }
  .kc-ls-copt { min-height: 52px; border-radius: 14px; }
  .kc-ls-lang { min-height: 60px; border-width: 2px; border-radius: 16px; }
  .kc-ls-plate { width: 38px; height: 38px; border-radius: 12px; }
  .kc-ls-accent-chip { min-height: 46px; border-width: 2px; }
  .kc-fs-opts { grid-template-columns: 1fr; }

  /* Step 1 is the "Story Studio - Language Mobile" accordion: three cards stacked on a
     390px screen run well past the fold and push the bottom bar's CTA off it, so each
     column collapses to ONE row that states its own answer and only one opens at a time.
     Every part of that is markup renderKcStep already writes plus a class — see
     kcPaintLangAccordion. The lists are capped at 46vh for the same reason the row exists:
     a four-language country plus an open list must still leave Next on the screen. */
  /* The row stacks its LABEL ABOVE its answer ("Story Studio Step 1 Mobile"). Side by
     side, "Read-aloud accent" and "🇿🇦 South African" fought for one 390px line and
     overlapped — a label the value can run into is the bug, not the label's length. Both
     cells sit in column 2 of a two-row grid and the numeral and the chevron span both
     rows, so this is still the four children `kcLangRowMarkup` already writes and the
     desktop row (display: none up there) is untouched.
     `minmax(0, 1fr)` on that column, `min-width: 0` on the value and the ellipsis chain on
     its `b` are what keep a long country name off the chevron: a bare `1fr` blows the
     track out and the overlap comes back wearing the new layout. */
  .kc-ls-row { display: grid; grid-template-columns: 26px minmax(0, 1fr) 18px; grid-template-rows: auto auto; align-items: center; gap: 0 12px; width: 100%; min-height: 66px; padding: 11px 14px; border: 0; background: transparent; font-family: inherit; text-align: left; cursor: pointer; }
  /* A non-English book REMOVES the accent section rather than greying it. The desktop
     panel keeps the greyed column for a reason that is a three-column reason — a column
     that vanished on a language click jumps the other two sideways under the pointer that
     just clicked — and stacked cards have nothing beside them to jump. What is left down
     here was a dead row saying "Not needed" that could not be opened to say why; the
     language card's own "No read-aloud yet" pill is the part a family has to see, and it
     is on the card they are tapping. `kcPaintLangAccordion` already clamps an open state
     against `.off`, so nothing can be left open inside a hidden card. */
  .kc-ls-col.off { display: none; }
  .kc-ls-col.open { border-color: var(--blue); box-shadow: 0 16px 30px -24px rgba(13,47,67,.9); }
  .kc-ls-row .kc-ls-num { grid-area: 1 / 1 / 3 / 2; width: 26px; height: 26px; font-size: 11.5px; }
  .kc-ls-col.open .kc-ls-row .kc-ls-num { background: var(--coral); }
  .kc-ls-rowlab { grid-area: 1 / 2 / 2 / 3; font-size: 10px; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; color: #6c604a; }
  .kc-ls-rowval { grid-area: 2 / 2 / 3 / 3; justify-self: start; display: flex; align-items: center; gap: 8px; margin-top: 4px; min-width: 0; }
  .kc-ls-rowval span { flex: none; font-size: 17px; line-height: 1; }
  .kc-ls-rowval b { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: var(--display); font-size: 18px; font-weight: 500; letter-spacing: -.2px; color: var(--ink); }
  .kc-ls-chev { grid-area: 1 / 3 / 3 / 4; justify-self: center; font-size: 18px; line-height: 1; color: #8a7a5c; transition: transform .18s; }
  .kc-ls-col.open .kc-ls-chev { transform: rotate(90deg); color: var(--coral); }
  /* The body is `display: contents` above the breakpoint, so this is the ONE place the
     collapse lives — nothing in the renderer knows there is a phone. */
  .kc-ls-body { display: none; padding: 0 13px 13px; }
  .kc-ls-col.open .kc-ls-body { display: block; }
  /* The section head's numeral and legend are what the collapsed row already says; its
     aside is not (the live "N of M" count, "Spoken in X, plus English"), so it stays as
     the open section's note. Scoped to .kc-ls-col — the finish step shares .kc-ls-head. */
  .kc-ls-col .kc-ls-head { margin-bottom: 9px; }
  .kc-ls-col .kc-ls-head .kc-ls-num,
  .kc-ls-col .kc-ls-head .kc-ls-legend { display: none; }
  .kc-ls-col .kc-ls-aside { font-size: 12px; line-height: 1.4; color: #7d6f57; }
  .kc-ls-langs { max-height: 46vh; overflow-y: auto; }
  /* The mascot steps aside while a list is open — a bubble, a 46vh list and the bottom bar
     do not share a phone screen. .kc-ask is display:flex, so this is a rule and not a
     `hidden` attribute (the .kc-star[hidden] / .kc-nav[hidden] trap). */
  .kc-stage.kc-lang-open .kc-ask { display: none; }
}

/* kid studio — the child's card is not ticked when the step opens, so the tile glows
   until SOMEBODY has been added (see kcNobodyAdded). It is an invitation, not an error:
   gold like the rest of the studio, and it stops the moment the step is engaged with.
   Named apart from .selected on purpose — a glowing tile is not a chosen one. */
.kc-glow { border-color: var(--gold); animation: kcGlow 1.7s ease-in-out infinite; }
.kc-glow:hover { animation: none; }
@keyframes kcGlow {
  0%, 100% { box-shadow: 0 8px 20px rgba(13,47,67,.08), 0 0 0 0 rgba(233,169,43,.55); }
  50%      { box-shadow: 0 12px 26px rgba(233,169,43,.28), 0 0 0 9px rgba(233,169,43,0); }
}
@media (prefers-reduced-motion: reduce) {
  .kc-glow { animation: none; box-shadow: 0 8px 20px rgba(13,47,67,.08), 0 0 0 4px rgba(233,169,43,.35); }
}

/* kid studio — "In your story" before anybody is in it */
.kc-added-empty { color: var(--ink-soft); font-size: 13px; font-style: italic; }

/* kid studio — the child's chip doubles as the photo spot */
.kc-me-chip { border-color: var(--gold-light); background: #fffdf4; }
.kc-me-chip .kc-sub { color: var(--coral-dark); font-size: 10px; font-weight: 900; text-transform: uppercase; letter-spacing: .6px; }

/* kid studio — the studio's question is a heading, not a chat input */
.kc-bubble { background: transparent; box-shadow: none; border-radius: 0; padding: 4px 110px 4px 6px; }
.kc-bubble::before { display: none; }
.kc-bubble h2 { font-size: clamp(28px, 4vw, 44px); }
.kc-bubble p { font-size: clamp(13px, 1.5vw, 16px); }
/* kid studio — Adventure Picker: a category rail on the left, the places grid on the right */
/* The rail and the grid scroll SEPARATELY, and that is what fifteen worlds cost. The row
   used to be the scroll box itself (it is `.kc-scroll .kc-picker`, one element), which was
   fine while the rail was five buttons and shorter than the step body; at fifteen it is
   ~1290px in a ~200–520px body, so scrolling to the last world took the grid beside it off
   the screen entirely. `.kc-scroll` still gives this element its bounded height — the
   overflow is moved onto the two columns instead, and `stretch` is what gives them that
   height to bound against. Below 640px the picker stacks and the rail becomes a horizontal
   strip, so the media block hands the scroll back to the row. */
.kc-picker { display: flex; gap: clamp(14px, 2vw, 24px); align-items: stretch; overflow-y: hidden; }

/* kid studio — the picker row's playful scrollbars ("Place Picker Scroll.dc.html").
   Each column is a `position: relative` pane holding the scroller and, as its
   SIBLING, the track the thumb rides in — inside the scroller it would scroll away
   with the content. The native bar is hidden in both engines and never styled:
   `::-webkit-scrollbar*` does not exist in Firefox, `scrollbar-color` takes flat
   colours only, and declaring `scrollbar-color`/`scrollbar-width` in Blink makes it
   ignore every `::-webkit-scrollbar` rule — so nothing beyond `scrollbar-width: none`
   may be added here. Gold is content, coral is navigation, one tone per pane.
   The thumb's height and offset are written in PIXELS by kcMountScrollPanes; there is
   deliberately no `min-height` here, because the sheet cannot know when the 52px floor
   would be taller than the track itself. The rail pane carries the track's own 30px on
   top of the rail's width, so the buttons are the width they always were. */
.kc-sb-pane { position: relative; min-height: 0; }
.kc-picker > .kc-sb-pane-rail { flex: 0 0 calc(clamp(180px, 20vw, 224px) + 30px); }
.kc-picker > .kc-sb-pane-main { flex: 1; min-width: 0; }
.kc-sb-pane > [data-scroll-pane] { height: 100%; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; -ms-overflow-style: none; }
.kc-sb-pane > [data-scroll-pane]::-webkit-scrollbar { width: 0; height: 0; }
.kc-sb-pane-rail > [data-scroll-pane] { padding-right: 30px; }
.kc-sb-pane-main > [data-scroll-pane] { padding-right: 40px; }
.kc-sb-track { position: absolute; top: 0; right: 0; bottom: 0; width: 18px; border-radius: 999px; background: #fdeecb; border: 2px solid #f3dfaf; box-shadow: inset 0 2px 5px rgba(125,90,14,.12); }
/* A track inside the rounded grid panel insets on all three sides to clear the radius. */
.kc-sb-pane-main .kc-sb-track { top: 12px; right: 12px; bottom: 12px; }
/* The stripe is fixed at 135deg with a 14px period — it never scales with the thumb. */
/* It rests HIDDEN and the first paint (one frame later) is what shows it — otherwise a
   pane that does not overflow flashes a full-length thumb before it is measured. There is
   deliberately no transition on that: the rail is re-rendered on every world click, and a
   fade-in each time reads as a flicker. */
/* `touch-action: none` is what makes the thumb draggable by a FINGER: the wiring is on
   pointer events with a capture, and without this the browser claims the gesture as a pan
   and the thumb never moves. It costs a desktop nothing — a mouse drag was never a pan. */
.kc-sb-thumb { position: absolute; left: 1px; right: 1px; top: 0; height: 100%; opacity: 0; border-radius: 999px; border: 2px solid #fffdf7; box-shadow: 0 4px 10px -4px rgba(125,90,14,.6); cursor: grab; touch-action: none; }
/* The phone's own bar (`kcMountPhoneScrollbar`) — dressed in the block at the foot of the
   studio's phone section. It is hidden here rather than only styled there, because the node
   outlives the breakpoint: a phone widened into a tablet band keeps the track it was mounted
   with until the next render, and an undressed one is an absolutely-placed thumb with no
   positioned track to sit in. */
.kc-sb-vbar { display: none; }
.kc-sb-gold .kc-sb-thumb { background: repeating-linear-gradient(135deg, #e9a92b 0 7px, #f7d97d 7px 14px); }
.kc-sb-coral .kc-sb-thumb { background: repeating-linear-gradient(135deg, #eb755b 0 7px, #f7a58f 7px 14px); }

.kc-rail { display: flex; flex-direction: column; gap: 10px; }
.kc-rail-btn { display: flex; align-items: center; gap: 12px; min-height: 74px; padding: 12px 14px; border: 1px solid #e4dcc9; border-radius: 16px; background: #fffdf7; box-shadow: 0 4px 12px rgba(10,42,59,.05); cursor: pointer; font: inherit; text-align: left; transition: transform .16s, box-shadow .16s, border-color .16s, background .16s; }
.kc-rail-btn:hover { transform: translateX(3px); border-color: #e0b96a; box-shadow: 0 12px 24px -12px rgba(10,42,59,.22); }
.kc-rail-btn.active { border: 2px solid var(--cat); transform: translateX(4px); box-shadow: 0 14px 26px -12px color-mix(in srgb, var(--cat) 40%, transparent); }
.kc-rail-icon { flex: none; width: 42px; height: 42px; display: grid; place-items: center; border-radius: 13px; font-size: 22px; }
.kc-rail-text { flex: 1; min-width: 0; font-family: var(--display); font-weight: 500; font-size: clamp(14px, 1.5vw, 16px); line-height: 1.2; color: var(--ink); }
.kc-rail-text small { display: block; margin-top: 2px; font-family: var(--sans); font-weight: 600; font-size: 12px; color: #a08a5f; }
.kc-rail-arrow { flex: none; color: var(--cat); font-size: 22px; font-weight: 700; line-height: 1; opacity: 0; }
.kc-rail-btn.active .kc-rail-arrow { opacity: 1; }
/* A multi-select world (life skills, values) counts its picks in place of the arrow,
   so the tally is visible from whichever world is open. */
.kc-rail-count { flex: none; min-width: 26px; height: 26px; display: grid; place-items: center; padding: 0 7px; border-radius: 999px; background: var(--cat); color: #fff; font-size: 13px; font-weight: 900; line-height: 1; }
.kc-picker-main { min-width: 0; }
.kc-picker-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
.kc-picker-head h3 { margin: 0; font-family: var(--display); font-weight: 500; font-size: clamp(19px, 2.2vw, 23px); color: var(--ink); }
.kc-picker-head span { color: #8a7a5c; font-size: 14px; }

/* kid studio — the saved family row ("My Family.dc.html" panel 3, V60).

   A member on a membership saves everyone once at /family, and this row loads them into the
   story with one tap, above the generic cards. It is the SAME `.kc-grid` and `.kc-choice`
   the generic grid uses — a saved Granny is added as exactly the companion that grid's own
   "Granny" tile would have added — so there is no second tile shape to keep in step and
   nothing here restyles a control. What this block adds is the row's HEAD (which carries a
   link out to /family, the one thing the generic head has no room for) and the dashed rule
   that separates the two grids.

   Two things are load-bearing. `.kc-saved-manage` is pushed right with `margin-left: auto`
   and never with `justify-content`, which would move the heading whenever the sub-line is
   short — the rule the /shelf cover's own rows keep. And the rule is a BORDER on an empty
   box rather than an `<hr>`, because a bare `hr` in the pane inherits the UA's own margins
   and inset border, which reads as a card that lost its edge. */
.kc-saved-head { margin-bottom: 12px; }
.kc-saved-manage { margin-left: auto; color: var(--blue); font-size: 13.5px; font-weight: 700; text-decoration: none; }
.kc-saved-manage:hover { color: var(--coral-dark); text-decoration: underline; text-underline-offset: 3px; }
.kc-saved-rule { height: 0; margin: 22px 0 16px; border-top: 1px dashed #dcc99f; }

/* kid studio — the write-back ask ("My Family.dc.html" panel 2, right). It rides the OLD
   `.kc-modal` shell the Bible and fairy-tale pickers use, not `.kc-pmodal`: it is a
   question with two answers and no fields, so the numbered-body chassis would be furniture
   around one sentence. The two buttons are deliberately NOT twins — saving to the family is
   the act, using it once is the way out — the same weighting `.gen-keep` / `.gen-redo`
   carry on the cast pause. */
.kc-writeback { max-width: 420px; }
/* The people this book will invent a face for. A LIST and not a sentence: "no photo for X, Y
   and Z" needs list grammar in eleven languages, and the names are the whole point of the
   card — they have to be scannable, not buried in prose. */
/* ADMIN ONLY, inside the reader, over another family's book: who was photographed and who
   this book invented. Deliberately plain and un-designed — it is a support instrument, not a
   surface a customer ever sees, and dressing it up would be the one way it got mistaken for
   one. `[hidden]` has to be said: the panel declares `display: block`. */
.admin-cast { display: block; margin: 0 0 14px; padding: 12px 14px; border: 1px dashed #d9c9a6; border-radius: 12px; background: #fdf8ee; text-align: left; }
.admin-cast[hidden] { display: none; }
.admin-cast strong { display: block; font-size: 13px; letter-spacing: .04em; text-transform: uppercase; color: #7a6a4a; }
.admin-cast ul { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.admin-cast li { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; color: var(--ink); }
.admin-cast li em { font-style: normal; font-weight: 600; color: #a4906f; }
.admin-cast li.is-no em { color: var(--coral-dark); }
.admin-cast-none { margin: 8px 0 0; font-size: 13.5px; font-weight: 600; color: #a4906f; }
.kc-nophoto-list { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; text-align: left; }
.kc-nophoto-list li { display: flex; align-items: center; gap: 9px; padding: 8px 12px; border-radius: 11px; background: #fbf3e4; font-size: 14.5px; font-weight: 700; color: var(--ink); }
.kc-wb-actions { display: flex; gap: 9px; margin-top: 16px; }
.kc-wb-actions .kc-pm-cta { flex: 1; padding: 12px 18px; font-size: 14.5px; }
.kc-wb-quiet {
  flex: 1; padding: 12px 18px; border: 1.5px solid #e0d2b2; border-radius: 999px;
  background: #fffdf7; color: #6f7b80; font: inherit; font-size: 14.5px; font-weight: 800; cursor: pointer;
}
.kc-wb-quiet:hover { border-color: var(--blue); color: var(--ink); }
@media (max-width: 640px) {
  /* The studio's own breakpoint. Stacked, the act stays on top: a full-width pair at 390px
     is two 170px buttons and the longer label wraps. */
  .kc-wb-actions { flex-direction: column; }
}

/* kid studio — the two picker steps' search field ("Place Search Final.dc.html").

   One field, full width ABOVE the split, on the place step and the what-happens step.
   It is a sibling of `.kc-picker` and deliberately outside the grid pane: every
   keystroke rewrites that pane's contents, and a field inside it would lose the caret
   on the first character typed (the country list's rule, for the country list's reason).
   On a phone it sits under the question and scrolls away with it, which is what the
   stage-is-the-scroller rule already gives it — no rule of its own is needed for that. */
.kc-psearch { flex: none; position: relative; margin: 0 4px 14px; }
.kc-psearch-icon { position: absolute; left: 18px; top: 50%; transform: translateY(-50%); font-size: 18px; pointer-events: none; }
.kc-psearch-input { width: 100%; box-sizing: border-box; min-height: 58px; padding: 0 118px 0 50px; border: 2px solid #e6d3a6; border-radius: 16px 22px 16px 22px; background: var(--paper); font-family: var(--sans); font-size: 17px; font-weight: 600; color: var(--ink); box-shadow: 0 6px 16px rgba(10,42,59,.06); outline: 0; }
.kc-psearch-input:focus { border-color: var(--gold); box-shadow: 0 0 0 4px rgba(233,169,43,.18); }
/* The field carries a Clear button of its own, so the engine's built-in ✕ is a SECOND
   one — sitting in a different place on each browser and clearing without telling the
   handler that owns the query. */
.kc-psearch-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.kc-psearch-clear { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); display: inline-flex; align-items: center; gap: 8px; height: 34px; padding: 0 12px; border: 0; border-radius: 999px; background: #f0e7d2; color: #7a6a48; font-family: var(--sans); font-size: 13px; font-weight: 800; cursor: pointer; transition: background .16s, color .16s; }
.kc-psearch-clear:hover { background: var(--coral); color: #fff; }
/* It is a flex item, which beats the UA sheet's `[hidden]` — the `.kc-star[hidden]`
   and `.addon-card[hidden]` rule again. */
.kc-psearch-clear[hidden] { display: none; }
/* Search is a LENS over the worlds, not a different screen: the rail stays exactly
   where it is and dims. (A phone hides it instead — see the phone block.) */
.kc-picker > .kc-sb-pane-rail { transition: opacity .18s; }
.kc-picker.is-searching > .kc-sb-pane-rail { opacity: .45; }

/* Nothing matched. It points back at the rail and NOT at a "write your own place"
   step, which the design has and this studio does not. */
.kc-nomatch { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 46px 20px; border: 2px dashed #e0cf9f; border-radius: 22px; background: #fffdf5; text-align: center; }
.kc-nomatch-emoji { font-size: 42px; }
.kc-nomatch-title { margin: 0; font-family: var(--display); font-weight: 500; font-size: 22px; color: var(--ink); }
.kc-nomatch-sub { margin: 0; max-width: 420px; font-size: 14px; color: #8a7a5c; }
.kc-nomatch-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 4px; }
.kc-psug { min-height: 38px; padding: 0 14px; border: 1px solid #e0cda4; border-radius: 999px; background: #fffdf7; font: inherit; font-size: 14px; font-weight: 700; color: var(--ink); cursor: pointer; transition: border-color .16s, transform .16s; }
.kc-psug:hover { border-color: var(--gold); transform: translateY(-1px); }

/* kid studio — "In your story" strip: who's been added, above the who-joins grid */
.kc-added { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 10px; margin-bottom: 16px; padding: 12px 14px; border: 1px dashed #e0cf9f; border-radius: 16px; background: #fffdf5; }
.kc-added-title { font-family: var(--display); font-weight: 500; font-size: 14px; color: #8a7a5c; letter-spacing: .3px; }
.kc-added-chips { display: flex; flex-wrap: wrap; gap: 8px; flex: 1; min-width: 0; }
.kc-added-chip { display: inline-flex; align-items: center; gap: 7px; padding: 5px 8px 5px 9px; border: 2px solid var(--gold-light); border-radius: 999px; background: #fff; box-shadow: 0 3px 8px rgba(13,47,67,.08); font-weight: 800; font-size: 14px; color: var(--ink); }
.kc-added-chip[data-star] { border-color: var(--coral); background: #fff7f3; }
.kc-added-edit { cursor: pointer; transition: border-color .15s, transform .15s; }
.kc-added-edit:hover { border-color: var(--gold); transform: translateY(-1px); }
.kc-added-emoji { font-size: 20px; line-height: 1; }
.kc-added-photo { width: 24px; height: 24px; box-shadow: 0 2px 5px rgba(13,47,67,.22); }
.kc-added-label { white-space: nowrap; }
.kc-added-x { display: grid; place-items: center; width: 20px; height: 20px; border: none; border-radius: 50%; background: #f0e7d2; color: #7a6a48; font-size: 15px; line-height: 1; cursor: pointer; transition: background .15s, color .15s, transform .15s; }
.kc-added-x:hover { background: var(--coral); color: #fff; transform: scale(1.1); }

/* place cards — design look: emoji in a category-tinted tile, tight card, coral selection */
.kc-place-grid { grid-template-columns: repeat(auto-fill, minmax(clamp(130px, 15vw, 156px), 1fr)); gap: 16px; }
.kc-place { flex-direction: column; justify-content: flex-start; gap: 0; padding: 22px 14px 18px; border: 2px solid #efe6d3; border-radius: 20px; background: #fffdf7; box-shadow: 0 8px 20px rgba(10,42,59,.06); }
.kc-place:hover { transform: translateY(-5px); box-shadow: 0 16px 30px rgba(13,47,67,.14); }
.kc-place-icon { width: clamp(56px, 6vw, 64px); height: clamp(56px, 6vw, 64px); display: grid; place-items: center; border-radius: 18px; font-size: clamp(30px, 3.6vw, 36px); line-height: 1; filter: drop-shadow(0 4px 6px rgba(0,0,0,.14)); transition: transform .2s; }
.kc-place:hover .kc-place-icon { transform: scale(1.06) rotate(3deg); }
.kc-place .kc-clabel { margin-top: 12px; font-weight: 700; font-size: 15px; }
/* "18 to choose from" / "Tap to change" under a tile that stands for a list. */
.kc-place .kc-sub { margin-top: 4px; font-size: 11px; font-weight: 800; color: var(--ink-soft); }
.kc-place.selected { border-color: var(--coral); background: #fffdf7; box-shadow: 0 16px 34px -14px rgba(215,91,69,.45); }
.kc-place.selected .kc-sub { color: var(--coral-dark); text-transform: uppercase; letter-spacing: .5px; font-size: 10px; font-weight: 900; }
.kc-place.selected .kc-place-icon { animation: kcPop .4s ease; }
.kc-place .kc-check { top: -9px; right: -9px; width: 30px; height: 30px; font-size: 15px; box-shadow: 0 5px 12px rgba(215,91,69,.5); }

@media (max-width: 640px) {
  /* Stacked, the two columns are one column: the row scrolls again, as it did before the
     rail grew, or each half would carry a scrollbar of its own inside a phone-height body. */
  /* `visible`, not `auto`: on a phone the STAGE is the scroll box (see the phone block
     below), and a second scroller here would trap the rail and the grid in a box inside
     it — the "In your story" tray would scroll away from the tiles it describes. */
  .kc-picker { flex-direction: column; overflow: visible; }
  /* The GRID pane's overflow goes back to the row, so its own vertical track is a dead
     decorative spine here and goes with the padding that cleared it — what says how far
     down the tiles run on a phone is the STAGE's bar (see the phone block below), because
     the stage is what actually scrolls. */
  .kc-picker > .kc-sb-pane-rail { flex: none; width: 100%; }
  .kc-sb-pane > [data-scroll-pane] { height: auto; padding-right: 0; overflow: visible; }
  /* The desktop rule's `overflow-x: hidden` outranks `.kc-rail`'s own `overflow-x: auto`,
     and a hidden axis cannot be panned — so the horizontal rail is handed its scroll
     back here by name, or the strip is unreachable past the fourth world on a phone. */
  .kc-sb-pane-rail > [data-scroll-pane] { overflow-x: auto; padding-bottom: 20px; }
  .kc-sb-track { display: none; }

  /* The rail's bar TURNS — it is the same track, the same thumb and the same wiring, laid
     along the bottom edge of the strip instead of down its side. `kcMountScrollPanes` asks
     for the axis on every paint (`kcIsPhone()`), so nothing re-renders on a rotation; what
     the sheet has to do is re-state the four properties the vertical rule set, because a
     bar that keeps `top: 0; bottom: 0; width: 18px` from the desktop rule is a vertical
     spine drawn down the left of a horizontal strip. The thumb's own `width`/`left` are
     written inline by the paint, and the axis it is NOT on is cleared there, or a stale
     `height`/`top` outranks these rules the moment a phone is turned into a tablet band. */
  .kc-sb-pane-rail > .kc-sb-track { display: block; top: auto; right: 0; bottom: 0; left: 0; width: auto; height: 14px; }
  .kc-sb-pane-rail .kc-sb-thumb { top: 1px; bottom: 1px; left: 0; right: auto; height: auto; width: 0; }
  .kc-rail { flex: none; width: 100%; flex-direction: row; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }
  .kc-rail::-webkit-scrollbar { display: none; }
  /* The design keeps the world's own count on the strip ("10 to pick", "24 places") —
     it is the only thing on a phone that says how much is behind a world before it is
     opened — so `small` comes BACK. The arrow does not: it is the desktop rail's
     "you are here" marker and the active button's border already says it. */
  .kc-rail-btn { flex: 0 0 auto; min-height: 56px; padding: 9px 14px 9px 10px; gap: 9px; border-radius: 16px; scroll-snap-align: start; }
  .kc-rail-btn:hover, .kc-rail-btn.active { transform: none; }
  .kc-rail-icon { width: 36px; height: 36px; border-radius: 12px; font-size: 18px; }
  .kc-rail-text { font-family: var(--sans); font-size: 13px; font-weight: 800; line-height: 1.15; white-space: nowrap; }
  .kc-rail-text small { display: block; margin-top: 2px; font-size: 11px; font-weight: 700; white-space: nowrap; }
  .kc-rail-arrow { display: none; }
}

/* ── kid studio on a phone ("Book Steps Mobile.dc.html") ───────────────────────────────
   The desktop studio is untouched: every rule here is inside the 640px block, and the
   markup it restyles is the SAME markup — one renderer, one set of wiring. What the
   design changes below the header (which is edited into the .kc-top block above) is the
   scroll box and the tile size.

   Six things are load-bearing. (1) THE STAGE IS THE SCROLLER. On desktop the question
   (`.kc-ask`) is fixed and only `.kc-scroll` / the two picker panes move; on a 430×900
   phone a fixed 90px question over a 62px header and a 76px bar leaves the tiles a
   letterbox, and the design scrolls the title away. So `.kc-stage` takes the overflow and
   everything below it goes `visible`. (2) IT IS `overflow-x: clip`, NEVER `auto`:
   `overflow-y: auto` computes the other axis to `auto` as well, and `.kc-check` sits at
   `right: -9px` on every selected tile — the studio would pan sideways by nine pixels.
   (3) THE GUTTER MOVES ONTO THE STAGE for that same reason: clipping happens at the
   padding box, so the 16px gutter is split 6px on `.kc-body` and 10px on `.kc-stage`, and
   the tick has room to hang. The design's edge-bleeding world rail (`margin: 0 -16px`) is
   deliberately NOT ported — it would be clipped by the same rule, and a rail that starts
   inside the gutter is the smaller loss. (4) The grid is TWO fixed columns, not the
   desktop's `auto-fill` — at 430px `minmax(130px, 1fr)` also lands on two, but a 320px
   phone would drop to one and a 620px one to four. (5) `.kc-nav[hidden]` has to be said:
   `.kc-nav` sets `display: flex`, which beats the UA sheet's `[hidden]`, and the bar now
   carries a border and a gradient — the handoff and the celebration both hide it. (6) The
   bar pads to `env(safe-area-inset-bottom)` or the home indicator sits on the CTA.

   Two departures from the design. The Next button keeps "Next →" / "Make my book! ✨"
   rather than its disabled "Pick one to continue": that string is one shared JS label and
   the desktop button would move with it. And the finish step's two option lists stay the
   stacked cards the "Finish Step" port drew rather than becoming icon/label/tick rows —
   the tick lives inside `.kc-fs-top`, so the row shape is a markup change, not a restyle. */
@media (max-width: 640px) {
  .kc { background: linear-gradient(#fff6de, #fdf1d6 42%, #f8e3ba); }
  .kc-body { position: relative; padding: 0 6px 6px; }
  .kc-stage { padding: 0 23px 0 10px; overflow-y: auto; overflow-x: clip; -webkit-overflow-scrolling: touch; }

  /* ── the phone's own drawn scrollbar ──────────────────────────────────────────────
     Every scroller in the phone studio wears one: the STAGE (the steps), the handoff,
     the celebration and the person modal's body. The track is built by
     `kcMountPhoneScrollbar` rather than written into four templates, because above this
     breakpoint none of those four scrolls at all — the overflow is on `.kc-scroll` and
     the two picker panes — so a track in the markup would be a dead spine on every
     desktop render.

     Three things are load-bearing. (1) It is a SIBLING of the scroller, never a child:
     inside, it scrolls away with the content, which is the rule the picker panes already
     keep — so the PARENT is what carries `position: relative`, and that is why `.kc-body`
     and `#kc-main` take one here. (2) The stage's right gutter is the bar's own arithmetic
     and moves with its width — `2px` of offset + the bar + `.kc-check`'s `right: -9px`,
     plus a pixel of slack, so 23px at an 11px bar — and it has to be the stage's own
     padding rather than the bar
     being pulled outward: `.kc-stage` is `overflow-x: clip` for that tick, so the tick
     needs the room too. Widen the bar and this number widens with it, or the tick on a
     selected tile is clipped by the bar. (3) The track is `pointer-events` live but the
     whole apparatus is 11px wide (raised from 8px on 2026-08-07, at the owner's request —
     a thumb a finger can find) — it is a POSITION READOUT first and a control second,
     which is what the 52px thumb floor and the press-to-jump both exist for. */
  .kc-sb-vbar { display: block; position: absolute; top: 0; right: 2px; bottom: 0; width: 11px; border-radius: 999px; background: #fdeecb; border: 1.5px solid #f3dfaf; box-shadow: inset 0 2px 5px rgba(125,90,14,.12); z-index: 3; }
  .kc-sb-vbar .kc-sb-thumb { left: 0; right: 0; border-width: 1.5px; }
  /* The modal is a bottom sheet whose body is a BAND between the head and the foot, so its
     track is pinned to that body's own offset box on every paint (`fit`) — the head's
     rounded corner is what it must not run over. */
  .kc-pmodal > .kc-sb-vbar { right: 4px; }
  .kc-scroll { flex: none; min-height: 0; overflow: visible; padding: 0 0 8px; }

  /* The studio's question: a 52px stamp beside a Georgia line, the design's own head. */
  .kc-ask { align-items: center; gap: 12px; margin-bottom: 0; padding: 16px 0 12px; }
  /* Phone: the 52px stamp is smaller than the 44px mark's own scale, so the
     scale comes down with it — the disc stays pale for the same reason the
     desktop one does (a gold spine on a gold disc disappears). */
  .kc-mascot { width: 52px; height: 52px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #fffdf6, #ffeec2); box-shadow: 0 14px 22px -16px rgba(125,90,14,.9); }
  .kc-mascot .brand-mark { transform: scale(1.05); }
  .kc-bubble { padding: 0; }
  .kc-bubble h2 { font-size: 26px; line-height: 1.1; letter-spacing: -.4px; }
  .kc-bubble p { margin-top: 4px; font-size: 13px; line-height: 1.45; }

  .kc-rail { gap: 8px; padding-bottom: 4px; scroll-snap-type: x proximity; }
  .kc-picker-head { gap: 8px; margin-bottom: 10px; }
  .kc-picker-head h3 { font-size: 19px; }
  .kc-picker-head span { font-size: 12px; }
  .kc-added { margin-bottom: 12px; padding: 11px 14px; }
  .kc-bible-search { margin-bottom: 12px; }

  /* The search field sits under the question and scrolls away with it (the stage is
     the scroller down here), and the Clear pill becomes the design's round ✕ — the
     word costs 60px of a 390px field. `font-size: 0` on the button hides the word and
     the span puts the glyph back; the accessible name is on the button's aria-label. */
  .kc-psearch { margin: 0 0 12px; }
  .kc-psearch-icon { left: 15px; font-size: 16px; }
  .kc-psearch-input { min-height: 50px; padding: 0 46px 0 44px; font-size: 16px; box-shadow: none; }
  .kc-psearch-clear { right: 12px; width: 30px; height: 30px; padding: 0; gap: 0; justify-content: center; border-radius: 50%; font-size: 0; }
  .kc-psearch-clear span { font-size: 16px; }
  /* A sixteen-world strip above the results owns half a phone screen and cannot be
     dimmed into the background the way a desktop column can, so the rail goes while a
     query is live and the results own the screen. Its drawn bar goes with the pane. */
  .kc-picker.is-searching > .kc-sb-pane-rail { display: none; }
  .kc-nomatch { gap: 12px; padding: 30px 16px; border-radius: 20px; }
  .kc-nomatch-emoji { font-size: 34px; }
  .kc-nomatch-title { font-size: 18px; }
  .kc-nomatch-sub { font-size: 13px; }

  .kc-grid, .kc-place-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .kc-choice, .kc-place { min-height: 126px; padding: 14px 9px; border-radius: 18px; }
  .kc-choice .kc-emoji { font-size: 42px; }
  .kc-choice .kc-clabel, .kc-place .kc-clabel { font-size: 13px; font-weight: 800; line-height: 1.25; }
  .kc-place .kc-clabel { margin-top: 8px; }
  .kc-place-icon { width: 52px; height: 52px; border-radius: 16px; font-size: 27px; }
  .kc-place .kc-sub { font-size: 10.5px; }

  .kc-nav { gap: 10px; padding: 12px 16px; padding-bottom: max(16px, env(safe-area-inset-bottom)); background: linear-gradient(rgba(251,236,201,0), #fbecc9 34%); border-top: 1.5px solid rgba(238,221,184,.8); }
  .kc-nav[hidden] { display: none; }
  .kc-back { width: 52px; height: 52px; min-height: 0; flex: none; padding: 0; display: grid; place-items: center; border: 1.5px solid #e0cda4; border-radius: 16px; background: var(--paper); color: var(--blue); font-size: 19px; overflow: hidden; }
  /* The design keeps the dead arrow on screen at .4 rather than removing it: a bar that
     loses its left button on step one re-flows the CTA under the thumb that just used it. */
  .kc-back:disabled { opacity: .4; }
  .kc-back-word { display: none; }
  .kc-back::before { content: "←"; }
  .kc-next { flex: 1; min-width: 0; min-height: 52px; justify-content: center; gap: 9px; padding: 0 18px; border-radius: 999px; font-size: 16.5px; box-shadow: 0 16px 28px -16px rgba(47,82,68,.95); }
  .kc-next .arrow { font-size: 19px; }
}

/* "Still being painted": a PAID page whose deferred illustration has not landed yet. It
   covers the art half whole — the stock hero jpg is underneath, and a family who paid for
   this page must not be shown it as if it were their book. `[hidden]` is spelled out
   because the block below sets `display: grid`, which the attribute cannot beat on its own
   (the same reason `.addon-card[hidden]` and `.kc-star[hidden]` exist). */
.story-art-painting { position: absolute; inset: 0; z-index: 3; display: grid; align-content: center; justify-items: center; gap: 8px; padding: 24px; text-align: center; background: linear-gradient(180deg, rgba(14, 61, 86, .92), rgba(14, 61, 86, .97)); color: #fdf3df; }
.story-art-painting[hidden] { display: none; }
.story-art-painting-mark { font-size: 40px; line-height: 1; animation: story-paint-bob 2.2s ease-in-out infinite alternate; }
.story-art-painting strong { font-family: var(--display); font-size: clamp(18px, 2.4vw, 24px); font-weight: 700; }
.story-art-painting small { max-width: 30ch; font-size: 13px; font-weight: 600; color: rgba(253, 243, 223, .8); line-height: 1.5; }
/* story-prefixed: this sheet already carries hero-/faq-/ft-/sub-/cs-/kc- bob variants and a
   duplicate @keyframes name is last-wins with no warning. */
@keyframes story-paint-bob { from { transform: translateY(-4px) rotate(-6deg); } to { transform: translateY(4px) rotate(6deg); } }

/* ============================================================================
   BOOK READER — Claude Design reskin ("Book Reader.dc.html")
   Restyles the existing #storyDialog reader onto the warm, spread-and-spine
   look from Claude Design. Purely visual: every id/hook app.js drives
   (storyArtPage, storyPageText, storyProgressDots, previousStoryPage …) is
   kept. Placed last so equal-specificity rules win over the originals above.
   ========================================================================= */

@keyframes reader-twinkle { 0%,100% { opacity:.3; transform:scale(.8) } 50% { opacity:1; transform:scale(1.15) } }
@keyframes reader-floaty  { 0%,100% { transform:translateY(0) }        50% { transform:translateY(-7px) } }
@keyframes reader-eq      { 0%,100% { height:7px }                     50% { height:16px } }

/* --- shell: warm storybook glow + floating twinkles --------------------- */
.story-dialog { position: relative; border-radius: 22px; background: #eae1d1; }
/* The reader is a fixed-height flex column: header, spread, footer. The spread
   takes whatever is left (`flex: 1`), so the pages MEASURE the chrome instead of
   subtracting a guess at it — `.story-dialog-header` is `flex-wrap: wrap` with
   four children and has no height a rule here could know. That is the same
   mechanism the fullscreen block above already uses; this extends it to the
   windowed reader rather than inventing a second one.
   `height`, not `max-height`: `flex: 1` needs a definite box to divide, and a
   definite box is also what keeps every spread exactly the same size (the
   invariant the old fixed `height: 600px` on the pages existed for).
   `[open]`, because a bare `display: flex` would beat the UA's `display: none`
   and paint a closed dialog — the same reason `.sub-dialog[open]` is spelled
   that way.
   The WIDTH tracks the height for the same reason: the spread is 8:5 and its
   height is whatever the band leaves, so a dialog sized on width alone is all
   gutter on a short screen (1328px of card around an 818px book at 1366×768).
   `153vh` is 1.6 × the 96vh height, less the chrome the band does not get;
   `max(960px, …)` is the floor the header needs before it wraps. */
.story-dialog[open] { display: flex; flex-direction: column; overflow: hidden; width: min(1520px, calc(100% - 28px), max(960px, calc(153vh - 190px))); height: min(96vh, 1040px); max-height: min(96vh, 1040px); }
.story-dialog .story-dialog-header, .story-dialog .story-reader-footer { flex: none; }
.reader-twinkles { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
.reader-twinkles span { position: absolute; color: #e9a92b; animation: reader-twinkle 3.6s ease-in-out infinite; }
.reader-twinkles span:nth-child(even) { color: #eb9d8a; }
.story-dialog > :not(.reader-twinkles) { position: relative; z-index: 1; }

/* --- top bar: airy, sits over the glow ---------------------------------- */
.story-dialog-header { background: transparent; border-bottom: 1px solid rgba(160,125,60,.18); padding: 22px 26px 18px 34px; }
.story-dialog-header .kicker { color: #a07d3c; }
.story-dialog-header h2 { font-family: var(--display); font-size: 27px; font-weight: 700; letter-spacing: -.5px; color: var(--ink); }
.story-dialog-header p { color: #a07d3c; font-size: 12.5px; font-weight: 600; }
.story-dialog .dialog-close { top: 20px; right: 22px; left: auto; width: 40px; height: 40px; border-radius: 999px; background: #fffdf7; border: 1.5px solid #e8dcc2; color: #395363; box-shadow: 0 6px 16px rgba(10,42,59,.08); }
.fullscreen-toggle-btn { border-radius: 999px; border: 1.5px solid #e8dcc2; background: #fffdf7; color: #395363; font-weight: 600; box-shadow: 0 6px 16px rgba(10,42,59,.08); }
.preview-before-payment { border-radius: 999px; }

/* Controls + the "Unlock" CTA now live INLINE in the header (the old
   full-width audio bar and the approval bar under the book are gone). */
.story-dialog-header { flex-wrap: wrap; gap: 12px 18px; }
/* It carries `tabindex="-1" autofocus` (see index.html): it is the reader's focus anchor,
   not a control, so it draws no ring of its own — `:focus`, not `:focus-visible`, because
   the anchor is focused programmatically and matches both. Tab from here still lands on
   the Read button, which keeps its own ring. */
.story-dialog-header .reader-title { flex: 0 0 auto; }
.story-dialog-header .reader-title:focus { outline: none; }
.story-dialog-header .audio-player { flex: 1 1 260px; min-width: 0; background: transparent; border: none; padding: 0; flex-wrap: wrap; }
.story-dialog-header .audio-player label { margin-left: 0; }
.story-dialog-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; margin-left: auto; }
.story-dialog-actions .button { min-height: 40px; padding: 0 16px; gap: 10px; font-size: 12px; }

/* ==========================================================================
   The unlock reads as "this is the bit you buy" ("Preview Header - Unlock
   CTA.html", option A — the file's own recommendation).

   What it replaces: a coral "Unlock full story →" pill whose label sounded
   like a view toggle, with no price anywhere on the screen, beside a grey
   line in `#audioStatus` that explained the button by quoting the button.
   Now the middle of the header carries a PEEK METER — how much book is still
   shut, drawn — and the pill carries the price under a verb, with a key that
   turns in its knob on hover.

   Five things are load-bearing.

   (1) The peek meter counts in the READER's own unit, and it is the same
       arithmetic `#storyReaderCounter` does (`items.length - 1`, the cover
       excluded): pages, where a page is a spread. `#storyPreviewMeta`'s
       locked wording therefore no longer says "all 24 pages" — 3 free, 9
       hiding and 24 total, adjacent in one header with nothing on screen
       reconciling spreads to printed pages, is exactly the drift the
       narration pill's naming rule exists to stop. "24 pages" survives on
       the storefront and on `#previewFormat`, where nothing counts against it.
   (2) The CTA names NO page count for the same reason — "Unlock the whole
       book", not the design's "Unlock all 14 pages". A reader-unit twelve on
       the buy button would undersell a book sold as 24 pages.
   (3) `[hidden]` has to be beaten in the sheet: `.reader-peek` is
       `display: flex`, so `hidden` alone does nothing to it (the reason
       `.addon-card[hidden]` and `.kc-star[hidden]` exist).
   (4) `.audio-player` gives up its `flex: 1 1 260px` while the meter is up
       (`.has-peek`, set by `updateReaderLockUI`). On a locked book that
       element is empty — every button is hidden and the status line is
       hidden below — but 260px of flex basis still pushed the meter onto a
       second row, and the header is `flex: none` in a fixed-height column,
       so a taller header comes straight off the spread.
   (5) `#audioStatus` keeps being WRITTEN (`NARRATION_LOCKED_STATUS` is
       compared by identity in three places, and the phone sheet shows that
       line as the only thing explaining a missing read button) — it is
       hidden here, not silenced.
   ========================================================================== */
/* `flex: none`: the meter is a fixed object and the TITLE is what gives when the
   header runs out of room (it ellipsises — see `.has-peek` below). Allowed to
   shrink, it kept its nowrap line and its 150px of ticks and simply overflowed its
   own box, so the count ran under the pill instead of the name running short. */
.reader-peek { display: flex; align-items: center; gap: 11px; flex: none; padding: 7px 15px 7px 9px; border: 1.5px solid #e8dcc2; border-radius: 999px; background: #fffdf7; box-shadow: 0 5px 14px rgba(10,42,59,.06); }
.reader-peek[hidden] { display: none; }
.reader-peek .peek-icon { flex: none; display: grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: #e3f0e7; color: var(--green); animation: peek-bob 2.9s ease-in-out infinite; }
@keyframes peek-bob { 0%, 100% { transform: translateY(0) rotate(-4deg); } 50% { transform: translateY(-3px) rotate(4deg); } }
.reader-peek .peek-body { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.reader-peek .peek-line { color: #3f6b53; font-size: 12px; font-weight: 700; white-space: nowrap; }
.reader-peek .peek-line b { font-weight: 900; }
/* One tick per page. `flex: 1 1 0` so twelve of them and twenty-four of them
   both fill the same 150px — the meter is a proportion, not a ruler. */
.reader-peek .peek-ticks { display: flex; align-items: flex-end; gap: 2px; width: 150px; height: 17px; }
.reader-peek .peek-ticks i { flex: 1 1 0; height: 17px; border: 1.5px solid #e2d3b4; border-radius: 1px 3px 3px 1px; background: #fdf8ec; }
.reader-peek .peek-ticks i.read { background: var(--green); border-color: var(--green); }
.reader-peek .peek-ticks i.shut { border-style: dashed; }
.story-dialog.has-peek .story-dialog-header .audio-player { flex: 0 0 auto; }
.story-dialog.has-peek .story-dialog-header .audio-player small { display: none; }
/* The title gives, the way it already does on a phone. The header is three items
   wide while the meter is up and the dialog's own floor is 960px, so a long book
   name wrapped the pill onto a second row — 61px of header on a spread that is
   whatever the header leaves. Only `.has-peek`: a paid book has the width back and
   its title may take two lines. The name is on the cover a hand below either way. */
/* `nowrap` is what makes the shrink happen at all: a wrapping flex line moves the
   pill to a second row rather than narrowing the title, so the ellipsis above never
   fired. It is the design's own `.shell` rule, and it is safe here for the same
   reason `.audio-player` gives up its basis — while the meter is up that element is
   empty, so there is nothing left in this header that NEEDS a second line. */
.story-dialog.has-peek .story-dialog-header { flex-wrap: nowrap; }
.story-dialog.has-peek .story-dialog-header .reader-title { flex: 0 1 auto; min-width: 0; }
.story-dialog.has-peek .story-dialog-header h2 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The pill. Green with a gold key rather than the coral it was: coral is the
   buy colour on the SHELF's own unlock button one click upstream, and this is
   the design's choice for the reader — swapping it back is `--coral` here and
   in the two shadows. */
.story-dialog-actions #approveStory.button-unlock {
  position: relative; display: inline-flex; align-items: center; gap: 12px;
  min-height: 0; padding: 8px 20px 8px 10px;
  border: 0; border-radius: 999px;
  background: var(--green); color: #fff;
  font-family: var(--sans); text-align: left;
  box-shadow: 0 12px 24px -12px rgba(69,111,92,.9);
  animation: cta-a-bob 2.8s ease-in-out infinite;
  transition: transform .18s, box-shadow .18s, background .18s;
}
/* The halo is a ::after and not a box-shadow so it can scale past the pill
   without moving it; `pointer-events: none` because it covers the button. */
.story-dialog-actions #approveStory.button-unlock::after { content: ""; position: absolute; inset: 0; border-radius: 999px; border: 2px solid var(--green); animation: cta-a-pulse 2.8s ease-out infinite; pointer-events: none; }
.story-dialog-actions #approveStory.button-unlock:hover { transform: translateY(-2px); background: #3f7d5a; box-shadow: 0 16px 30px -12px rgba(63,125,90,.95); animation-play-state: paused; }
.story-dialog-actions #approveStory.button-unlock:hover::after { animation-play-state: paused; opacity: 0; }
.story-dialog-actions #approveStory.button-unlock:active { transform: translateY(0); }
@keyframes cta-a-bob { 0%, 100% { transform: translateY(0) rotate(0); } 50% { transform: translateY(-3px) rotate(-.5deg); } }
@keyframes cta-a-pulse { 0% { transform: scale(1); opacity: .55; } 70%, 100% { transform: scale(1.13); opacity: 0; } }
#approveStory .unlock-knob { flex: none; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; background: #fdf0cf; color: #a86a1c; transition: transform .32s cubic-bezier(.5,-.4,.4,1.5); }
.story-dialog-actions #approveStory.button-unlock:hover .unlock-knob { transform: rotate(-32deg) scale(1.06); }
#approveStory .unlock-copy { display: flex; flex-direction: column; gap: 2px; }
#approveStory .unlock-copy b { font-size: 14.5px; font-weight: 800; letter-spacing: -.1px; white-space: nowrap; }
#approveStory .unlock-copy small { color: #d6ebdd; font-size: 12px; font-weight: 600; white-space: nowrap; }
/* The one-line wording is the LANDSCAPE PHONE's alone (the sheet's own block turns it
   on): everywhere else the pill has the room for both of its lines, and the two shown
   together would say the price twice. */
#approveStory .unlock-short { display: none; }
/* The arrow is the PHONE's padlock shackle (see the sheet's item 6) and nothing
   here: this pill draws a key instead. It stays in the markup on both states so
   the phone block has one selector for both. */
.story-dialog-actions #approveStory.button-unlock .unlock-shackle { display: none; }
/* A `display` the attribute cannot beat has to be beaten in the sheet — the same
   reason `.addon-card[hidden]` and `.kc-star[hidden]` exist. `.button` is
   `display: inline-flex`, so `updateReaderLockUI`'s `approve.hidden = …` set an
   attribute the UA sheet could no longer act on: the padlock stayed live on a
   read-only public-library book, and its click saved over a book this member does
   not own. Stated once here, for every width — the phone, landscape and 380px
   blocks size this button but set no `display` of their own. */
.story-dialog-actions #approveStory[hidden] { display: none; }
/* Three animations that all say "press me" at once, on a screen a child is
   being read to on. The pill stays green and the meter stays drawn. */
@media (prefers-reduced-motion: reduce) {
  .reader-peek .peek-icon,
  .reader-ra-chip.is-end .ra-ico,
  .story-nav-button.is-unlock-key::after,
  .story-dialog.is-locked .story-progress-dots::after,
  .story-dialog-actions #approveStory.button-unlock,
  .story-dialog-actions #approveStory.button-unlock::after { animation: none; }
  .story-dialog-actions #approveStory.button-unlock::after,
  .story-nav-button.is-unlock-key::after { opacity: 0; }
}

/* ══ The preview's chrome on a PHONE ("Preview Header - Unlock CTA.html", the
      portrait and landscape frames) ═══════════════════════════════════════════
   Three pieces, and all three live in the reader FOOTER: a read-aloud chip where
   a wrapping grey sentence used to be, a seamed page bar with a star knob where
   the segments were, and — on the last page of the free preview — a forward arrow
   that has become the key. The desktop says the same things in its header (the
   peek meter and the green pill), so the chip and the bar are painted on a phone
   only; the KEY is stated here for every width, because its behaviour is the same
   at every width (see `renderStoryPage`) and an arrow that looks dead while being
   clickable is worse than either look on its own.
   ========================================================================== */
.reader-ra-chip { display: none; align-items: center; gap: 9px; min-width: 0; padding: 7px 12px 7px 8px; border: 1.5px solid #cfe2d5; border-radius: 999px; background: #eef7f0; }
/* `[hidden]` has to be beaten in the sheet — the `.addon-card[hidden]` trap again.
   Two classes beat the phone blocks' one whatever the source order, so this is
   stated once, here, and never repeated down there. */
.reader-ra-chip[hidden] { display: none; }
.reader-ra-chip .ra-ico { flex: none; display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: #dcebe1; color: var(--green); }
.reader-ra-chip .ra-ico-card { display: none; }
.reader-ra-chip .ra-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.reader-ra-chip .ra-txt b { color: #2f5b45; font-size: 12px; font-weight: 800; line-height: 1.2; }
/* 12px is the floor on BOTH lines: the design's 11px first pass failed contrast. */
.reader-ra-chip .ra-txt small { color: var(--green); font-size: 12px; font-weight: 600; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The end of the free preview: the card icon (the payment signal), a solid green disc,
   and the sub-line WRAPS rather than ellipsising — it carries the instruction, and an
   instruction is the one string in this footer that must never be truncated. */
.reader-ra-chip.is-end { border-color: #c3ddcd; background: #e7f3eb; }
.reader-ra-chip.is-end .ra-ico { background: var(--green); color: #fff; animation: peek-bob 2.9s ease-in-out infinite; }
.reader-ra-chip.is-end .ra-ico-book { display: none; }
.reader-ra-chip.is-end .ra-ico-card { display: block; }
.reader-ra-chip.is-end .ra-txt small { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.3; }

/* The forward arrow as the key. `font-size: 0` hides the `›` and the label from the
   EYE only — they stay in the accessibility tree, and `renderStoryPage` rewrites the
   aria-label to say what the button now does — which also covers `goToCheckout` replacing
   the whole button with the words "Taking you to checkout…" for the length of the save. */
.story-nav-button .nav-key { display: none; }
.story-nav-button.is-unlock-key {
  position: relative;
  border-color: transparent; background: var(--green); color: #fff; font-size: 0;
  box-shadow: 0 10px 20px -10px rgba(69,111,92,.95);
}
.story-nav-button.is-unlock-key .nav-key { display: grid; place-items: center; }
.story-nav-button.is-unlock-key .nav-key svg { transition: transform .32s cubic-bezier(.5,-.4,.4,1.5); }
.story-nav-button.is-unlock-key:not(:disabled):hover { background: #3f7d5a; }
.story-nav-button.is-unlock-key:not(:disabled):hover .nav-key svg { transform: rotate(-32deg); }
/* The end-state dim is for a DEAD arrow, and this one is the opposite of dead. It is
   disabled for exactly one reason — `goToCheckout` disabling it while it saves — and that
   is the moment a family is waiting on: the label it writes is invisible at `font-size: 0`
   and the SVG is gone with it, so the ring is the only thing left saying "working on it".
   At .18 opacity the button they just pressed to buy is a faint smudge. */
.story-reader-footer.is-end .story-nav-button.is-unlock-key:disabled { opacity: 1; box-shadow: 0 10px 20px -10px rgba(69,111,92,.95); }
@keyframes knob-bob { 0%, 100% { transform: translateY(0) rotate(-9deg); } 50% { transform: translateY(-3px) rotate(9deg); } }

/* --- controls row: "Read to me" is the coral hero pill ------------------ */
.audio-player { background: rgba(255,253,247,.55); border-bottom: 1px solid rgba(160,125,60,.16); padding: 11px 32px; gap: 11px; }
.audio-player button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; box-sizing: border-box; height: 42px; min-width: 150px; white-space: nowrap; overflow: hidden; padding: 10px 15px; border-radius: 999px; border: 1.5px solid #e8dcc2; background: #fffdf7; color: #7a6a4c; font-size: 13px; font-weight: 600; box-shadow: 0 5px 14px rgba(10,42,59,.06); }
.audio-player #playAllAudio { border-color: transparent; background: var(--coral); color: #fff; box-shadow: 0 10px 22px -10px rgba(215,91,69,.8); }
.audio-player #playAllAudio:hover { background: var(--coral-dark); }
/* ⏹ — a narrow pill, so the one extra button does not push the accent and reading-style
   pickers off the reader header. */
.audio-player .audio-transport { min-width: 0; padding: 10px 16px; }
/* Reading and paused both sit on the read button itself, which is also the pause button.
   Green rather than coral: mid-story is not the same offer as "not started yet". The id is
   spelled out — for BOTH states — because `#playAllAudio`'s own coral rule above outranks
   a class, and spelling out only the paused one left it reading coral while playing and
   green while paused: one button, two colours. */
.audio-player button.is-paused,
.audio-player button.is-playing,
.audio-player #playAllAudio.is-paused,
.audio-player #playAllAudio.is-playing { background: #3f7d5a; border-color: transparent; color: #fff; box-shadow: 0 10px 22px -10px rgba(63,125,90,.7); }
.audio-player label { color: #7a6a4c; font-size: 12px; font-weight: 600; }
.audio-player select { border: 1.5px solid #e8dcc2; border-radius: 999px; padding: 6px 12px; background: #fffdf7; font-family: inherit; font-weight: 600; color: var(--ink); }
.audio-player small { color: #a07d3c; font-size: 12px; }
/* equalizer bars while a track plays (three animated bars in place of ▶) */
.audio-player button.is-playing.is-loading .audio-icon { animation: spin 1s linear infinite; }
.audio-player button.is-playing:not(.is-loading) .audio-icon { position: relative; display: inline-block; width: 3px; height: 15px; margin: 0 6px 0 1px; border-radius: 2px; background: currentColor; font-size: 0; color: #fff; animation: reader-eq .7s ease-in-out infinite .18s; }
.audio-player button.is-playing:not(.is-loading) .audio-icon::before,
.audio-player button.is-playing:not(.is-loading) .audio-icon::after { content: ""; position: absolute; bottom: 0; width: 3px; height: 15px; border-radius: 2px; background: currentColor; }
.audio-player button.is-playing:not(.is-loading) .audio-icon::before { left: -5px; animation: reader-eq .7s ease-in-out infinite; }
.audio-player button.is-playing:not(.is-loading) .audio-icon::after  { left: 5px;  animation: reader-eq .7s ease-in-out infinite .36s; }
/* The stop square is DRAWN, for the reason the bars above are: ⏹ (U+23F9) is
   Emoji_Presentation, so it falls out of the text font to whatever the platform has —
   a colour-emoji bitmap, or a symbols face — and either one arrives with ragged edges at
   13px next to crisply-hinted label text. `#stopAudio`'s icon is the one in this header
   that JS never rewrites (`audioButtons` holds only the read button, so neither
   `resetAudioButtons` nor `setAudioButtonState` touches it), so the glyph in the markup
   stays put and `font-size: 0` is all that is needed to hide it. `flex: none` because the
   button is `inline-flex` and the span is a flex item that would otherwise shrink. */
.audio-player #stopAudio .audio-icon,
.audio-player #castStop .cc-stop { display: inline-block; flex: none; width: 12px; height: 12px; border-radius: 3px; background: currentColor; font-size: 0; }
/* The Cast mark is an inline SVG (see index.html for why), and left inline an SVG sits
   on the TEXT BASELINE — the span keeps a descender gap under it, so once the flex
   button centres the span the mark rides ~2px high of true centre, visibly so in the
   phone's bare 44px circle. The span goes flex and the SVG block so the mark centres on
   the pill's own axis; `flex: none` for #stopAudio's reason. No size here — the SVG's
   own 15px attributes are the size, matched to the ♫ beside it (its ink is ~14×11 of
   the 15px box, level with a 13px glyph and the 12px stop square). */
.audio-player #castAudio .audio-icon { display: inline-flex; flex: none; }
.audio-player #castAudio .audio-icon svg { display: block; }
/* Connected: the glyph swaps to Google's cast_connected variant and the pill lights up in
   the header's session green — the same values `.audio-player button.is-playing` paints,
   restated here because .is-casting is a different fact (a session exists) from
   .is-playing (sound is coming out), and the read button must be able to show one
   without the other. */
/* Connecting: the glyph becomes the same ring the read button spins while fetching —
   one "working on it" language across the header. The label says "Connecting…" (app.js
   writes it), so the ring is never the only signal. */
.audio-player #castAudio.is-connecting .audio-icon svg { display: none; }
.audio-player #castAudio.is-connecting .audio-icon {
  width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid rgba(14,61,86,.22); border-top-color: currentColor;
  animation: spin .9s linear infinite;
}
.audio-player #castAudio .cast-live { display: none; }
.audio-player #castAudio.is-casting .cast-live { display: block; }
.audio-player #castAudio.is-casting .cast-idle { display: none; }
.audio-player #castAudio.is-casting { background: #3f7d5a; border-color: transparent; color: #fff; box-shadow: 0 10px 22px -10px rgba(63,125,90,.7); }

/* --- the cast transport (#castControls): live only while a session is up ---
   `display` is set on the SPAN, so `[hidden]` must be re-armed explicitly — the same
   .addon-card[hidden] trap the narration gate documents: an attribute cannot beat an
   explicit display.
   ONE glyph language with the reader's own transport, geometry for geometry: the SAME
   8/13 border triangle the phone draws for #playAllAudio, the SAME 4×15 twin bars while
   playing, and the stop square IS #stopAudio's rule (extended below), so pausing the
   speaker looks exactly like pausing the phone. The green while playing needs no rule
   here at all — `.audio-player button.is-playing` already paints any transport button
   in this header, which is precisely the sameness being bought. is-playing is written
   by app.js from the bridge's own answer, never guessed from what was last clicked. */
#castControls { display: inline-flex; gap: 6px; align-items: center; }
#castControls[hidden] { display: none; }
#castControls .audio-transport { min-width: 34px; }
#castPlayPause .cc-icon {
  display: inline-block; width: 0; height: 0; margin-left: 2px; font-size: 0;
  border-style: solid; border-width: 8px 0 8px 13px;
  border-color: transparent transparent transparent currentColor;
}
#castPlayPause.is-playing .cc-icon {
  position: relative; width: 4px; height: 15px; margin: 0 0 0 8px;
  border: 0; border-radius: 1.5px; background: currentColor;
}
#castPlayPause.is-playing .cc-icon::before {
  content: ""; position: absolute; left: -8px; bottom: 0;
  width: 4px; height: 15px; border-radius: 1.5px; background: currentColor;
}
/* Coming up: the SAME ring #castAudio spins while connecting and the read button spins while
   fetching — one "working on it" language across this header, so a family reads the wait the
   same way wherever it happens. It has to be a drawn state and not a word: `#audioStatus` is
   hidden for the whole session, the labels are clipped on a phone, and the alternative was the
   idle ▶ triangle, which says the opposite of what is true. Declared AFTER .is-playing: the two
   carry equal specificity and file order is what decides, and a stale is-playing tick must not
   leave the second bar standing inside the ring (hence the ::before). */
#castPlayPause.is-busy .cc-icon {
  width: 15px; height: 15px; margin: 0 0 0 1px;
  border-radius: 50%;
  border: 2px solid rgba(14,61,86,.22); border-top-color: currentColor;
  animation: spin .9s linear infinite;
}
#castPlayPause.is-busy .cc-icon::before { content: none; }
/* The speaker mark, and the bar it reveals. The SVG goes `block` inside a flex span for
   #castAudio's reason — left inline it rides the text baseline and sits high of centre in
   a 44px circle. The bar is a sibling of the buttons rather than a popover: `.audio-player`
   already wraps, so on a narrow phone it takes a line of its own instead of needing a
   positioning context inside a grid item that is `pointer-events: none`. */
/* While a session is up the status line goes: it is the READ button's line ("Press play to
   hear a warm narration of this page…") and that button is not on the screen — the story is
   coming out of the speaker, and the transport beside it is the whole of what can be done
   about that. Hidden, never silenced: it goes on being written, so the identity comparisons
   in `applyNarrationAvailability` and the bake poll are untouched, the bargain `.has-peek`
   already strikes. */
.audio-player:has(#castControls:not([hidden])) small { display: none; }
#castControls .cc-vol { display: inline-flex; flex: none; font-size: 0; }
#castControls .cc-vol svg { display: block; }
/* The width is STATED, never left to the slider. `#castControls` is an inline-flex, so it
   sizes to its contents' max-content — and a percentage width on a child counts as auto in
   that measurement, so the bar took the range input's own intrinsic ~155px whatever
   flex-basis said, and the transport wrapped onto two lines on a phone. */
.cast-vol-bar { display: inline-flex; align-items: center; flex: 0 1 auto; width: 148px;
  height: 34px; box-sizing: border-box; padding: 0 12px; border-radius: 999px;
  border: 1.5px solid #e8dcc2; background: #fffdf7; }
.cast-vol-bar[hidden] { display: none; }
/* `accent-color` paints the native track and thumb in the header's session green, so the
   bar needs no per-engine pseudo-element rules to look like it belongs here. */
#castVolSlider { width: 100%; min-width: 0; height: 20px; margin: 0; background: transparent; accent-color: #3f7d5a; }
/* --- bedtime (#castPanel): the session's own panel, off the lit Cast button ----------
   A card under the header on a desktop and a bottom sheet on a phone (below). Positioned
   against the DIALOG — a <dialog> in the top layer is its descendants' containing block —
   because the header it visually hangs from is dissolved on a phone (`display: contents`)
   and has no box. `[hidden]` re-armed for the #castControls reason: display is set here.
   Green is the session colour throughout this header (the playing state, the slider), so a
   chosen chip and a queued badge are green too; the moon is the header's ink, not coral —
   this is not a call to action, it is a setting.
   `#storyDialog > .cast-panel`, not `.cast-panel`: every direct child of the dialog is made
   `position: relative; z-index: 1` by `.story-dialog > :not(.reader-twinkles)` (the twinkle
   layer's rule, specificity (0,2,0)), and a bare class loses to it — the panel then joined
   the flex column as a row of its own, nudged 24px off the left edge by `right` read as a
   relative offset. Seen on the first desktop cast. The phone rule below carries the same
   selector, or it would lose to this one. */
#storyDialog > .cast-panel { position: absolute; top: 74px; right: 24px; z-index: 6; width: min(360px, calc(100% - 32px));
  box-sizing: border-box; padding: 14px 16px 12px; border-radius: 18px; border: 1.5px solid #e8dcc2;
  background: #fffdf7; color: var(--ink); box-shadow: 0 22px 44px -20px rgba(10,42,59,.4); }
.cast-panel[hidden] { display: none; }
.cast-panel-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.cast-panel-moon { display: inline-flex; flex: none; width: 34px; height: 34px; align-items: center; justify-content: center;
  border-radius: 50%; background: rgba(14,61,86,.08); color: #0e3d56; }
.cast-panel-moon svg { display: block; }
.cast-panel-titles { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.cast-panel-titles strong { font-size: 15px; color: #0e3d56; }
.cast-panel-titles small { color: #a07d3c; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cast-panel-close { flex: none; width: 32px; height: 32px; border-radius: 50%; border: 1.5px solid #e8dcc2; background: #fffdf7;
  color: #7a6a4c; font-size: 20px; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; transition: background .16s ease, color .16s ease; }
.cast-panel-close:hover { background: #f3ece0; color: #0e3d56; }
.cast-panel-section { padding: 10px 0; border-top: 1px solid rgba(160,125,60,.16); }
.cast-panel-section[hidden] { display: none; }
.cast-panel-label { font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: #a07d3c; margin-bottom: 8px; }
.cast-panel-note { display: block; color: #7a6a4c; font-size: 12px; margin-top: 6px; }
.cast-panel-note[hidden] { display: none; }
.cast-sleep-chips { display: flex; flex-wrap: wrap; gap: 6px; }
/* Everything in here is pressable and has to LOOK it: the panel is a card of quiet paper
   pills, and without a cursor and a hover the chips read as labels — the first desktop cast
   reported exactly that. The hover is the border and the ground, never a size change: a chip
   that grows under the pointer nudges the ones beside it. */
.cast-chip { height: 32px; padding: 0 12px; border-radius: 999px; border: 1.5px solid #e8dcc2; background: #fffdf7;
  color: #7a6a4c; font-size: 13px; font-weight: 600; font-family: inherit; cursor: pointer;
  transition: background .16s ease, border-color .16s ease, color .16s ease; }
.cast-chip:hover { border-color: #3f7d5a; background: rgba(63,125,90,.08); color: #2f5244; }
.cast-chip:active { background: rgba(63,125,90,.16); }
.cast-chip.is-on { background: #3f7d5a; border-color: transparent; color: #fff; }
.cast-chip.is-on:hover { background: #35664b; color: #fff; }
.cast-panel :is(button, label, input):focus-visible { outline: 2px solid #3f7d5a; outline-offset: 2px; }
.cast-queue-list { list-style: none; margin: 8px 0 0; padding: 0; max-height: 220px; overflow: auto; }
.cast-queue-row label { display: flex; align-items: center; gap: 10px; padding: 7px 8px; margin: 0 -8px; border-radius: 10px;
  cursor: pointer; transition: background .16s ease; }
.cast-queue-row label:hover { background: rgba(63,125,90,.08); }
/* A row whose tick cannot move (playing, already read, mid-request) must not offer a hand
   and must not light up — the disabled box is the only other thing saying so. */
.cast-queue-row:has(input:disabled) label { cursor: default; }
.cast-queue-row:has(input:disabled) label:hover { background: none; }
.cast-queue-row.is-busy label { opacity: .55; }
.cast-queue-row input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: #3f7d5a; cursor: pointer; }
.cast-queue-row input:disabled { cursor: default; }
.cast-queue-title { flex: 1 1 auto; min-width: 0; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Whose book this is, inline with the title rather than a column of its own: the row is
   already a tick, a name and a position, and a fourth column on a 360px phone is what
   pushes the name to an ellipsis. */
.cast-queue-from { display: inline-block; margin-left: 2px; padding: 1px 7px; border-radius: 999px;
  background: rgba(14,61,86,.08); color: #0e3d56; font-size: 10px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; vertical-align: 1px; }
.cast-queue-badge { flex: none; font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #a07d3c; }
.cast-queue-badge.is-next { min-width: 22px; height: 22px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  background: rgba(63,125,90,.14); color: #2f5244; letter-spacing: 0; }
.cast-queue-badge.is-now { color: #3f7d5a; }
.cast-queue-badge[hidden] { display: none; }
.cast-panel-foot { padding-top: 10px; border-top: 1px solid rgba(160,125,60,.16); display: flex; justify-content: flex-end; }
.cast-panel-stop { height: 36px; padding: 0 16px; border-radius: 999px; border: 1.5px solid #e8dcc2; background: #fffdf7;
  color: #7a6a4c; font-size: 13px; font-weight: 600; font-family: inherit; cursor: pointer;
  transition: background .16s ease, border-color .16s ease, color .16s ease; }
/* The one destructive press in the panel, so the hover is the site's coral rather than the
   session green every other control here uses. */
.cast-panel-stop:hover { border-color: var(--coral); background: rgba(215,91,69,.08); color: var(--coral-dark); }

/* --- the voice being MADE ("Narration Progress.dc.html", treatment 1a) ---
   Thirteen tracks are thirteen pages, so they are drawn as thirteen pages rather than said
   three ways in one grey line ("0 of 13 tracks ready (0%)"). The count, the percentage and
   the progress bar collapse into the ticks; the word "tracks" is gone, because a parent
   buying a book has pages. `flex: 0 0 auto` beside the coral Read pill's `min-width: 150px`
   is what keeps the whole thing inside the ~260px the header has before it wraps to a second
   row — and the ticks FLEX, so a longer book gets thinner ticks rather than a wider header.
   The bob is what makes it read as working: a still line at 0% for eleven seconds reads as
   broken. The sheet's global `prefers-reduced-motion` rule stops it with `!important`, so
   this needs no opt-out of its own. */
.narration-bake { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; padding: 6px 14px 6px 7px; border: 1.5px solid #e8dcc2; border-radius: 999px; background: #fffdf7; box-shadow: 0 5px 14px rgba(10,42,59,.06); }
.narration-bake[hidden] { display: none; }
.narration-bake .nb-icon { position: relative; flex: none; display: grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: #fdf0cf; animation: nb-bob 2.6s ease-in-out infinite; }
/* The microphone, drawn: a capsule, the cradle arc under it, and the stem. */
.narration-bake .nb-mic { position: relative; display: block; width: 15px; height: 17px; color: #a86a1c; }
.narration-bake .nb-mic i { position: absolute; display: block; }
.narration-bake .nb-mic i:nth-child(1) { left: 3px; top: 0; width: 9px; height: 11px; border-radius: 999px; background: currentColor; }
.narration-bake .nb-mic i:nth-child(2) { left: 0; top: 6px; width: 15px; height: 8px; border: 2px solid currentColor; border-top: 0; border-radius: 0 0 999px 999px; }
.narration-bake .nb-mic i:nth-child(3) { left: 6.5px; bottom: 0; width: 2px; height: 3px; background: currentColor; }
/* The ✓ / ! that replaces the microphone in the two end states. Drawn from a character in
   the sheet rather than the markup, so JS swaps a class and never a glyph. */
.narration-bake .nb-mark { display: none; font-size: 15px; font-weight: 800; line-height: 1; }
.narration-bake .nb-body { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.narration-bake .nb-line { font-size: 12px; font-weight: 700; color: #a07d3c; white-space: nowrap; }
.narration-bake .nb-ticks { display: flex; align-items: flex-end; gap: 2px; width: 156px; height: 22px; }
.narration-bake .nb-ticks i { flex: 1 1 0%; min-width: 0; height: 17px; border: 1.5px solid #e2d3b4; border-radius: 1px 3px 3px 1px; background: #fdf8ec; transition: background .3s, border-color .3s, height .3s; }
.narration-bake .nb-ticks i.is-voiced { background: var(--coral); border-color: var(--coral); }
/* Taller, not just a different colour — the page being made now is the one thing in the row
   that has to be findable at a glance. */
.narration-bake .nb-ticks i.is-making { height: 22px; background: #f7d97d; border-color: #e6bd3f; box-shadow: 0 4px 10px -4px rgba(160,125,60,.85); }
.narration-bake .nb-ticks i.is-quiet { background: #fdeee9; border-color: #f0bdaf; }
/* `.audio-player button` is a 42px, 150px-wide pill — every one of those has to be undone
   here, or the retry sits inside the status pill as a second hero button. */
/* The step legend and the percentage are the phone band's ("Generation Progress Mobile - A")
   and are hidden at every other width: the desktop header is a ~260px flex line and this pill
   already measures 203-221px in it, so a third line and a readout beside it would wrap the
   header and take the height off the spread. They are still WRITTEN at every width — the same
   bargain `#audioStatus` strikes — so nothing has to know which layout is on screen. */
.narration-bake .nb-legend, .narration-bake .nb-pct { display: none; }
.narration-bake .nb-retry { display: block; align-self: flex-start; height: auto; min-width: 0; padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; color: var(--coral-dark); font-size: 11.5px; font-weight: 700; text-align: left; cursor: pointer; }
.narration-bake .nb-retry:hover { color: var(--coral); }
.narration-bake .nb-retry[hidden] { display: none; }

/* All done. It says so for two seconds and then retires, giving the header its space back —
   a permanent badge for a job that finished is one more thing in a row that has to hold a
   title, a Read pill and an unlock CTA. */
.narration-bake.is-ready { background: #eef7f0; border-color: #bcd9c4; padding-right: 16px; }
.narration-bake.is-ready .nb-icon { background: #456f5c; color: #fff; animation: none; }
.narration-bake.is-ready .nb-line { font-size: 12.5px; font-weight: 800; color: #3f6b53; }
/* A page lost its voice. The ticks stay pale coral, so the sentence and the picture agree —
   and the bob STOPS, because motion is the thing that means "still working". */
.narration-bake.is-quiet { background: #fdeee9; border-color: #f0bdaf; padding-right: 16px; }
.narration-bake.is-quiet .nb-icon { background: #f7ddd4; color: #c26a52; animation: none; }
.narration-bake.is-quiet .nb-line { font-weight: 700; color: #a8523a; }
.narration-bake.is-ready .nb-mic, .narration-bake.is-quiet .nb-mic { display: none; }
.narration-bake.is-ready .nb-mark, .narration-bake.is-quiet .nb-mark { display: block; }
.narration-bake.is-ready .nb-mark::before { content: "✓"; }
.narration-bake.is-quiet .nb-mark::before { content: "!"; }
/* No ticks on the done state: thirteen coral ticks under "Every page has a voice" is the
   same fact drawn twice, and the pill is about to retire anyway. */
.narration-bake.is-ready .nb-ticks { display: none; }
.narration-bake.is-ready .nb-body { gap: 0; }
/* nb- prefixed, because this sheet already carries five `bob` variants and a duplicate
   @keyframes name is last-wins with no warning. */
@keyframes nb-bob { 0%, 100% { transform: translateY(0) rotate(-5deg); } 50% { transform: translateY(-4px) rotate(5deg); } }
/* The phone band's crawling stripes. One stripe pair is 18px wide, so -36px is two of them
   and the loop is seamless whatever the fill's width happens to be. */
@keyframes nb-slide { to { background-position: -36px 0; } }

/* --- the spread: a floating, rounded, spined book ----------------------- */
/* The spread takes the whole band between the header and the footer, and is
   book-shaped rather than letterboxed: `aspect-ratio` sizes its WIDTH off the
   height flex just gave it, so two portrait pages sit side by side at 3:2 the
   way they did at 1120×760 — only bigger, and a touch wider so a 900px-tall
   laptop is not left with a NARROWER book than the fixed 760px cap gave it.
   `align-self: center` is what lets the
   width be `auto` at all (a column flex item stretches across otherwise), and
   `max-width` is the escape hatch on a short-and-narrow window, where the ratio
   gives way rather than the pages running off the edge. */
.story-reader { position: relative; flex: 1 1 0%; min-height: 0; align-self: center; aspect-ratio: 8 / 5; height: auto; width: auto; max-width: calc(100% - 32px); margin: 16px auto 12px; padding: 0; filter: none; border-radius: 0; background: #fffdf7; box-shadow: 0 44px 80px -34px rgba(40,25,5,.55), 0 8px 22px -12px rgba(40,25,5,.32); }
.reader-spine { position: absolute; left: 50%; top: 10px; bottom: 10px; width: 34px; transform: translateX(-50%); z-index: 4; pointer-events: none; background: linear-gradient(90deg, rgba(40,25,5,0) 0%, rgba(40,25,5,.14) 46%, rgba(40,25,5,.2) 50%, rgba(40,25,5,.14) 54%, rgba(40,25,5,0) 100%); }
/* No pixel cap of their own any more: the pages fill the spread, the spread
   fills the band, and the band is what is left of the dialog. A `760px` here is
   what left a 1080p reader with a book two thirds the size of the space it was
   given. `.story-copy-page` keeps its own `overflow-y: auto` (see 617) — it is
   what absorbs a long page now that the dialog itself is `overflow: hidden`. */
.story-art-page, .story-copy-page { height: 100%; }
/* The copy is centred by the FLEX PARENT, not by `margin: auto` on the block
   (which is what 621 does). Both look identical while the text fits — but the
   dialog is `overflow: hidden` now, so the copy page's own `overflow-y: auto`
   (617) is the only scroll left, and a `margin: auto` child resolves against
   negative free space and overflows at BOTH ends: the opening lines of a long
   page end up above the scroll origin, where nothing can reach them.
   So the copy block GROWS into the free space and centres its own lines inside
   itself: short copy sits centred exactly as before, long copy makes the block
   as tall as its text and the page scrolls from the first line. Centring on
   `.story-copy-page` instead would drag `.story-page-topline` down with it —
   the row of stars belongs at the head of the page. */
.story-page-copy { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; margin: 0 auto; }
/* Square outer corners: an 18px round on the dark art page revealed the reader's
   white (#fffdf7) card background at the top-left/bottom-left corners — the white
   triangles. Squaring the pages + the reader kills every corner reveal. */
.story-art-page { border-radius: 0; background: #e9e2d2; isolation: isolate; }
.story-art-page::after { width: 0; }

/* A generated illustration is 2:3 for a cover and 3:2 for a page (GenerationService's
   `aspect_ratio`) against an art half that is very nearly square — so `object-fit: cover`
   cut a third off every one of them: the sides of a cover, the top and bottom of a page.
   `.is-fitted` (set by renderStoryPage for a REAL illustration only) shows the whole
   picture and fills the letterbox with a blurred copy of the same art, so the spread
   stays full-bleed instead of gaining bars of paper.
   The stock placeholder deliberately keeps the cropped treatment: it is one jpg faked
   into per-page variety by object-position and scale, and contained it would be the
   identical picture on all twelve pages.
   `isolation: isolate` above is what the backdrop's z-index: -1 needs — it makes this
   section the stacking context, so the fill paints above the page's own background and
   below the in-flow illustration. */
/* The inside cover (the dedication page) has no illustration of its own. It used to
   borrow the COVER's artwork, which put the identical picture on the first two pages of
   every paid book — a book that looks like page one failed to paint. A book's paint
   budget is the cover plus one per spread, so the honest fix is a decorative plate: no
   image, no thirteenth paint. The glyph comes from `data-plate` (renderStoryPage writes
   the item's own icon) rather than being hardcoded here, so the page's character and its
   picture stay one decision. */
.story-art-page.is-plate { background: radial-gradient(120% 90% at 50% 0%, #fdf3dd, #f0e2c4 62%, #e7d6b5); }
.story-art-page.is-plate > img { display: none; }
.story-art-page.is-plate::before {
  content: attr(data-plate);
  position: absolute; inset: 0; z-index: 1;
  display: grid; place-items: center;
  font-size: clamp(72px, 17vh, 150px); line-height: 1;
  color: rgba(235, 117, 91, .38);
}

.story-art-fill { display: none; }
.story-art-page.is-fitted > img { object-fit: contain; }
/* pointer-events none is a CLICK fix, not tidiness: the blur filter + scale(1.15) make
   Chromium hit-test this element on its unclipped bounds, ~50px above the art page's own
   overflow:hidden box — invisibly, because PAINT is clipped — where it sat over the bottom
   strip of every header button and swallowed the clicks (the reader's "Read the story"
   pill only worked on its top half). The fill is pure decoration; nothing clicks it. */
.story-art-page.is-fitted > .story-art-fill { display: block; position: absolute; z-index: -1; inset: 0; object-fit: cover; object-position: center; filter: blur(26px) saturate(1.15) brightness(.92); transform: scale(1.15); pointer-events: none; }
.story-copy-page { border-radius: 0; background: linear-gradient(180deg, #fffdf7, #fdf6e8); box-shadow: none; padding: 46px 52px 34px; }
.story-copy-page::after { opacity: .12; }

/* page tag on the illustration (bottom-left) */
.story-scene-badge { left: 16px; bottom: 14px; gap: 6px; padding: 5px 11px; border-radius: 999px; background: rgba(13,47,67,.62); }
.story-scene-badge span { color: var(--gold-light); font-size: 12px; }
.story-scene-badge small { font-size: 11px; font-weight: 700; letter-spacing: .3px; text-transform: none; }

/* cover copy overlaid on the cover art */
.story-cover-copy > span { color: var(--gold-light); font-size: 11px; letter-spacing: 2px; }
.story-cover-copy strong { font-weight: 700; }
.story-cover-copy small { font-size: 14px; }

/* running-story text: gold chapter tag, kept heading, drop-cap body ------ */
/* Raised with the page: 448px on a half that is now ~620px wide left the copy
   as a narrow strip with an inch of paper either side of it. Still a cap and
   not full-bleed — the 590-592 note holds, a line of story that runs the whole
   width of a big monitor is unreadable. */
.story-page-copy { max-width: 540px; }
.story-page-copy h3 { margin: 10px 0 14px; font-family: var(--display); font-size: clamp(24px, 2.6vw, 34px); font-weight: 700; line-height: 1.08; color: var(--ink); }
.story-page-copy p { color: #2c3a2a; font-family: var(--display); font-size: clamp(18px, 1.7vw, 23px); line-height: 1.72; text-wrap: pretty; }
.story-dropcap { float: left; font-family: var(--display); font-size: 3.1em; line-height: .8; font-weight: 700; color: var(--coral); margin: 6px 12px 0 0; }

/* --- footer: circular arrows, beads + counter --------------------------- */
.story-reader-footer { gap: 24px; padding: 4px 30px 8px; }
/* The `‹ ›` are TEXT, and a glyph's ink is not centred in its em box — the pair sits
   ~2px below the circle's own centre, which nothing hides in a round button holding
   nothing else. The extra BOTTOM padding lifts the flex-centred content by half itself,
   and it is stated in `em` so the one rule serves the 58px circle here and the 48px one
   on a phone (a fixed figure would over-lift the smaller glyph). It also cancels itself
   where it must: `.is-unlock-key` sets `font-size: 0`, so the em term is 0 and the key
   SVG — a box that is centred already — sits back on the true centre. */
.story-nav-button { min-width: 0; width: 58px; height: 58px; padding: 8px 8px calc(8px + .155em); border-radius: 50%; border: 1.5px solid #e8dcc2; background: #fffdf7; color: var(--coral); font-size: 30px; font-weight: 600; line-height: 1; box-shadow: 0 12px 26px -12px rgba(40,25,5,.5); transition: transform .16s, background .16s; }
.story-nav-button span { display: none; }
.story-nav-button:not(:disabled):hover { transform: scale(1.08); background: #fff; }
.story-nav-button:disabled { opacity: .35; }
.reader-progress { display: flex; flex-direction: column; align-items: center; gap: 9px; }
.story-progress-dots { gap: 8px; }
.story-progress-dots button { width: 11px; height: 11px; border-radius: 999px; background: #e3d3b2; }
.story-progress-dots button.done { background: #e0a86a; }
.story-progress-dots button.active { width: 26px; height: 11px; border-radius: 999px; background: var(--coral); box-shadow: 0 3px 8px -2px rgba(215,91,69,.6); }
.reader-counter { font-size: 14px; font-weight: 600; color: #a07d3c; }

/* --- the last page says so ---------------------------------------------- */
/* A child watching the story is looking at the BOOK, not at the arrows under it, so the
   end of it is stated on the paper: a printed book's closing flourish, which is what one
   actually does with this page. The ornament carries it wordlessly on purpose — this is
   inside the book, where the only hand-written words are PREVIEW_COPY's 41 translations —
   and `.story-end-word` is English, so renderStoryPage shows it for an English book only.
   `[hidden]` needs the rule below or the attribute loses to this `display: flex` (the same
   reason `.addon-card[hidden]` and `.kc-star[hidden]` exist). */
.story-end { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; gap: 14px; width: 100%; max-width: 540px; margin: 22px auto 0; padding-top: 4px; color: #b08a3e; transform: translateY(0); animation: reader-end-in .5s ease both; }
.story-end[hidden] { display: none; }
.story-end-rule { flex: 1 1 auto; max-width: 120px; height: 1px; background: linear-gradient(90deg, transparent, currentColor, transparent); opacity: .5; }
.story-end-mark { flex: none; font-size: 20px; line-height: 1; }
.story-end-word { flex: none; font-family: var(--display); font-size: 19px; font-weight: 700; letter-spacing: .5px; white-space: nowrap; }
.story-end-word[hidden] { display: none; }
/* The free preview's last page is NOT the end of the story — coral is the buy colour
   everywhere else on the site, so the two endings do not read as the same thing. */
.story-end.is-preview { color: var(--coral); }
/* The words for it, in the footer: English, like the counter directly above them. */
.reader-endnote { max-width: 320px; font-size: 13px; font-weight: 600; line-height: 1.35; text-align: center; text-wrap: balance; color: #a07d3c; }
.reader-endnote[hidden] { display: none; }
.story-reader-footer.is-end .reader-endnote { color: #8a6a2c; }
/* The dead arrow is dimmed further and stops inviting a press — at .35 alone it still
   read as a button that had not been tried yet. */
.story-reader-footer.is-end .story-nav-button:disabled { opacity: .18; box-shadow: none; }
@keyframes reader-end-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

/* --- approval bar: warm to match ---------------------------------------- */
.story-approval-bar { background: rgba(255,253,247,.6); border-top: 1px solid rgba(160,125,60,.18); }
.edit-story-button, .edit-page-button { border-radius: 999px; }
.story-dialog-actions .edit-page-button {
  min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 0 16px; border: 1.5px solid #e4bd55; background: #fff8e5; color: #7b5410;
  font-size: 12px; font-weight: 800; white-space: nowrap; cursor: pointer;
  box-shadow: 0 6px 16px rgba(10,42,59,.08);
}
.story-dialog-actions .edit-page-button:hover { background: #fff3cc; }
.story-dialog-actions .edit-page-button:focus-visible { outline: 2px solid #c18b16; outline-offset: 3px; }
.story-dialog-actions .edit-page-button[hidden] { display: none; }
.story-dialog-actions .edit-page-button svg { flex: none; }

/* fullscreen keeps the rounded spread — width/height caps + centring live in
   the fullscreen block above (425+); here we only carry the card corners. */
.story-dialog:fullscreen .story-reader, .story-dialog.fullscreen .story-reader { border-radius: 0; }

/* The footer is the page arrows and the progress dots only. "Read this page" is gone:
   reading is the header's one Read/Pause button, and a page is reached by the narration
   walking on to it. `.page-read-button` went with it — a rule left behind here is a
   button somebody re-adds to the markup with no JS behind it. */

/* The reader's transport keeps a focus ring — a keyboard reader must be able to see where
   they are — but it styles its OWN. The browser's default ring takes its colour from the
   desktop accent (Ubuntu paints it orange), which sits hard against the pill's edge and
   reads as a border in the wrong colour: a coral Pause with a red rim, a green Resume
   with an orange one. Ring in the button's own colour, offset clear of the pill so it
   reads as a halo. `:focus-visible`, not `:focus`, because that is exactly the ring being
   replaced — a mouse click still leaves none. */
.audio-player button:focus-visible { outline: 2px solid var(--coral); outline-offset: 3px; }
.audio-player button.is-playing:focus-visible,
.audio-player button.is-paused:focus-visible { outline-color: #3f7d5a; }

/* Drop the "unlocked" status badges (top-right chip + approval-bar line),
   the meta subtitle line, and the on-illustration brand badge. */
#previewAccessBadge, #storyApprovalText, #storyPreviewMeta, .story-scene-badge { display: none; }
/* Close cross lives inline in the header, level with the title (both modes).
   Optical-centre the × glyph inside the circle (it renders a hair high) — this rule
   said so and then wrote `padding: 0`, which is the base rule's correction UNDONE. */
.story-dialog .dialog-close { position: static; top: auto; right: auto; left: auto; flex: none; display: grid; place-items: center; line-height: 1; padding: .13em 0 0; }

/* The picture-only toggle is a PHONE control and nothing else — a desktop reader has
   the whole spread already. Hidden here rather than behind a `min-width` guard after
   the phone block, so there is one rule to read and the phone block simply turns it on. */
.reader-immersive { display: none; }

/* ── PHONE READER: the reader is a SHEET, not a scrolling page ────────────────
   ("Book Reader Responsive.html" — ported from the DESIGN FILE this time.)

   The first port was made from that file's spec header alone: the file is past the
   design tool's 256 KiB read cap, so what landed was a paraphrase — a flex column
   with the transport stacked in the header, the beads under the book and no
   picture-only mode at all. The design is a GRID, and the difference is the whole
   point of it: three stacked bars of chrome over a picture cost 191px of an 844px
   screen, and the book got what was left.

   The dialog IS the phone's grid, one band per row, and the header's own box is
   dissolved (`display: contents`) so its children are items of THAT grid. That is
   what lets the narration buttons and the unlock action sit where they are wanted
   without moving a line of markup: DOM order — and so reading order — is untouched,
   only the painted rows move, and every id app.js writes into stays where it was.

     row 1  the book's name · picture-only · unlock · ✕
     row 2  the book (picture over words)
     row 3  the shelf: narration on the left, the pager on the right
     row 4  the notice row — production's own (see below)

   Eleven things are load-bearing.

   (1) `margin: 0` and `max-width: none` are spelled out. The UA sheet centres a
       <dialog> with `margin: auto` and the desktop rule at 3055 sets
       `width: min(1520px, …)`; either one left over turns a full-height sheet into
       an overflowing centred card — the clipped-copy failure the old stacked block
       existed to avoid, wearing new rules.
   (2) `100dvh`, not `100vh`: on iOS `100vh` is the tallest the viewport ever gets,
       so the footer sits under the URL bar — and this sheet has no scroll to give.
       The `100vh` line before each is the fallback for engines without `dvh`.
   (3) The gate is BOTH phone shapes. An iPhone 12/13/14 on its side is 844 CSS px
       WIDE, so a width-only gate left a landscape phone on the desktop spread with
       the landscape tweaks layered over the wrong layout. The landscape block at the
       foot is pure overrides on top of this one.
   (4) The width arm is 760px and not the design's 720: the early block at 1491
       already stacks `.story-reader` into one column from 760 down, so a sheet that
       started at 720 would leave 40px of width where the spread is stacked but the
       pages still carry the desktop `height: 100%` inside an 8:5 box — clipped, with
       nothing on screen to say so. Every other gate here is the design's own.
   (5) The art half is a FIXED share of the sheet (42dvh, floor 190px, cap 46dvh) —
       not the picture's own height, so every page is framed identically, which is
       the invariant the desktop spread's fixed height exists for. Nearly square
       rather than a letterbox band: a 2:3 cover shown whole (`.is-fitted`) in a wide
       band is more blurred backdrop than picture. Because the half has a definite
       height, `.is-fitted` needs NO phone special-casing — contain plus the blurred
       backdrop behave exactly as they do on the desktop spread, and the old rules
       that dropped the floor and hid `.story-art-fill` would now letterbox a real
       illustration in bars of paper: the crop bug `.is-fitted` exists to fix,
       inverted. `.is-plate` and `#storyArtPainting` are `inset: 0` overlays and
       follow the band for free.
   (6) `#approveStory` becomes the LOCK ITSELF. At the top of a phone there is room
       for the book's name OR a sentence-long CTA, not both. The padlock is DRAWN for
       the reason `#stopAudio`'s square is: a 🔒 falls out of the text font to
       whatever the platform has. The body is a `::before`; the shackle is the arrow
       `<span class="unlock-shackle">` the markup carries on both states with its
       glyph zeroed — a CLASS and not `#approveStory span`, because the unlock pill
       (the header's own option A) is spans too and each one drew a shackle of its
       own on top of the last. `font-size: 0` hides the button's words from the EYE
       only — they stay in the accessibility tree; the key knob and the price line
       are `display: none` here, being decoration and a figure the checkout states
       again. The peek meter is hidden for the same reason nothing else is added to
       this row: every child of the header is placed by hand and a fifth item would
       auto-place into the book.
   (7) The read control is drawn too — a triangle, two bars while playing, a ring
       while fetching — at ID specificity, because on a phone that ring is the ONLY
       "working on it" signal left: the labels are clipped and the status line is
       gone. A border-drawn triangle spun about its own box reads as a wobble, so the
       shape becomes a ring first.
   (8) The footer reserves the narration group's width on the LEFT (48 + 8 + 44 + 12
       of padding = 112, plus a 16px gutter) rather than living in a column — that
       column is sized by the header's lock button and left the pager 128px on a
       320px phone, wrapped onto two rows. Narration on the left and the pager on the
       right are two corners rather than one crowded huddle.
   (9) The beads become ONE two-tone track. Twenty-four 11px beads do not fit a phone
       row and an 11px bead was never a 44px target anyway. No gap: at 24 pages on a
       320px phone a 3px gap is wider than the segment it separates and the bar
       dissolves into dashes. The current page is a raised notch with a 12px floor so
       it stays findable between 5px segments. They are still `<button>`s and a tap
       still jumps.
  (10) **Row 4 is production's, not the design's.** The demo the design was drawn on
       predates `#narrationBake`, the Cast button and the sample-language picker, all
       of which live INSIDE `.audio-player` — and that element is one grid item on a
       nowrap row of two round buttons. They wrap onto a line of their own instead,
       which is row 4 (`auto`, so it is 0px tall when all three are hidden). The
       container is `pointer-events: none` with its children `auto`: its box is as
       wide as its widest line and would otherwise swallow taps meant for the pager
       sitting under it in row 3.
  (11) **`#audioStatus` is hidden, EXCEPT when there is no read button to explain
       itself.** The design hides the status line outright (a narration failure is
       spoken on the button's own label, which `setAudioButtonState` already writes)
       — but that line is also the only thing that says why a LOCKED book, a guest's
       book or a book in a language the narrator cannot speak has no read button at
       all, and `applyNarrationAvailability` hides the button in every one of those
       cases. `:has()` brings it back on row 4 for exactly those.
   ========================================================================== */
@media (max-width: 760px), (max-width: 900px) and (max-height: 500px) {
  .story-dialog { border: 0; border-radius: 0; background: #fdf8ec; }
  /* `showModal()` already gives a top-layer dialog `position: fixed; inset: 0`, so
     width/height/margin are all this needs to fill the screen. The safe-area padding
     is on the DIALOG rather than on the footer: row 4 is what sits at the bottom of
     the screen when a bake is running, and the home indicator does not care which
     row it is under. */
  .story-dialog[open] {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto 60px;
    grid-template-rows: 64px 1fr auto auto; column-gap: 8px;
    width: 100%; max-width: none; margin: 0;
    padding-top: env(safe-area-inset-top, 0px);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    height: 100vh; height: 100dvh;
    max-height: 100vh; max-height: 100dvh;
    border-radius: 0; overflow: hidden;
  }
  .story-dialog-header, .story-dialog-actions { display: contents; }
  /* A desktop grace note that costs paint on every scrolled frame and animates
     underneath the words on a screen this size. */
  .reader-twinkles { display: none; }

  /* --- row 1: the book's name, the buy action, the way out ------------------
     A 64px band so the 44px cross has room to breathe either side of it, and so
     the lock sits level with both rather than pretending to be a third bar. */
  .story-dialog-header .reader-title {
    grid-area: 1 / 1; align-self: center; min-width: 0;
    padding: 0 0 0 16px; background: none; border: 0;
  }
  .story-dialog-header h2 { font-size: 18px; letter-spacing: -.3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* The cross sits at the START of a 60px track — the 16px to its right is the
     track, not a margin: a margin on a 44px-wide item in a 44px track just overflows
     it and the cross ends up flush to the screen edge. In the grid and not absolute,
     because the lock has to sit to its LEFT and a sibling should not have to reserve
     clearance for a floating cross. */
  .story-dialog .dialog-close { grid-area: 1 / 4; align-self: center; justify-self: start; position: static; width: 44px; height: 44px; font-size: 23px; }
  .story-dialog-actions #approveStory {
    grid-area: 1 / 3; align-self: center; position: relative;
    width: 44px; height: 44px; min-height: 44px; flex: none;
    margin: 0; padding: 0; border-radius: 50%;
    font-size: 0; gap: 0; overflow: hidden;
  }
  /* Paid books replace the lock in this same slot with the per-page control — the
     admin-only pencil, or the family's own "Not happy?". All three are mutually hidden by
     updateReaderLockUI/updatePageEditUI, so the header still has four painted items and
     nothing auto-places into the book row.
     `#reportPage` is named EVERYWHERE `#editCurrentPage` is, in all four phone blocks: it
     was added to the header on 2026-08-28 and inherited none of this, so on a phone it kept
     the desktop's full text pill — a 100px+ "Not happy?" wedged into a track sized for a
     44px circle, squeezing the book's title beside it. The two are one control in two
     voices and must be sized as one. */
  .story-dialog-actions #editCurrentPage,
  .story-dialog-actions #reportPage {
    grid-area: 1 / 3; align-self: center; position: relative;
    width: 44px; height: 44px; min-height: 44px; margin: 0; padding: 0;
    border-radius: 50%; font-size: 0; gap: 0;
  }
  .story-dialog-actions #editCurrentPage span,
  .story-dialog-actions #reportPage span { display: none; }
  .story-dialog-actions #editCurrentPage svg,
  .story-dialog-actions #reportPage svg { display: block; width: 20px; height: 20px; }
  /* The padlock's body and, below, its shackle. Both are centred with a TRANSFORM and
     never a negative margin: the universal reset is `*` and a pseudo-element is not an
     element, so these boxes are content-box and a margin of half the declared width is
     half a border out — which is what put the shackle 2.5px right of the body it sits on
     until 2026-08-06. A transform is half the RENDERED box whatever the borders do.
     The two together span `top` of the shackle to the bottom of the body, so the pair is
     placed 1px higher than the arithmetic centre of each on its own — the mark is what
     has to sit in the middle of the circle, not either half of it. */
  .story-dialog-actions #approveStory::before {
    content: ""; position: absolute; left: 50%; top: 20px;
    width: 18px; height: 13px; transform: translateX(-50%);
    border-radius: 3px; background: currentColor;
  }
  /* The shackle. It is the arrow `<span>` the markup carries on BOTH states, with
     its glyph zeroed — and it is named by a class now rather than matched as
     `#approveStory span`, because the unlock pill's own markup (the key knob and
     the two lines of copy) is spans too and would each have drawn a second
     shackle on top of the first. The `.button-unlock` selector is here only to
     out-rank the desktop rule that hides this span on that variant: equal
     specificity, later in the file, so the phone wins. */
  .story-dialog-actions #approveStory.button-unlock .unlock-shackle,
  .story-dialog-actions #approveStory .unlock-shackle {
    display: block; position: absolute; left: 50%; top: 11px;
    width: 12px; height: 9px; transform: translateX(-50%); padding: 0;
    border: 2.5px solid currentColor; border-bottom: 0; border-radius: 7px 7px 0 0;
    background: none; font-size: 0;
  }
  /* Everything the desktop pill says, on a button that is 44px of padlock. The
     words survive in the accessibility tree the way `font-size: 0` leaves them:
     `display: none` here is only the KEY and the two lines it sits beside, which
     are decoration and a price the checkout page states again. */
  .story-dialog-actions #approveStory .unlock-knob,
  .story-dialog-actions #approveStory .unlock-copy { display: none; }
  /* The peek meter has no track. This header is `display: contents` and every one
     of its children is placed by hand in a 64px row of four; a fifth item would
     auto-place into the book. The locked state is said by the padlock button and,
     where there is no read button to explain itself, by `#audioStatus` in row 4. */
  .reader-peek { display: none; }
  /* Picture only. The mark is the EXPAND one — four corner brackets, 18px square, in
     index.html as an inline SVG (see the comment on the button for why it is a path and
     not a box). Two things are load-bearing. (1) The SVG is a direct child of a
     `display: grid` button, so it is BLOCKIFIED and `place-items` centres it exactly —
     `display: block` is written anyway, because it is the whole guard the day this button
     stops being a grid: an svg inside an INLINE box sits on the text baseline and keeps a
     descender gap under it, which rides the mark ~2px high of centre (the trap
     `#castAudio` and `#castVolume` each carry a rule for). (2) There is no
     `::before`/`::after` here any more, and there may not be: the universal reset is
     `*`, which does NOT match a pseudo-element, so a bordered pseudo is content-box and
     every negative-margin centring in one is off by half its border — which is exactly
     how the frame this replaced came to sit with its inner rectangle 1.5px down and to
     the right of its own centre. */
  .reader-immersive {
    grid-area: 1 / 2; align-self: center; justify-self: end;
    display: grid; place-items: center; position: relative;
    width: 44px; height: 44px; margin: 0; padding: 0;
    border: 1.5px solid #e8dcc2; border-radius: 50%;
    background: #fffdf7; color: #395363; cursor: pointer;
    box-shadow: 0 6px 16px rgba(10,42,59,.08);
  }
  .reader-immersive svg { display: block; width: 20px; height: 20px; }

  /* --- row 3, left: narration ----------------------------------------------
     Two round buttons — ▶ to read the page aloud, ⏹ to stop. The words are clipped
     rather than removed so a screen reader still hears them. */
  /* The two side paddings are custom properties because the STATUS BAND on the line
     below has to cancel them: the design's band is full-bleed, and a negative margin
     that guessed at a number would be a hairline of sheet down one edge the day this
     padding changes (the ≤380px block already changes the right one). */
  .story-dialog-header .audio-player {
    --ap-pad-l: 12px; --ap-pad-r: 0px;
    grid-area: 3 / 1 / 4 / -1; align-self: center;
    position: relative; z-index: 2; pointer-events: none;
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    padding: 0 var(--ap-pad-r) 0 var(--ap-pad-l); border: 0; background: none;
  }
  /* The span into row 4 is CONDITIONAL, and that is the whole of (10). The player is
     ONE grid item, so a row span does not put its wrapped lines in row 4 — it centres
     the whole element across both rows and lets the flex box decide where the lines
     fall. That works only while the first line is the 48px read button, which is the
     footer's own height; the two other shapes it takes each broke a different way.
     (a) NOTHING wrapped (the ordinary paid book): a permanent span made row 4 an 18px
     band of nothing — Chromium still grows a spanned auto track — so the footer sat
     18px off the bottom of a sheet that is exactly `100dvh` for the reason above, and
     the ▶ circle rode 9px below the pager it is meant to sit level with.
     (b) NO read button at all: the status line is then the player's entire content, so
     centring it across rows 3 and 4 put those 14px INSIDE the footer, straight through
     the ‹ › arrows and the end-note — on a LOCKED book, which is every book a family
     has not paid for yet, and the one state (11) exists to serve.
     So: row 3 alone by default (the design's own placement, level with the pager), the
     span only while something is actually there to wrap, and row 4 ALONE when the read
     button is gone and the line is all there is. `:has(#playAllAudio[hidden])` outranks
     the span rule on its own (an id inside `:has()` carries that id's specificity), so
     the two cannot fight over a book that is both locked and baking. */
  /* While it spans, the ▶ circle sits 6px above the ‹ beside it — the footer's own top
     padding, which the player has no equivalent of. It is left there deliberately: the
     art band is a fixed 42dvh, so the 6px a matching `padding-top` would cost is payable
     only out of `.story-copy-page`, and a line of the story reflowing when the bake
     starts and again when it finishes is a worse thing to watch than two round buttons
     sitting 6px apart for the few seconds a voice takes to make. */
  .story-dialog-header .audio-player:has(#narrationBake:not([hidden])),
  .story-dialog-header .audio-player:has(#sampleLangLabel:not([hidden])) { grid-area: 3 / 1 / 5 / -1; }
  .story-dialog-header .audio-player:has(#playAllAudio[hidden]) { grid-area: 4 / 1 / 5 / -1; }
  /* Casting takes row 4 ALONE, and it used to be in the span list above. The span works by
     CENTRING the whole player across rows 3 and 4 and letting the flex box place its lines,
     which is fine while the player's first line is one 48px circle sitting level with the
     pager — the footer reserves 112px on the left for exactly that. A live session makes it
     four items and a volume bar, which is the width of the sheet: spanned, it lay straight
     across the ‹ › arrows and the page label. Its own row costs 44px and collides with
     nothing. Stated AFTER the read-button rule it ties with on specificity (an id inside
     `:has()` each), so file order is what decides — they place it the same way, and this is
     the one that survives if either changes. */
  .story-dialog-header .audio-player:has(#castControls:not([hidden])) { grid-area: 4 / 1 / 5 / -1; }
  .story-dialog-header .audio-player > * { pointer-events: auto; }
  .story-dialog-header .audio-player small { display: none; }
  .audio-player #playAllAudio { flex: none; width: 48px; height: 48px; min-width: 0; padding: 0; border-radius: 50%; }
  /* A drawn play triangle, and two drawn bars while it is playing — not the ♫ in the
     markup, which says "music" rather than "start". `.is-loading` becomes a ring and
     `.is-paused` falls through to the triangle, which is what "resume" should look like. */
  .audio-player #playAllAudio .audio-icon {
    display: inline-block; width: 0; height: 0; margin-left: 2px; font-size: 0;
    border-style: solid; border-width: 8px 0 8px 13px;
    border-color: transparent transparent transparent currentColor;
    background: none; border-radius: 0; animation: none;
  }
  .audio-player #playAllAudio.is-playing:not(.is-loading) .audio-icon {
    position: relative; width: 4px; height: 15px; margin: 0 0 0 8px;
    border: 0; border-radius: 1.5px; background: currentColor; animation: none;
  }
  .audio-player #playAllAudio.is-playing:not(.is-loading) .audio-icon::before {
    content: ""; position: absolute; left: -8px; bottom: 0;
    width: 4px; height: 15px; border-radius: 1.5px; background: currentColor; animation: none;
  }
  .audio-player #playAllAudio.is-playing:not(.is-loading) .audio-icon::after { display: none; }
  .audio-player #playAllAudio.is-loading .audio-icon,
  .audio-player #playAllAudio.is-playing.is-loading .audio-icon {
    width: 15px; height: 15px; margin: 0; border-radius: 50%;
    border: 2px solid rgba(255,255,255,.42); border-top-color: #fff;
    background: none; animation: spin .9s linear infinite;
  }
  .audio-player #playAllAudio.is-loading .audio-icon::before,
  .audio-player #playAllAudio.is-loading .audio-icon::after { display: none; }
  .audio-player .audio-transport,
  .audio-player #castAudio { flex: none; position: relative; width: 44px; height: 44px; min-width: 0; padding: 0; border-radius: 50%; }
  .audio-player button .audio-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

  /* --- row 4: the notice row (production's own — see (10) and (11)) ---------
     On a phone the pill is the design's two-stage STATUS BAND ("Generation Progress
     Mobile - A"): an orb, a copy line over one continuous bar over the step legend, and the
     percentage on the right. A book that has just been paid for is finished in a fixed order
     — `BookFinishService` paints the eight deferred spreads and then enqueues the bake — so
     the wait really is two stages, and it is worth saying which one is on: four minutes of
     "Finishing the last 8 pictures…" with no label reads as one open-ended wait.

     Six things are load-bearing. (1) IT IS A BAND, NOT A CARD. Full-bleed along the bottom
     edge with one rule along its top, which is what the two negative margins buy back (see
     `--ap-pad-*` above) — and its bottom margin is 0, because the home indicator's clearance
     is on the DIALOG and counting it twice is a fat gap on iOS alone. (2) ONE BAR, ONE JOB AT
     A TIME. Both working stages are `--nb-fill` and the per-page ticks stand down under both:
     those ticks count narration TRACKS, so step 1 has none of them voiced (the band would say
     "42%" over an empty bar) and step 2 would leave the track being made sitting GOLD on the
     end of a coral bar. They stay per-page in the DOM (`paintNarrationBakeTicks` writes them,
     and they are still the object the desktop draws) and are what `.is-quiet` draws, where
     WHICH page went silent is the message; on a phone the row becomes one 8px seam, because
     at twelve pages across a 360px row a 2px gap is wider than the segment it separates and
     the bar dissolves into dashes — the call `#storyProgressDots` already makes one row down.
     (3) The tone is per STAGE and the three are the design's: gold for the pictures, coral for
     the read-aloud, green when it is done. Step 2 moves the ORB and the FILL and leaves the
     band cream — a band that turns coral whole reads as an alert, and coral is what
     `.is-quiet` wears, which has to stay tellable apart from an ordinary second stage. (4) The
     glyph is DRAWN in both stages, the rule every icon in this header keeps: the same three
     `<i>`s that are a microphone become a brush. (5) It RETIRES by collapsing (`.is-retiring`,
     added by `updateNarrationBake` a beat before it hides the band): a full-bleed band that
     simply went `hidden` would take its grid row out in one frame and the footer would jump.
     (6) Everything here is inside the phone block, so the desktop pill is rule-for-rule
     untouched — the landscape block restates the border, the radius and the padding it zeroes,
     since there the band rides inline on a 56px header row. */
  .narration-bake {
    flex: 1 1 100%; width: auto;
    /* Full-bleed, which is what makes it a BAND rather than a card sitting on one: the
       design's band is the bottom edge of the screen, ruled off from the footer above it
       by a single top border. The two negative margins are the player's own padding given
       back (see `--ap-pad-*` above), and the bottom one is 0 so the band ends where the
       sheet does — the home indicator's clearance is on the DIALOG, not on this. */
    margin: 8px calc(-1 * var(--ap-pad-r, 0px)) 0 calc(-1 * var(--ap-pad-l, 12px));
    display: grid; grid-template-columns: 34px 1fr auto; align-items: center; gap: 11px;
    padding: 11px 14px 13px; border: 0; border-top: 1.5px solid #e9dcbe; border-radius: 0;
    background: #fdf6e6; box-shadow: none;
  }
  .narration-bake .nb-icon { width: 34px; height: 34px; }
  .narration-bake .nb-body { flex: none; min-width: 0; gap: 6px; }
  .narration-bake .nb-line { min-width: 0; font-size: 12.5px; color: #5a4b2c; overflow: hidden; text-overflow: ellipsis; }
  .narration-bake .nb-ticks { position: relative; width: auto; gap: 0; height: 8px; border-radius: 99px; overflow: hidden; background: #efe3c8; }
  .narration-bake .nb-ticks i { height: 8px; border: 0; border-radius: 0; background: transparent; }
  .narration-bake .nb-ticks i.is-making { height: 8px; box-shadow: none; background: #f7d97d; }
  .narration-bake .nb-legend { display: block; font-size: 9.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: #6c604a; }
  .narration-bake .nb-pct { display: block; font-family: var(--display); font-size: 15px; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }

  /* ONE BAR, ONE JOB AT A TIME — the design's own sentence, and the whole of the band's
     bar. It fills gold for the pictures, RESETS and fills coral for the read-aloud, and
     stands at a gold 100% for the beat the done band is up. So both working stages draw
     the same `--nb-fill` fill and the per-page ticks stand down under it at every one of
     them: the ticks count narration TRACKS, so in step 1 not one of them is voiced (an
     untouched tick row beside "42%"), and in step 2 the one being made is GOLD — a gold
     chunk on the end of a coral bar, which is the two stages drawn at once. `.is-quiet` is
     neither class, so a page that went silent keeps its per-page ticks untouched: there the
     WHICH is the whole point, and there is no percentage beside them to disagree with. */
  .narration-bake.is-painting .nb-ticks i,
  .narration-bake.is-voicing .nb-ticks i { display: none; }
  .narration-bake.is-painting .nb-ticks::after,
  .narration-bake.is-voicing .nb-ticks::after,
  .narration-bake.is-ready .nb-ticks::after {
    content: ""; display: block; width: var(--nb-fill, 0%); height: 100%; border-radius: 99px;
    background: repeating-linear-gradient(115deg, var(--gold) 0 9px, #f2bd4d 9px 18px);
    background-size: 200% 100%;
    transition: width .45s ease;
  }
  /* The stripes crawl while there is something to wait for, and stop dead on the done band.
     `background-size` above is what gives `background-position` room to move — without it
     the animation runs and nothing appears to happen. The sheet's global
     `prefers-reduced-motion` rule stops it with `!important`, as it does the orb's bob. */
  .narration-bake.is-painting .nb-ticks::after,
  .narration-bake.is-voicing .nb-ticks::after { animation: nb-slide 1.1s linear infinite; }
  /* The microphone's three parts, re-laid as a brush: handle, ferrule, bristles. The second
     one carries the mic's own border, so it has to be taken off rather than overdrawn. */
  .narration-bake.is-painting .nb-mic { width: 16px; height: 16px; }
  .narration-bake.is-painting .nb-mic i:nth-child(1) { left: 6px; top: 0; width: 4px; height: 7px; border-radius: 2px 2px 0 0; background: currentColor; }
  .narration-bake.is-painting .nb-mic i:nth-child(2) { left: 3px; top: 6px; width: 10px; height: 5px; border: 0; border-radius: 2px; background: currentColor; }
  .narration-bake.is-painting .nb-mic i:nth-child(3) { left: 4.5px; bottom: 0; width: 7px; height: 4px; border-radius: 0 0 3px 3px; background: currentColor; }

  /* Step 2 — the read-aloud. The BAND does not change colour: only the orb and the fill do,
     which is the design's own choice and not a shortcut. A band that turns coral whole is a
     band that reads as an ALERT — coral is the buy colour and the colour a page that went
     silent already wears (`.is-quiet`, which does tint the whole band, and has to stay
     distinguishable from an ordinary second stage). */
  .narration-bake.is-voicing .nb-icon { background: #fadfd7; color: #c26a52; }
  .narration-bake.is-voicing .nb-ticks::after {
    background: repeating-linear-gradient(115deg, #d2513a 0 9px, #e0654d 9px 18px);
    background-size: 200% 100%;
  }

  /* The two settled states. Their base rules carry a `padding-right: 16px` that the band's
     own padding cannot out-rank (both (0,2,0), and the base ones come first only by file
     order), so it is re-stated rather than left to win a race. */
  .narration-bake.is-ready { padding: 11px 14px 13px; background: #eef6ef; border-top-color: #cfe2d4; }
  /* `border-color` is what the base rule sets and the band has no side borders left for it
     to land on, so the top one is named here or the quiet band is ruled off in the working
     stages' gold. */
  .narration-bake.is-quiet { padding: 11px 14px 13px; border-top-color: #f0bdaf; }
  /* The done band keeps its bar, at a gold 100%, and its readout — the design's third
     phone. The desktop pill hides both (`.is-ready .nb-ticks` and `.nb-pct` are base
     rules): there the pill is one line in a 260px flex row, and here it is a band with a
     column reserved for the number whether or not it holds one. */
  .narration-bake.is-ready .nb-ticks { display: block; }
  .narration-bake.is-ready .nb-pct { display: block; }
  .narration-bake.is-ready .nb-body { gap: 6px; }
  /* Pale green with a dark tick, not the desktop pill's solid `--green` disc: at 34px on a
     band this size a filled circle is the loudest thing on the screen, and the job it is
     reporting is FINISHED. */
  .narration-bake.is-ready .nb-icon { background: #dceae1; color: #245640; }
  .narration-bake.is-ready .nb-line { color: #245640; }

  /* …and then the band RETIRES, which on a phone is a collapse and not a disappearance:
     it is full-bleed along the bottom edge, so `hidden` alone takes a 60px grid row out in
     one frame and the footer and the pager jump into the space. `max-height` rather than
     `height`, for the reason every collapse uses it — the band's height is its content's
     and there is no number to transition from. Stated AFTER the two settled states: it ties
     with `.is-ready` on specificity and has to out-rank its padding, and file order is what
     decides that. `updateNarrationBake` adds the class one beat before it hides the band
     for good. */
  .narration-bake { overflow: hidden; max-height: 140px; transition: max-height .5s ease, opacity .4s ease, padding .5s ease; }
  .narration-bake.is-retiring { max-height: 0; opacity: 0; padding-top: 0; padding-bottom: 0; border-top-width: 0; }
  .story-dialog-header #sampleLangLabel { flex: 1 1 100%; margin: 6px 0 4px; }
  .story-dialog-header .audio-player:has(#playAllAudio[hidden]) small { display: block; flex: 1 1 100%; width: 100%; margin: 4px 12px 4px 0; }
  /* …except while casting. That line is brought back to explain a MISSING read button, and
     during a cast the reason it is missing is the transport sitting right beside it — the
     idle "press Read the story" it would carry is an instruction to press a button that is
     not on the screen. Later in the file than the rule above, which it ties on specificity
     with. */
  .story-dialog-header .audio-player:has(#castControls:not([hidden])) small { display: none; }
  .story-dialog-header .audio-player small:empty { display: none; }
  /* Full-height pill, so the bar is the same 44px tap target as the circles beside it, and
     104px rather than the desktop's 148: a flex line BREAKS before it shrinks (line breaking
     measures the hypothetical size, not the shrunk one) and `#castControls` is ONE item, so
     every pixel here decides whether the Cast button keeps its place on the row. At 104 the
     whole transport is one 44px line from 360px up — the commonest Android width — and below
     that it wraps, which is honest and costs nothing but height on a row that is `auto`. */
  .cast-vol-bar { width: 104px; height: 44px; }
  /* Bedtime is a BOTTOM SHEET here: the card's top-right anchor is the desktop header's,
     and on the phone grid there is no header box to hang from. It covers the pager, which is
     what a press outside it dismisses it for; the list scrolls inside its own cap so the
     sheet itself never grows past the book. */
  #storyDialog > .cast-panel { top: auto; right: 0; bottom: 0; left: 0; width: auto; max-height: 72dvh; overflow: auto;
    border-radius: 18px 18px 0 0; border-bottom: 0; padding: 14px 16px calc(12px + env(safe-area-inset-bottom)); }

  /* --- row 2: the spread — picture over words ------------------------------- */
  .story-reader {
    grid-area: 2 / 1 / 3 / -1; min-height: 0; align-self: stretch;
    display: flex; flex-direction: column;
    aspect-ratio: auto; width: auto; max-width: none; height: auto;
    margin: 0; padding: 0; border-radius: 0; box-shadow: none;
    border-top: 1.5px solid rgba(160,125,60,.22);
    border-bottom: 1.5px solid rgba(160,125,60,.22);
  }
  .reader-spine { display: none; }
  .story-art-page {
    flex: none; height: 42dvh; min-height: 190px; max-height: 46dvh;
    border-radius: 0;
  }
  .story-art-page.is-plate::before { font-size: clamp(64px, 20vw, 120px); }
  .story-art-painting-mark { font-size: 32px; }
  .story-art-painting { gap: 6px; padding: 20px; }
  /* ONE line, not three rotated ones wrapping across the child's face. */
  .preview-watermark { inset: 16% 0 auto; font-size: 8.5px; letter-spacing: 2.5px; white-space: nowrap; transform: none; }

  /* The words are the only scroll in the reader. `overscroll-behavior: contain` is
     the TOUCH half of the lock: `body.modal-open`'s `overflow: hidden` stops the
     wheel behind the dialog, but a drag past the end of this page still chains to
     the page underneath and rubber-bands the whole sheet on iOS. */
  .story-copy-page {
    flex: 1 1 auto; min-height: 0; height: auto;
    overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
    border-radius: 0; padding: 18px 20px 22px;
    box-shadow: inset 0 14px 20px -16px rgba(43,42,35,.28);
  }
  .story-page-topline { font-size: 8px; }
  /* No 540px cap: on a phone the column IS the measure. */
  .story-page-copy { max-width: none; margin: 0; }
  .story-page-copy h3 { margin: 8px 0 12px; font-size: 24px; }
  .story-page-copy p { font-size: 18px; line-height: 1.68; }
  .story-dropcap { font-size: 2.6em; margin: 4px 10px 0 0; }
  /* The flourish sits under the last line, not at the foot of a filled page. */
  .story-end { gap: 10px; margin-top: 18px; }
  .story-end-rule { max-width: 64px; }
  .story-end-word { font-size: 17px; }

  /* --- row 3, right: two thumbs and a bar ----------------------------------- */
  .story-reader-footer {
    grid-area: 3 / 1 / 4 / -1; min-height: 0; gap: 4px; justify-content: flex-end;
    padding: 6px 12px 6px 128px; background: none; border: 0;
  }
  .story-nav-button { flex: none; width: 48px; height: 48px; font-size: 26px; box-shadow: 0 8px 18px -10px rgba(40,25,5,.45); }
  .reader-progress { flex: 1 1 auto; min-width: 0; gap: 7px; }
  .story-progress-dots { display: flex; align-items: center; width: 100%; gap: 0; }
  .story-progress-dots button { flex: 1 1 0%; min-width: 0; width: auto; height: 5px; border-radius: 0; }
  .story-progress-dots button:first-child { border-radius: 999px 0 0 999px; }
  .story-progress-dots button:last-child { border-radius: 0 999px 999px 0; }
  .story-progress-dots button.active { flex: 0 0 auto; width: auto; min-width: 12px; height: 9px; border-radius: 999px; box-shadow: none; }
  .reader-counter { font-size: 13.5px; font-weight: 700; }
  .reader-endnote { max-width: none; font-size: 12.5px; }

  /* --- row 3 on a LOCKED book: the preview's own shelf --------------------
     Everything below is scoped to `.is-locked` (written by `updateReaderLockUI`,
     which is the one function called from all three places `state.paid` can flip)
     and so leaves a paid book's footer exactly as it was. The design's rule for
     both orientations is one line: the row is `chip · pager`, and the chip never
     wraps under the pager. */
  .reader-ra-chip { display: flex; }
  /* The 128px on the left reserves the narration group's width — and a locked book
     HAS no read button to reserve it for (`applyNarrationAvailability` hides it), so
     it is dead space that would squeeze the chip to nothing. */
  .story-dialog.is-locked .story-reader-footer { gap: 10px; padding: 8px 14px 10px; }
  /* `flex: 1 1 0`, never `auto`: portrait wraps this row on purpose (the bar takes a
     line of its own), and a wrapping flex line BREAKS before it shrinks — with a
     content-sized basis the end-state chip claimed 304px of a 362px row and pushed the
     key onto a third line, under the fold. From zero it simply fills what the two
     buttons leave. */
  .story-dialog.is-locked .reader-ra-chip { flex: 1 1 0; }
  /* The sentence the chip replaces. `#audioStatus` is still WRITTEN — three places
     compare `NARRATION_LOCKED_STATUS` by identity — and row 4 still shows it for the
     other two reasons a read button goes missing (a language the narrator cannot speak,
     a guest), which is why this hides it only while the chip is actually up. Hidden,
     never silenced: the same bargain `.has-peek` strikes on the desktop header. */
  .story-dialog.is-locked:has(#readerRaChip:not([hidden])) .story-dialog-header .audio-player small { display: none; }
  /* The page bar. On a locked book the track is four segments rather than twenty-four,
     so it is drawn as the design's seamed bar instead of collapsing into a number: a
     gold→coral fill, a dashed seam per page, and a star knob that bobs along it. Both
     pseudo-elements are the CONTAINER's, so `--p` (written by `renderStoryPage`) is one
     number for the fill and the knob and they cannot drift apart. The knob travels
     `--p × (track − knob)`, never `left: <pct>` with a negative margin, which overhangs
     the track at 100% and collides with the control beside it.
     Everything the portrait counter block does is undone here — the `::after` number,
     the block display, the clipped buttons, the hidden counter — because those four ARE
     the number, and this is the bar instead. Two classes beat that block's one whatever
     the source order, which is why these rules can sit above it. */
  .story-dialog.is-locked .story-progress-dots {
    position: relative; display: flex; align-items: stretch;
    /* `flex: 1 1 auto`, not the 100% the segmented track takes: this row wraps (the
       end-note needs a line of its own), and a 100% basis takes the page label with
       it rather than letting the bar give up the width. */
    flex: 1 1 auto; width: auto; height: 12px; gap: 0; font-size: 0;
    border: 1.5px solid #ecdfc6; border-radius: 999px; background: #f7edd9;
  }
  .story-dialog.is-locked .story-progress-dots::before {
    content: ""; position: absolute; top: -1.5px; bottom: -1.5px; left: -1.5px;
    width: calc(var(--p, 0) * 100% + 1.5px); border-radius: 999px;
    background: linear-gradient(90deg, var(--gold), var(--coral));
    pointer-events: none;
  }
  .story-dialog.is-locked .story-progress-dots::after {
    content: "✦"; position: absolute; top: 50%; left: calc(var(--p, 0) * (100% - 25px));
    display: grid; place-items: center; width: 25px; height: 25px; margin: -12.5px 0 0;
    border: 2.5px solid var(--coral); border-radius: 50%; background: #fffdf7;
    color: var(--gold); font-size: 12px; font-weight: 400; line-height: 1;
    box-shadow: 0 7px 15px -6px rgba(215,91,69,.9);
    animation: knob-bob 2.6s ease-in-out infinite; pointer-events: none;
  }
  /* The seams are the jump buttons themselves — they are still in the DOM and still
     tappable, which is what the portrait block's clipping had to preserve too. */
  .story-dialog.is-locked .story-progress-dots button,
  .story-dialog.is-locked .story-progress-dots button.active {
    position: static; flex: 1 1 0; width: auto; height: auto; min-width: 0;
    padding: 0; border: 0; border-right: 1.5px dashed rgba(255,253,247,.8);
    border-radius: 0; background: none; box-shadow: none;
    clip-path: none; overflow: visible;
  }
  .story-dialog.is-locked .story-progress-dots button:last-child { border-right: 0; }
  .story-dialog.is-locked .reader-counter { display: block; flex: none; color: #a07d3c; font-size: 12px; font-weight: 700; white-space: nowrap; }
  /* The bar stays at 100% on the last preview page — the portrait block hides the track
     there to make room for the end-note, and here the end-note is the chip's own second
     line, which says the same thing with the price on it. */
  .story-dialog.is-locked .story-reader-footer.is-end .story-progress-dots { display: flex; }
  .story-dialog.is-locked .story-reader-footer:has(.reader-ra-chip:not([hidden])) .reader-endnote { display: none; }
  /* The end-note is still the whole answer on a locked book the narrator cannot SPEAK,
     where there is no chip to carry it — and `.reader-progress` is a row here, so it
     takes a line of its own under the bar rather than being squeezed in beside it. */
  .story-dialog.is-locked .reader-progress { flex-wrap: wrap; }
  .story-dialog.is-locked .reader-endnote { flex: 1 1 100%; text-align: center; }
  /* The unlock is a KEY on a phone, not the drawn padlock: the same SVG the desktop
     pill turns in its knob, which the phone block hides along with the copy. The
     padlock rules stay where they are, under `.is-locked` — the button is hidden on
     every paid book now, so this is the only state it is ever seen in. */
  /* `padding: 0` is restated here, and it is load-bearing rather than a default said
     twice: the phone block above sets it at `#approveStory` + one class, and the desktop
     pill's `.button-unlock` rule (`8px 20px 8px 10px`, id + TWO classes) out-ranks it —
     so the 44px circle kept the pill's asymmetric padding and centred the key 5px left
     of the middle. This selector carries three classes, so it wins over both. */
  .story-dialog.is-locked .story-dialog-actions #approveStory { justify-content: center; padding: 0; }
  .story-dialog.is-locked .story-dialog-actions #approveStory::before { content: none; }
  .story-dialog.is-locked .story-dialog-actions #approveStory.button-unlock .unlock-shackle,
  .story-dialog.is-locked .story-dialog-actions #approveStory .unlock-shackle { display: none; }
  .story-dialog.is-locked .story-dialog-actions #approveStory .unlock-knob { display: grid; width: auto; height: auto; background: none; color: #fff; }
  .story-dialog.is-locked .story-dialog-actions #approveStory .unlock-short { display: none; }
  /* The key that the forward arrow becomes: the design's 46px, and the pulse that says
     it is the thing to press. It is the minimum hit target here — do not shrink it. */
  .story-nav-button.is-unlock-key { width: 46px; height: 46px; }
  .story-nav-button.is-unlock-key::after {
    content: ""; position: absolute; inset: 0; border-radius: 50%;
    border: 2px solid var(--green); animation: cta-a-pulse 2.8s ease-out infinite;
    pointer-events: none;
  }

  /* --- picture only ---------------------------------------------------------
     The words go, the picture takes the whole sheet, and the only controls left are
     the two narration buttons and the cross — which fade after a few seconds of
     stillness and return on a tap, a swipe or a moved mouse (the idle timer is in
     app.js, beside the swipe). Pages still turn on a swipe, which is what lets the
     arrows go at all. */
  .story-dialog.is-immersive { background: #0c1b24; }
  .story-dialog.is-immersive[open] { grid-template-rows: 0 1fr 0 0; }
  .story-dialog.is-immersive .reader-title,
  .story-dialog.is-immersive .reader-immersive,
  .story-dialog.is-immersive .story-dialog-actions #approveStory,
  .story-dialog.is-immersive .story-dialog-actions #editCurrentPage,
  .story-dialog.is-immersive .story-dialog-actions #reportPage,
  .story-dialog.is-immersive .story-reader-footer,
  .story-dialog.is-immersive .narration-bake,
  .story-dialog.is-immersive .story-copy-page { display: none; }
  .story-dialog.is-immersive .story-reader { border: 0; }
  .story-dialog.is-immersive .story-art-page {
    flex: 1 1 auto; width: auto; height: auto; min-height: 0; max-height: none;
  }
  /* Out of the grid and over the picture, one in each corner. */
  .story-dialog.is-immersive .audio-player {
    position: absolute; z-index: 8; left: 12px; right: auto;
    bottom: calc(12px + env(safe-area-inset-bottom, 0px)); top: auto;
    width: auto; padding: 0; transition: opacity .22s;
  }
  .story-dialog.is-immersive .dialog-close {
    position: absolute; z-index: 8; right: 12px; left: auto;
    top: calc(10px + env(safe-area-inset-top, 0px)); bottom: auto;
    transition: opacity .22s;
  }
  .story-dialog.is-immersive.chrome-hidden .audio-player,
  .story-dialog.is-immersive.chrome-hidden .dialog-close { opacity: 0; pointer-events: none; }
}

/* Portrait phone only: the bar becomes the number.
   Twenty-four segments in 138px is a decorative smear, and the counter under it was
   already saying where you were in words. So the segments go — kept in the DOM and
   clipped to 1px, because renderStoryPage rebuilds them on every render and they are
   the jump targets — and their place is taken by a plain "4 / 24".
   The two numbers are CSS counters read off the classes app.js already writes, so
   nothing in it has to change: every button counts towards the total, and `.done` —
   which renderStoryPage puts on every page BEFORE the current one — counts towards
   the page number, whose counter starts at 1.
   `.reader-counter` is hidden because "Page 4 of 24" under "4 / 24" is the same
   sentence twice; the end-note ("End of the free preview") stays, since that one says
   something else. Landscape keeps the segmented track — it has the width for it. */
@media (max-width: 760px) and (min-height: 501px) {
  .story-progress-dots {
    position: relative; display: block; width: auto; gap: 0;
    counter-reset: pgnum 1 pgtot 0;
    font-size: 15px; font-weight: 700; letter-spacing: .3px; color: #a07d3c;
    font-variant-numeric: tabular-nums;
  }
  .story-progress-dots::after { content: counter(pgnum) " / " counter(pgtot); }
  .story-progress-dots button,
  .story-progress-dots button.active {
    position: absolute; top: 0; left: 0;
    width: 1px; height: 1px; min-width: 0; flex: none;
    padding: 0; border: 0; border-radius: 0; overflow: hidden;
    clip-path: inset(50%); background: none; box-shadow: none;
    counter-increment: pgtot;
  }
  .story-progress-dots button.done { counter-increment: pgtot pgnum; }
  .reader-counter { display: none; }
  .reader-progress { flex: 0 1 auto; min-width: 0; gap: 0; }
  /* The row never wraps, and the middle column shrinks rather than claiming its
     content's width — on the last page the end-note's 138px of max-content pushed the
     › arrow onto a second line and took 52px off the story. On the last page the
     NUMBER is the redundant one ("End of the free preview" already says where you
     are), so the two swap on the `is-end` class the footer already carries. The note
     has to survive: it is the only thing that tells a preview that STOPPED from a
     story that ENDED, since the flourish on the paper says "The End" either way. */
  .story-reader-footer { flex-wrap: nowrap; }
  .story-reader-footer.is-end .story-progress-dots { display: none; }
  .story-reader-footer.is-end .reader-endnote { max-width: 100%; }

  /* A LOCKED book keeps the bar (see the block above) and therefore takes the design's
     two lines: the page bar and its label on their own row, then `chip · pager` under
     it. The wrap is what makes those two rows, and the `order`s are what put the chip
     to the LEFT of the arrows without moving anything in the DOM — reading order stays
     prev · progress · chip · next. This out-ranks the `nowrap` above by one class. */
  .story-dialog.is-locked .story-reader-footer { flex-wrap: wrap; justify-content: flex-end; row-gap: 4px; }
  .story-dialog.is-locked .reader-progress { order: -1; flex: 1 1 100%; flex-direction: row; align-items: center; gap: 13px; padding: 4px 3px 0; }
  .story-dialog.is-locked .reader-ra-chip { order: 0; }
  .story-dialog.is-locked #previousStoryPage { order: 1; }
  .story-dialog.is-locked #nextStoryPage { order: 2; }
}

/* Small phones (360–380 and the 320px stragglers). The header carries the title, the
   lock and the cross on one 64px row, so at 320px the title is what gives — it
   truncates. Everything else just tightens. */
@media (max-width: 380px) {
  .story-dialog[open] { grid-template-columns: minmax(0, 1fr) auto auto 56px; grid-template-rows: 62px 1fr auto auto; column-gap: 6px; }
  .reader-immersive { width: 42px; height: 42px; }
  .story-dialog-header .reader-title { padding-left: 13px; }
  .story-dialog-header h2 { font-size: 17px; }
  .story-dialog-actions #approveStory { width: 42px; height: 42px; min-height: 42px; }
  .story-dialog-actions #editCurrentPage,
  .story-dialog-actions #reportPage { width: 42px; height: 42px; min-height: 42px; }
  /* Both offsets come down 1px with the button, so the shackle-plus-body pair still sits
     on the centre of a 42px circle — see the 44px block for why the pair is what is
     centred rather than either half of it. */
  .story-dialog-actions #approveStory::before { top: 19px; }
  /* Both selectors again, for the reason the block above states them: the
     `.button-unlock` one is what out-ranks the desktop hide, so a rule that
     named only the bare class would leave the locked book's shackle at the
     44px button's offset inside a 42px one. */
  .story-dialog-actions #approveStory.button-unlock .unlock-shackle,
  .story-dialog-actions #approveStory .unlock-shackle { top: 10px; }
  .story-dialog .dialog-close { width: 42px; height: 42px; }
  .story-art-page { height: 36dvh; min-height: 160px; max-height: 40dvh; }
  .story-copy-page { padding: 16px 16px 20px; }
  .story-page-copy h3 { font-size: 22px; }
  .story-page-copy p { font-size: 17px; }
  .story-reader-footer { gap: 4px; padding-left: 118px; padding-right: 10px; }
  .story-nav-button { width: 44px; height: 44px; font-size: 24px; }
  .audio-player #playAllAudio { width: 44px; height: 44px; }
  /* The custom property and not the shorthand: the status band cancels this padding with a
     negative margin of its own, and a `padding-right` written straight in here would leave
     the band 10px short of the right edge at this width alone. */
  .story-dialog-header .audio-player { gap: 6px; --ap-pad-r: 10px; }
}

/* Phone on its side. 42dvh of picture is a letterbox strip on a 390px-tall viewport
   and the words get four lines — so this is the one place on a phone where the spread
   is a spread again: picture left, words right, both full height. It has to come
   AFTER the block above: at 667×375 both queries match at equal specificity and file
   order is what decides. 844×390 is width to spare and no height at all, so the
   header row carries the lot (title · ▶ · ⏹ · lock · ✕) and the arrows keep the last
   row to themselves. Same grid, re-placed. */
@media (max-width: 900px) and (max-height: 500px) {
  .story-dialog[open] {
    grid-template-columns: minmax(0, auto) minmax(100px, 1fr) auto auto 60px;
    grid-template-rows: 56px 1fr auto auto;
    gap: 0 10px;
  }
  .story-dialog-header .reader-title { grid-area: 1 / 1; padding: 0 0 0 14px; }
  .story-dialog-header h2 { font-size: 17px; }
  /* Left-aligned, sitting straight after the title: column 2 is the one that takes
     the slack here (the title's track is `auto`), so the buttons stay beside the name
     of the book instead of drifting to the middle of the bar. */
  .story-dialog-header .audio-player,
  /* The three conditional placements above are re-stated, not inherited: each carries an
     id inside a `:has()` and therefore outranks a bare `.story-dialog-header .audio-player`
     at any source position, so a landscape phone with a bake running — or a locked one with
     only the status line — would otherwise drop the player into the portrait sheet's row 4
     while everything around it stayed on the header row. Equal specificity, later in the
     file, so these win. */
  .story-dialog-header .audio-player:has(#narrationBake:not([hidden])),
  .story-dialog-header .audio-player:has(#castControls:not([hidden])),
  .story-dialog-header .audio-player:has(#sampleLangLabel:not([hidden])),
  .story-dialog-header .audio-player:has(#playAllAudio[hidden]) { grid-area: 1 / 2 / 2 / 3; justify-self: start; align-self: center; flex-wrap: nowrap; padding: 0; }
  /* Width to spare here, so the notice row's contents ride the header row inline
     rather than wrapping under it — a wrapped line would grow a 56px band on a
     390px-tall screen and take it off the book. */
  /* A PILL again, not the portrait band. The block above makes it full-bleed with a rule
     along its top, which is right for something sitting on the bottom edge of the screen
     and wrong for something riding inline on a 56px header row — there it is a flat slab
     with a stray line over it, and it is taller than the row it is in. Stated in full
     (border, radius, padding) rather than left to inherit: the portrait rules zero the
     side borders and the radius, so nothing here can come back on its own. */
  .narration-bake {
    flex: 0 1 auto; width: auto; margin: 0;
    padding: 6px 12px; border: 1.5px solid #e9dcbe; border-radius: 999px;
  }
  .narration-bake.is-ready, .narration-bake.is-quiet { padding: 6px 12px; }
  .narration-bake.is-ready { border-color: #cfe2d4; }
  .narration-bake.is-quiet { border-color: #f0bdaf; }
  .narration-bake .nb-ticks { width: 96px; }
  .story-dialog-header #sampleLangLabel { flex: 0 1 auto; margin: 0; }
  .story-dialog-header .audio-player:has(#playAllAudio[hidden]) small { flex: 0 1 auto; width: auto; margin: 0; }
  .reader-immersive { grid-area: 1 / 3; justify-self: auto; }
  .story-dialog-actions #approveStory { grid-area: 1 / 4; margin: 0; }
  .story-dialog-actions #editCurrentPage,
  .story-dialog-actions #reportPage { grid-area: 1 / 4; margin: 0; }
  .story-dialog .dialog-close { grid-area: 1 / 5; }
  .story-reader { grid-area: 2 / 1 / 3 / -1; flex-direction: row; }
  /* Wider than the copy half: a landscape phone is the one shape where the picture
     can be the size it deserves, and 15.5px story text does not need 56% of 844px. */
  .story-art-page { flex: 0 0 54%; width: 54%; height: auto; min-height: 0; max-height: none; }
  /* The watermark is a child of the SPREAD, so side by side it lies across the words
     as well as the picture — 46% off the right keeps it on the art half, which is the
     only half it was ever marking. */
  .preview-watermark { inset: 42% 46% auto 0; }
  .story-copy-page { padding: 14px 18px 16px; box-shadow: inset 10px 0 18px -14px rgba(43,42,35,.28); }
  .story-page-copy h3 { margin: 4px 0 8px; font-size: 20px; }
  .story-page-copy p { font-size: 15.5px; line-height: 1.6; }
  .story-dropcap { font-size: 2.3em; }
  .story-reader-footer { grid-area: 3 / 1 / 4 / -1; justify-content: center; gap: 12px; padding: 4px 14px 6px; }
  .story-nav-button { width: 44px; height: 44px; font-size: 24px; }
  .reader-counter { font-size: 12.5px; }

  /* --- a LOCKED book on its side -------------------------------------------
     The design's landscape footer is ONE row — `‹ · chip · bar · label · ›` — and
     the chip is sized to its content while the bar absorbs the slack. If both could
     shrink, the shortage would come silently off the copy. There is width here for a
     single line, so the end-state sub-line does not wrap either. */
  .story-dialog.is-locked .story-reader-footer { flex-wrap: nowrap; justify-content: flex-start; gap: 12px; padding: 4px 16px 6px; }
  .story-dialog.is-locked #previousStoryPage { order: -2; }
  .story-dialog.is-locked .reader-ra-chip { order: -1; flex: none; }
  .story-dialog.is-locked .reader-progress { order: 0; flex: 1 1 auto; flex-direction: row; align-items: center; gap: 10px; padding: 0; }
  .story-dialog.is-locked #nextStoryPage { order: 1; }
  .story-dialog.is-locked .reader-ra-chip.is-end .ra-txt small { white-space: nowrap; }
  /* The header's unlock is the design's PILL here rather than the portrait key disc:
     there is width to spare on a phone's long side and no room at all in height, so the
     action says its price on the one line it has (`.unlock-short`, written by
     `updateReaderLockUI`) instead of hiding it behind a padlock. The two-line
     `.unlock-copy` stays in the accessibility tree; only its paint is swapped. */
  .story-dialog.is-locked .story-dialog-actions #approveStory {
    width: auto; height: 42px; min-height: 42px; justify-content: center;
    gap: 8px; padding: 0 16px 0 13px; border-radius: 999px;
    font-family: var(--sans); font-size: 13.5px; font-weight: 800;
  }
  .story-dialog.is-locked .story-dialog-actions #approveStory .unlock-short { display: block; white-space: nowrap; }
  .story-dialog.is-locked .story-dialog-actions #approveStory .unlock-copy { display: none; }
  /* The bar is the only item in that row that can give, so it needs a floor. */
  .story-dialog.is-locked .story-progress-dots { min-width: 72px; }
}

/* A SMALL phone on its side. The landscape row is five items and the bar absorbs all of
   the slack by design — which is 150px of bar on a 667×375 and was 51px on a 568×320, a
   stub with a knob sitting on it. So there is an order in which they give: the page label
   goes first (the design's own last-preview frame drops it too, and the chip and the bar
   between them already say where the reader is), then the chip's sub-line ellipsises, and
   the bar keeps the 72px floor above under both. Portrait is untouched — it has the two
   rows to spend and reaches none of this. */
@media (max-width: 700px) and (max-height: 500px) {
  .story-dialog.is-locked .reader-counter { display: none; }
  .story-dialog.is-locked .reader-ra-chip { flex: 0 1 auto; }
  .story-dialog.is-locked .reader-ra-chip .ra-txt small,
  .story-dialog.is-locked .reader-ra-chip.is-end .ra-txt small { overflow: hidden; text-overflow: ellipsis; }
  .story-dialog.is-immersive[open] { grid-template-rows: 0 1fr 0 0; }
}

/* ── Currency picker & single-book pricing ─────────────────────────────────
   Prices are quoted in USD and converted for display (see /api/pricing), so the
   header carries a switcher and the pricing cards are painted from the server
   rather than from the markup. The card styling deliberately echoes .plan-card
   so memberships and single books read as one price list. */
/* ── Pricing: one row, once-off book | club ────────────────────────────────
   The single-book card and the three plan cards share a row so the choice reads
   as "one book OR a membership". These rules land after the older
   .membership-section / .plan-* block above and deliberately restyle it. */
/* The navy band above DIPS into this one, which is the other half of the rhythm's
   fourth device and the reason neither section is arched any more. `.pricing-seam`
   sits INSIDE this section and is painted in the band ABOVE it (`--band-gift`) —
   the one wave here that is not filled from its own section's token, because a dip
   is the neighbour spilling down rather than this band rising up; filled from
   `--band-pricing` it would be sand on sand and paint nothing (see the `--band-*`
   block). The extra 14px of top padding is what clears it. */
.pricing-section { position: relative; overflow-x: clip; padding: calc(var(--band-pad) + 14px) 0 var(--band-pad); background: var(--band-pricing); }
.pricing-seam { position: absolute; left: 0; top: -1px; display: block; width: 100%; height: 58px; }
.pricing-seam path { fill: var(--band-gift); }
.pricing-twinkle { position: absolute; font-size: 18px; animation: price-twinkle 3.6s ease-in-out infinite; pointer-events: none; }
/* Stated off `--band-pad` rather than as a percentage of the band: at 7% of a tall
   price list the first two sat INSIDE the navy dip, which is a gold ✦ on navy in the
   one place the eye is reading a seam. Both clear it at every width the pad takes. */
.pricing-twinkle.t1 { top: calc(var(--band-pad) + 26px); left: 5%; color: var(--gold); }
.pricing-twinkle.t2 { top: calc(var(--band-pad) + 52px); right: 6%; color: #eb9d8a; font-size: 24px; animation-duration: 4.4s; animation-delay: .6s; }
.pricing-twinkle.t3 { bottom: 11%; left: 8%; color: #8fbfa0; font-size: 15px; animation-duration: 3.1s; animation-delay: 1.1s; }
@keyframes price-twinkle { 0%, 100% { opacity: .3; transform: scale(.84) rotate(0deg); } 50% { opacity: 1; transform: scale(1.16) rotate(16deg); } }

.pricing-section .section-heading { position: relative; max-width: 780px; }
/* Flat lettering where a pill used to sit, and no swash under the em: the design keeps
   this band's heading plain, and the coral italic is the only accent it carries. */
.pricing-kicker { display: inline-block; color: #a4906f; font-size: 11px; font-weight: 800; letter-spacing: 2.2px; text-transform: uppercase; }
.pricing-section h2 { margin-top: 12px; font-size: clamp(30px, 3.6vw, 40px); line-height: 1.1; letter-spacing: -.8px; text-wrap: balance; }
.pricing-section h2 em { color: var(--coral); font-style: italic; }
.pricing-section .section-heading p { max-width: 620px; margin-inline: auto; color: #8a7a5f; font-family: var(--display); font-style: italic; font-size: 16.5px; line-height: 1.55; }

/* ── The three tickets ─────────────────────────────────────────────────────
   "Pricing Packages.dc.html", concept 1a: the once-off book and the two memberships
   stand in ONE row as library tickets — a coloured spine with its perforation down the
   left edge, a tinted header block over a dashed foot, and a torn tear strip hanging
   off the right. Four things are load-bearing here. (1) The tracks are
   `minmax(0, …)`: a bare `1fr` is `minmax(auto, 1fr)`, and the format notes ("24 pages
   · soft-touch cover · includes the digital book") have a real min-content width that
   grows in the other ten languages — with `.pricing-section` clipping its X axis that
   arrives as a SILENT crop of the rightmost card, which is exactly the bug the gift
   band's `.fg-editions` already paid for. (2) Every piece of the ticket chrome is a
   pseudo-element, so the template gained no decorative node and `app.js` still finds
   every hook where it was. (3) A card's colours are three custom properties, set once
   per card, so a tier is one block rather than five overrides. (4) The tilt is stated
   on the element (`--tk-r`) and never in a keyframe — the global reduced-motion block
   kills animations, and a ticket whose only tilt lived in a frame would sit straight. */
.pricing-row {
  position: relative; display: grid; gap: 26px; align-items: stretch; margin-top: 46px;
  grid-template-columns: minmax(0, 1.06fr) minmax(0, 1fr) minmax(0, 1fr);
}
.pricing-row .ticket {
  --tk-line: #ecdcbb; --tk-spine: var(--gold); --tk-head: #fdf3e0; --tk-stripe: #f6eacd; --tk-r: 0deg;
  position: relative; min-height: 0; display: flex; flex-direction: column; align-items: stretch;
  padding: 0 0 24px 18px; border: 3px solid var(--tk-line); border-radius: 16px 26px 24px 16px;
  background: var(--paper); color: var(--ink); text-align: left;
  box-shadow: 0 22px 40px -26px rgba(40,25,5,.45); transform: rotate(var(--tk-r));
}
/* The spine and its perforation are ONE box: the dotted line is a repeating gradient
   painted over the spine's own colour, because a second absolutely-placed span would be
   a decorative node in the markup this section deliberately does not carry. */
.pricing-row .ticket::before {
  content: ""; position: absolute; left: -1px; top: -1px; bottom: -1px; width: 18px;
  border-radius: 14px 0 0 14px; background-color: var(--tk-spine);
  background-image: repeating-linear-gradient(to bottom, rgba(255,253,247,.9) 0 5px, transparent 5px 9px);
  background-repeat: no-repeat; background-size: 2px calc(100% - 60px); background-position: 6px 30px;
}
/* The tear strip hangs 12px OUTSIDE the card, and `.pricing-section` clips its X axis —
   the shell's own 24px gutter is what keeps the rightmost one on the page. */
.pricing-row .ticket::after {
  content: ""; position: absolute; right: -12px; top: 16px; bottom: 16px; width: 12px;
  border: 3px solid var(--tk-line); border-left: 0; border-radius: 0 9px 9px 0;
  background: repeating-linear-gradient(to bottom, var(--paper) 0 5px, var(--tk-stripe) 5px 7px);
}
.pricing-row .ticket > header {
  padding: 22px 22px 18px; border-radius: 0 22px 0 0;
  background: var(--tk-head); border-bottom: 3px dashed var(--tk-line);
}
.pricing-row .ticket-body { flex: 1; min-width: 0; display: flex; flex-direction: column; padding: 18px 22px 0 14px; }

/* The once-off card is PAPER like the two beside it — a dark card in that row said "this
   is the other kind of thing" about the option most visitors take — and it is the green
   ticket, so the three spines read left to right as book, silver, gold. */
/* Written `.pricing-row .single-book-card` and not the bare class: the ticket defaults
   above are `.pricing-row .ticket`, so a one-class selector loses the cascade and the
   green ticket comes out gold — a custom property is resolved by specificity like any
   other declaration. Same reason the two plan tiers are qualified. */
.pricing-row .single-book-card { --tk-line: #b3cdaf; --tk-spine: var(--green); --tk-head: #e2eedd; --tk-stripe: #e8f0e4; --tk-r: -1.1deg; }
.single-book-card header small { display: block; color: #45704f; font-size: 10.5px; font-weight: 800; letter-spacing: 1.6px; text-transform: uppercase; }
.single-book-card h3 { margin: 6px 0 0; font-family: var(--display); font-size: 28px; font-weight: 400; line-height: 1.04; }

.single-format-list { display: flex; flex-direction: column; gap: 9px; margin-top: 16px; }
.single-format-row {
  display: flex; align-items: center; gap: 11px; width: 100%; padding: 12px 13px;
  border: 1.5px solid #ecdcbb; border-radius: 16px; background: #fdf8ec;
  color: inherit; text-align: left; cursor: pointer; transition: background .16s, border-color .16s;
}
.single-format-row:hover { border-color: #e6bd3f; }
.single-format-row.selected { border-color: var(--gold); background: #fff3d6; }
.single-format-dot {
  width: 21px; height: 21px; flex: none; display: grid; place-items: center; border-radius: 7px;
  border: 1.5px solid #d9c8a4; background: var(--paper); color: transparent; font-size: 11px; font-weight: 800;
}
.single-format-row.selected .single-format-dot { border-color: var(--gold); background: var(--gold); color: var(--ink); }
.single-format-body { flex: 1; min-width: 0; }
.single-format-name { display: block; font-size: 14.5px; font-weight: 800; }
.single-format-note { display: block; margin-top: 2px; color: #8a7a5f; font-size: 12px; line-height: 1.4; }
.single-format-row > b { font-family: var(--display); font-size: 20px; font-weight: 700; }
/* A printed edition includes the digital book, so the digital row's own price is struck
   through and the word stands beside it. The `<b>` is left alone on purpose — renderPrices
   writes the money into it on every currency change, so a state that rewrote it would be
   silently undone the next time a price moved. */
.single-format-free { display: none; color: #6f8f7d; font-size: 12px; font-weight: 800; letter-spacing: .4px; text-transform: uppercase; }
.single-format-row.is-included .single-format-free { display: inline; }
.single-format-row.is-included > b { font-size: 15px; text-decoration: line-through; opacity: .42; }
.single-format-hint { display: block; margin-top: 10px; color: #8a7a5f; font-size: 12px; line-height: 1.5; }
/* `hidden` loses to the `display: block` above, the same reason `.addon-card[hidden]` and
   `.kc-star[hidden]` exist — renderPrices hides this line when no printed format ships. */
.single-format-hint[hidden] { display: none; }

.single-book-foot { margin-top: auto; padding-top: 18px; }
.single-total-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 14px; border-top: 2.5px dotted #e6d2ab; }
.single-total-row span { color: #a4906f; font-size: 12.5px; font-weight: 800; letter-spacing: 1.4px; text-transform: uppercase; }
.single-total-row strong { font-family: var(--display); font-size: 30px; font-weight: 400; }
/* Green, not coral: on this row the three CTAs carry their own ticket's colour, so the
   spine, the header block and the button of a card are one thing. */
.single-book-cta { width: 100%; min-height: 52px; gap: 10px; margin-top: 14px; border-radius: 999px; background: var(--green); color: #f4faf3; font-size: 15px; box-shadow: 0 14px 26px -18px rgba(40,85,60,.9); }
.single-book-cta:hover { background: #3b6150; }

.pricing-foot { margin: 34px 0 0; text-align: center; color: #a4906f; font-family: var(--display); font-style: italic; font-size: 14.5px; }

/* The two membership tickets. Silver is the steel one and Gold keeps `featured` — the
   ribbon went with the design's own banner-less card, so the gold spine, the gold border
   and the heavier shadow are what say which one is the loud card. */
.pricing-row .plan-card { --tk-line: #c4cbd0; --tk-spine: #909aa1; --tk-head: #e8ebed; --tk-stripe: #eaedef; --tk-r: 0.9deg; }
.pricing-row .plan-card.featured {
  --tk-line: var(--gold); --tk-spine: #c8901b; --tk-head: #fdeec9; --tk-stripe: #f6eacd; --tk-r: -0.7deg;
  min-height: 0; padding-top: 0; background: var(--paper); color: var(--ink);
  box-shadow: 0 28px 46px -24px rgba(40,25,5,.55);
}
/* The eyebrow, the tier and the price are inside the ticket's header block now, so these
   are `header small` / `header strong` and NOT the `> small` / `> strong` they were: a
   child selector over the wrapper matches nothing and the card loses its coral eyebrow
   and its display-face price while still looking mostly right. */
.pricing-row .plan-card header small { display: block; margin: 0; color: #5b6770; font-size: 11px; font-weight: 800; letter-spacing: 1.3px; text-transform: uppercase; }
.pricing-row .plan-card.featured header small { color: #9a7c35; }
.pricing-row .plan-card header h3 { margin: 4px 0 0; font-family: var(--display); font-size: 28px; font-weight: 400; line-height: 1.04; }
.pricing-row .plan-card header strong { display: flex; align-items: baseline; gap: 6px; margin-top: 6px; font-family: var(--display); font-size: 36px; font-weight: 700; line-height: 1; color: #4f5b64; }
.pricing-row .plan-card.featured header strong { color: #8a580c; }
.pricing-row .plan-card header strong em { color: #79848b; font-family: var(--sans); font-size: 13.5px; font-style: normal; font-weight: 700; }
.pricing-row .plan-card.featured header strong em { color: #97803f; }
.pricing-row .plan-card ul {
  gap: 11px; padding: 16px 0 0; margin: 18px 0 24px; border-top: 2.5px dotted #d5dbdf;
  color: #4a4130; font-size: 14px; line-height: 1.45;
}
.pricing-row .plan-card.featured ul { border-top-color: #ffd9a0; }
.pricing-row .plan-card li { display: flex; align-items: flex-start; gap: 9px; }
/* The books-a-month line hides when the tier is uncapped, and `hidden` alone loses to the
   `display: flex` above — the same reason .addon-card[hidden] and .kc-star[hidden] exist.
   Without this the card ships an empty stamped chip with no way to see why. */
.pricing-row .plan-card li[hidden] { display: none; }
.pricing-row .plan-card li::before {
  content: "✦"; flex: none; margin: 0; color: #939ea6; font-size: 12px; line-height: 1.5; font-weight: 400;
  width: auto; height: auto; display: inline; background: none; border-radius: 0;
}
.pricing-row .plan-card.featured li::before { color: var(--gold); }
/* The allowance is a STAMPED CHIP rather than one more ✦ line — it is the admin's own
   number and the one line here that can vanish. It stays an `<li>` inside the list on
   purpose: `renderPrices` owns its `textContent` and its `hidden`, and the subscribe
   modal builds its feature list by copying every non-hidden `<li>` off this card, so a
   chip lifted out of the list would drop the allowance out of the modal with it. */
.pricing-row .plan-card li.plan-books-chip {
  align-items: center; gap: 8px; margin-bottom: 5px; padding: 9px 12px;
  border: 1.5px dashed #ccd3d8; border-radius: 8px 14px 9px 13px; background: #f4f6f7;
  color: #5b6770; font-size: 12.5px; font-weight: 700; line-height: 1.3;
}
.pricing-row .plan-card.featured li.plan-books-chip { border-color: #e6bd3f; background: #fff3d6; color: #7a6a4f; }
.pricing-row .plan-card li.plan-books-chip::before {
  content: "📖"; display: grid; place-items: center; width: 22px; height: 26px;
  border-radius: 3px 6px 5px 3px; background: #a3adb4; font-size: 12px; line-height: 1;
}
.pricing-row .plan-card.featured li.plan-books-chip::before { background: var(--gold); }
.pricing-row .plan-card .button {
  width: 100%; margin-top: auto; padding: 0 14px; min-height: 50px; border-radius: 999px;
  background: var(--blue); color: #f2f8f9; font-size: 15px; box-shadow: none;
}
.pricing-row .plan-card .button:hover { background: #164e69; }
.pricing-row .plan-card.featured .button { background: var(--gold); color: #3a2a06; box-shadow: 0 14px 26px -18px rgba(180,120,20,.9); }
.pricing-row .plan-card.featured .button:hover { background: #d99a20; }

@media (max-width: 1080px) {
  /* Three tickets no longer fit side by side, and the tilts go with the row: a rotated
     card in a single column reads as a mistake rather than as a stack of tickets. */
  .pricing-row { grid-template-columns: minmax(0, 1fr); gap: 22px; max-width: 560px; margin-inline: auto; }
  .pricing-row .ticket { --tk-r: 0deg; }
}

/* "Not happy?" dialog — the rewrite dialog's chrome with a page picker above the message. */
.edit-page-dialog label { display: block; font-weight: 700; margin: 10px 0 6px; }
.edit-page-dialog select { width: 100%; padding: 10px 12px; margin-bottom: 10px; border: 1px solid var(--border, #dcd6c8); border-radius: 12px; font: inherit; background: var(--paper, #fff); }

/* ============================================================================
   "Not happy?" — the family's fix-it note (redesigned 2026-09-02, from Claude
   Design's "Not Happy Modal").

   It carries its OWN class and shares nothing with `.edit-page-dialog`: that
   class dresses the ADMIN rewrite dialog (L1971 and L6432), and a two-column
   1030px layout hung on it would have taken that dialog with it.

   This block is at the END of the sheet on purpose — the reader/book-spread
   redesign block above overrides earlier rules, so anything landing before it
   loses. Everything here is scoped to `.report-dialog`.
   ============================================================================ */

.report-dialog {
  width: min(1030px, calc(100% - 24px));
  max-height: min(820px, calc(100vh - 36px));
  padding: 0;
  position: relative;
  border: 2px solid var(--ink);
  border-radius: 14px 26px 16px 24px;
  background: var(--paper);
  box-shadow: 10px 12px 0 rgba(13, 47, 67, .28), 0 30px 70px rgba(13, 47, 67, .35);
  font-family: var(--sans);
}
.report-dialog::backdrop { background: rgba(13, 47, 67, .62); }

/* `[open]` is not optional — the UA stylesheet's `display: none` on a CLOSED
   dialog is what a bare `display: flex` here would override, painting the panel
   over the page at all times. Spelled the way `.story-dialog[open]` and
   `.sub-dialog[open]` are, and for the same reason. */
.report-dialog[open] { display: flex; flex-direction: column; overflow: hidden; }

.report-tab {
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%) rotate(-2deg);
  padding: 4px 26px; border: 2px solid var(--ink); border-radius: 3px;
  background: var(--gold-light); color: var(--ink);
  font-size: 11px; font-weight: 800; letter-spacing: .18em; text-transform: uppercase;
  box-shadow: 3px 3px 0 rgba(13, 47, 67, .22); z-index: 4;
}

.report-dialog .dialog-close {
  position: absolute; top: 14px; right: 16px; width: 38px; height: 38px;
  border: 2px solid var(--ink); border-radius: 12px 9px 13px 8px;
  background: var(--paper); color: var(--ink);
  font-size: 18px; line-height: 1; cursor: pointer; z-index: 5;
  box-shadow: 2px 3px 0 rgba(13, 47, 67, .26); transform: rotate(3deg);
  transition: background .16s, color .16s;
}
.report-dialog .dialog-close:hover { background: var(--coral); color: var(--paper); }

.report-head {
  flex: none; position: relative; padding: 50px 28px 18px;
  background: var(--cream); border-bottom: 3px dashed var(--band-rule);
  overflow-x: clip;
}
.report-head .kicker { margin: 0 0 6px; letter-spacing: 2.7px; }
.report-head h2 {
  margin: 0 0 8px; max-width: 26ch;
  font-family: var(--display); font-weight: 400; font-size: clamp(25px, 4.4vw, 36px); line-height: 1.03;
  color: var(--ink);
}
.report-lede { margin: 0; max-width: 58ch; color: var(--ink-soft); font-size: 14px; line-height: 1.55; text-wrap: pretty; }

.report-stars { position: absolute; right: 74px; top: 46px; display: flex; gap: 10px; color: var(--gold); font-size: 20px; }
.report-stars .report-star:nth-child(2) { font-size: 13px; }
.report-stars .report-star:nth-child(3) { font-size: 26px; }
.report-star { animation: report-twinkle 2.6s ease-in-out infinite; }
.report-star:nth-child(2) { animation-delay: .5s; }
.report-star:nth-child(3) { animation-delay: 1.1s; }
.report-star:nth-child(4) { animation-delay: 1.6s; }
@keyframes report-twinkle { 0%, 100% { opacity: .35; transform: scale(.85); } 50% { opacity: 1; transform: scale(1.15); } }
@keyframes report-pop { 0% { transform: scale(.6) rotate(-12deg); opacity: 0; } 60% { transform: scale(1.12) rotate(4deg); } 100% { transform: scale(1) rotate(-3deg); opacity: 1; } }

.report-body { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) 340px; }
.report-body[hidden] { display: none; }

.report-pick { padding: 20px 20px 24px 26px; overflow-y: auto; }
.report-pick-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 16px; }
.report-tag {
  margin: 0; padding: 3px 12px; border: 2px solid var(--ink); border-radius: 8px 14px 9px 13px;
  background: var(--gold-light); color: var(--ink);
  font-family: var(--display); font-size: 18px; transform: rotate(-1.5deg);
}
.report-count { margin: 0; color: var(--ink-soft); font-size: 12px; }

.report-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 16px; padding: 10px 10px 0 0; }

/* The tilt is per tile and settles when it is picked — the card straightens up and the
   shadow hardens, so "ticked" reads from across a room and not only from the ✓. */
.report-tile {
  position: relative; display: block; padding: 7px 7px 18px; text-align: left; cursor: pointer;
  border: 2px solid var(--ink); border-radius: 12px 20px 13px 18px;
  background: var(--paper); box-shadow: 3px 4px 0 rgba(13, 47, 67, .2);
  transition: transform .18s ease, box-shadow .18s ease, background .18s ease;
}
.report-tile:nth-child(4n+1) { transform: rotate(-2.4deg); border-radius: 12px 20px 13px 18px; }
.report-tile:nth-child(4n+2) { transform: rotate(1.8deg); border-radius: 18px 11px 20px 12px; }
.report-tile:nth-child(4n+3) { transform: rotate(-1.2deg); border-radius: 14px 22px 10px 19px; }
.report-tile:nth-child(4n+4) { transform: rotate(2.6deg); border-radius: 20px 13px 17px 11px; }
.report-tile:hover { transform: rotate(0deg) translateY(-4px) scale(1.03); z-index: 2; }
.report-tile.is-picked { background: var(--gold); box-shadow: 5px 6px 0 var(--ink); transform: rotate(0deg); }
.report-tile:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }

.report-tile-frame { position: relative; display: flex; flex-direction: column; width: 100%; aspect-ratio: 3 / 4; border: 1.5px solid var(--ink); border-radius: 4px; overflow: hidden; }
.report-tile-art { flex: none; height: 58%; background-size: cover; background-position: center; background-color: var(--blue-light); }
.report-tile-art.is-empty { background: repeating-linear-gradient(135deg, var(--cream) 0 7px, var(--blue-light) 7px 14px); }
/* Still being painted — a soft pulse, never the hatch. The hatch means "nothing is
   coming and you may say so"; these two states looking alike is a family complaining
   about a picture that arrives thirty seconds later. */
.report-tile-art.is-pending { background: var(--cream); animation: report-breathe 1.9s ease-in-out infinite; }
@keyframes report-breathe { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }
.report-tile.is-pending { cursor: default; opacity: .62; }
.report-tile.is-pending:hover { transform: none; }
.report-tile.is-pending .report-tile-tick { display: none; }
.report-tile-text { flex: 1; padding: 5px 6px; background: var(--paper); color: var(--ink-soft); font-size: 7px; line-height: 1.35; overflow: hidden; }

.report-tile-tick {
  position: absolute; top: -9px; right: -9px; width: 26px; height: 26px;
  display: grid; place-items: center; border: 2px solid var(--ink); border-radius: 9px 7px 10px 6px;
  background: var(--paper); color: rgba(13, 47, 67, .22);
  font-size: 14px; line-height: 1; transform: rotate(6deg);
  box-shadow: 2px 2px 0 rgba(13, 47, 67, .26);
  transition: background .18s ease, color .18s ease, transform .18s ease;
}
.report-tile.is-picked .report-tile-tick { background: var(--coral); color: var(--paper); transform: rotate(-8deg); }
.report-tile-label { position: absolute; left: 8px; bottom: 3px; color: var(--ink-soft); font-size: 10px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; }
.report-tile.is-picked .report-tile-label { color: var(--ink); font-weight: 700; }

.report-say {
  display: flex; flex-direction: column; gap: 15px;
  padding: 20px 24px 28px; overflow-y: auto;
  background: var(--cream); border-left: 3px dashed var(--band-rule);
}
.report-tally {
  padding: 11px 14px 12px; border: 2px solid var(--ink); border-radius: 12px 20px 13px 18px;
  background: var(--paper); box-shadow: 4px 5px 0 rgba(13, 47, 67, .16);
}
.report-tally .eyebrow { margin: 0 0 3px; }
.report-tally-count { margin: 0; font-family: var(--display); font-size: 21px; color: var(--ink); }
.report-tally-hint { margin: 5px 0 0; color: var(--ink-soft); font-size: 12.5px; line-height: 1.5; text-wrap: pretty; }

.report-sub { margin: 0 0 9px; font-family: var(--display); font-size: 17px; color: var(--ink); }
.report-reasons .chip-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.report-reasons .choice-chip { min-height: 32px; padding: 0 11px; border: 2px solid var(--ink); font-size: 12px; background: var(--paper); }
.report-reasons .choice-chip:nth-child(odd) { border-radius: 9px 15px 8px 14px; transform: rotate(-1.2deg); }
.report-reasons .choice-chip:nth-child(even) { border-radius: 14px 9px 15px 8px; transform: rotate(1.2deg); }
.report-reasons .choice-chip.selected { background: var(--gold-light); color: var(--ink); box-shadow: 3px 3px 0 var(--ink); }

.report-note { margin: 0; }
.report-note textarea { border: 2px solid var(--ink); border-radius: 10px 16px 11px 15px; background: var(--paper); font-size: 13px; }

.report-send { margin-top: auto; display: flex; flex-direction: column; gap: 9px; }
.report-send .button { justify-content: center; width: 100%; border: 2px solid var(--ink); box-shadow: 4px 5px 0 rgba(13, 47, 67, .3); transition: opacity .15s ease, box-shadow .15s ease; }
.report-send .button:disabled { opacity: .4; box-shadow: none; cursor: not-allowed; }
.report-send-help { margin: 0; color: var(--ink-soft); font-size: 11.5px; text-align: center; text-wrap: pretty; }

.report-done { position: relative; padding: 44px 28px 50px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 12px; overflow: hidden; }
.report-done[hidden] { display: none; }
/* "Tick every page that needs another go" over "Thank you — we are on it!" is the header
   arguing with the panel under it. The kicker and the heading still name the dialog; the
   instruction is spent the moment the note is sent. */
.report-dialog:has(.report-done:not([hidden])) .report-lede { display: none; }
.report-done-stars { position: absolute; inset: 0; pointer-events: none; color: var(--gold); font-size: 18px; }
.report-done-stars .report-star { position: absolute; }
.report-done-stars .report-star:nth-child(1) { left: 12%; top: 18%; }
.report-done-stars .report-star:nth-child(2) { right: 14%; top: 26%; font-size: 26px; }
.report-done-stars .report-star:nth-child(3) { left: 22%; bottom: 16%; font-size: 13px; color: var(--coral); }
.report-done-stars .report-star:nth-child(4) { right: 20%; bottom: 22%; color: var(--coral); }
.report-done-badge {
  width: 74px; height: 74px; display: grid; place-items: center;
  border: 2px solid var(--ink); border-radius: 14px 22px 15px 20px;
  background: var(--gold-light); font-size: 32px;
  box-shadow: 4px 5px 0 rgba(13, 47, 67, .22); animation: report-pop .5s ease-out both;
}
.report-done h3 { margin: 8px 0 0; font-family: var(--display); font-weight: 400; font-size: 27px; color: var(--ink); }
.report-done-summary { margin: 0; max-width: 48ch; color: var(--ink-soft); font-size: 14px; line-height: 1.55; text-wrap: pretty; }
.report-done .button { margin-top: 12px; border: 2px solid var(--ink); box-shadow: 3px 4px 0 rgba(13, 47, 67, .28); }

/* The phone. The card is the screen, the two columns become one, and the tiles
   drop to three across — this site is phone-first everywhere else and a 356px
   sidebar beside a page grid is not a layout at 360px. */
@media (max-width: 860px) {
  /* One column, and ONE scroller. Left as a two-row grid the picker and the note
     split the card's height between them and the tiles collapse to a sliver — the
     stacked card has to scroll as a single sheet of paper. */
  .report-body { display: block; overflow-y: auto; }
  .report-pick, .report-say { overflow: visible; }
  .report-say { border-left: 0; border-top: 3px dashed var(--band-rule); }
  .report-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 15px 13px; }
}
@media (max-width: 640px) {
  .report-dialog { width: calc(100% - 14px); max-height: calc(100vh - 20px); border-radius: 12px 20px 13px 18px; }
  .report-head { padding: 46px 18px 16px; }
  .report-stars { display: none; }
  .report-lede { font-size: 13px; }
  .report-pick { padding: 16px 16px 20px; }
  .report-say { padding: 16px; }
  .report-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .report-tile-text { font-size: 8px; }
  .report-done { padding: 34px 18px 40px; }
}

/* A tilt that redraws on hover is motion, and the tiles are a picker rather than
   decoration — a family who has asked for less of it still gets the ✓ and the fill. */
@media (prefers-reduced-motion: reduce) {
  .report-tile, .report-tile:hover, .report-tile.is-picked,
  .report-reasons .choice-chip:nth-child(odd), .report-reasons .choice-chip:nth-child(even),
  .report-tab, .report-dialog .dialog-close { transform: none; }
  .report-star, .report-done-badge, .report-tile-art.is-pending { animation: none; }
}
