/*
 * Design tokens (colors, radii, shadows, motion, layout) live in the :root
 * block immediately below. Reference them via var(--token). Full catalog:
 * docs/design-system/README.md.
 *
 * Responsive breakpoints (canonical scale — prefer these for new rules)
 *   768px — tablet / mobile-nav threshold (top nav collapses to hamburger; see nav.css)
 *   640px — phone-landscape / table→card transition (e.g. Archive)
 *   480px — phone-portrait: stack two-column rows, drop address-box min-widths
 *   360px — small-phone padding trims
 * Existing pages also carry a few one-off breakpoints (900/720/540/520/420);
 * those are legacy and not worth churning, but new work should land on the
 * scale above so behaviour stays predictable across pages.
 */

/* ─────────────────────────────────────────────────────────────────────────────
 * DESIGN TOKENS — single source of truth (adopted from docs/design-system).
 * Retheme here; components below reference these via var(--token). Full token
 * catalog + semantic-accent rules: docs/design-system/README.md.
 * ────────────────────────────────────────────────────────────────────────── */
:root {
  /* — Neutral / ink scale — */
  --color-ink:        #111;      /* primary text, primary button, headings     */
  --color-ink-soft:   #333;      /* primary-button hover, body-strong          */
  --color-gray-700:   #374151;   /* strong labels, secondary button text       */
  --color-gray-600:   #4b5563;   /* card body copy                             */
  --color-gray-500:   #6b7280;   /* muted text, captions, meta                 */
  --color-gray-400:   #9ca3af;   /* placeholder, empty states, footer, disabled*/
  --color-gray-350:   #888;      /* disabled button bg                         */
  --color-gray-300:   #d1d5db;   /* input borders                             */
  --color-gray-200:   #e5e7eb;   /* hairline borders, dividers, card borders   */
  --color-gray-100:   #f3f4f6;   /* hover fills, chips                         */
  --color-gray-75:    #f4f4f4;   /* subtle hover fill                         */
  --color-gray-50:    #f9fafb;   /* readonly / surface-sunken fill            */
  --color-body:       #555;      /* default body text                         */

  --surface:          #fff;      /* page + card background                     */
  --surface-sunken:   #f9fafb;   /* insets, previews, readonly fields          */

  /* — Success (green) — */
  --color-success:        #16a34a;  /* canonical success fg                    */
  --color-success-border: #86efac;
  --color-success-bg:     #f0fdf4;
  --color-success-deep:          #065f46;  /* deep text on success tint (∥ danger-deep) */
  --color-success-bg-strong:     #d1fae5;  /* stronger success fill (∥ danger-bg-strong)*/
  --color-success-border-strong: #a7f3d0;  /* stronger success border                 */

  /* — Error / danger (red) — */
  --color-danger:         #dc2626;  /* fg                                      */
  --color-danger-strong:  #b91c1c;  /* hover / stronger fg                     */
  --color-danger-deep:    #991b1b;  /* icon-button pressed / callout text      */
  --color-danger-border:  #fca5a5;
  --color-danger-border-soft: #fecaca; /* danger icon-button border           */
  --color-danger-bg:      #fef2f2;
  --color-danger-bg-strong: #fee2e2;   /* red-100 fill under danger-deep text     */

  /* — Warning + Premium/Pro tier (amber) — */
  --color-warning:        #d97706;
  --color-warning-text:   #92400e;  /* amber text on light amber               */
  --color-warning-text-2: #b45309;
  --color-warning-deep:   #78350f;  /* banner text                            */
  --color-warning-border: #fde68a;
  --color-warning-border-2: #fcd34d;
  --color-warning-bg:     #fef3c7;

  /* — Info + Featured/Highlight + focus accent (blue) — */
  --color-info:        #2563eb;  /* accent / focus ring on light               */
  --color-info-link:   #1d4ed8;  /* link hover                                 */
  --color-info-text:   #1e40af;  /* text on light blue                         */
  --color-info-border: #bfdbfe;
  --color-info-bg:     #eff6ff;
  --color-info-bg-strong: #dbeafe;   /* blue-100 badge/notice fill, stronger than -bg */

  /* — Content-type category palette (badges) — */
  --cat-email-bg:  #eff6ff;  --cat-email-fg:  #1e40af;  /* blue   */
  --cat-photo-bg:  #f0fdf4;  --cat-photo-fg:  #16a34a;  /* green  */
  --cat-pdf-bg:    #fef3c7;  --cat-pdf-fg:    #92400e;  /* amber  */
  --cat-social-bg: #f3e8ff;  --cat-social-fg: #6b21a8;  /* purple */

  /* — Book-format badge palette (hardcover/digital × memory/photo default
     cover thumbnails; a product-axis taxonomy distinct from the content
     categories above). Mirrors /manage-books' default-cover colors. — */
  --fmt-hardcover-memory: #475569;  /* slate  */
  --fmt-digital-memory:   #3b82f6;  /* blue   */
  --fmt-hardcover-photo:  #0d9488;  /* teal   */
  --fmt-digital-photo:    #8b5cf6;  /* violet */

  /* — Coming-soon (indigo) — */
  --color-comingsoon-bg:   #eef2ff;
  --color-comingsoon-text: #4338ca;

  /* — Typography — */
  --font-sans:  -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-serif: Georgia, 'Times New Roman', serif;
  --font-mono:  ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* — Radii — */
  --radius-sm:   4px;   /* badges, small chips                */
  --radius-md:   6px;   /* compact inputs / buttons           */
  --radius-lg:   8px;   /* buttons, inputs, small cards       */
  --radius-xl:   10px;  /* modals                             */
  --radius-2xl:  12px;  /* cards, dropdowns                   */
  --radius-pill: 999px; /* pills, toasts                      */

  /* — Elevation — */
  --shadow-card:     0 1px 4px rgba(0,0,0,0.04);
  --shadow-dropdown: 0 4px 12px rgba(0,0,0,0.08);
  --shadow-popover:  0 10px 28px rgba(0,0,0,0.18);
  --shadow-modal:    0 10px 40px rgba(0,0,0,0.25);
  --shadow-toast:    0 8px 24px rgba(0,0,0,0.28);
  --shadow-image:    0 8px 30px rgba(0,0,0,0.08);  /* soft elevation for editorial photos */
  /* Tinted elevation for the featured/selected card states (blue = featured pick,
     gray = selected add-on). Owner-blessed 2026-07-25 (round-2 consolidation, B5). */
  --shadow-featured: 0 6px 18px rgba(37,99,235,0.12);
  --shadow-selected: 0 6px 18px rgba(107,114,128,0.18);
  /* Content floating ON a scrim (lightbox image / swatch clone). Deliberately
     twice --shadow-modal's alpha: over the 50% scrim a 0.25 shadow is invisible,
     so this is what separates the content from the backdrop. Named for the ROLE,
     not the magnitude — it must not be grabbed as a "heavier modal". */
  --shadow-lightbox: 0 10px 40px rgba(0,0,0,0.5);

  /* — Overlay scrim (modal/lightbox backdrops) — */
  --overlay-scrim:   rgba(0,0,0,0.5);

  /* — Motion — */
  --transition-fast: 0.15s;
  --transition-base: 0.2s;

  /* — Focus — */
  --focus-ring:      var(--color-ink);   /* dark controls / global default     */
  --focus-ring-blue: var(--color-info);  /* form + interactive controls on light*/
  /* Ring on DARK chrome (nav bar, skip-link). Solid, never translucent: a focus
     ring is a functional signal, and attenuating it costs contrast for exactly
     the users who depend on it. This was a literal in five places at three
     different opacities before round 3 reconciled them upward. */
  --focus-ring-inverse: #fff;
  --focus-offset:    2px;

  /* — Layout — */
  --container-max:   640px;
  --container-pad:   48px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
img { height: auto; }
/* Repair the reset: the UA sheet centers a showModal() dialog via `margin: auto`
   on its fixed inset:0 box; the universal margin:0 above wipes that and pins
   every <dialog> to the top-left. */
dialog { margin: auto; }
[hidden] { display: none !important; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

/* Tier-★ marker on the Email List / Share nav links: a small star nav.js
   appends (keyed on the active list's capability) to flag a link whose page
   holds upgrade-locked teaser sections. The Email List + Share links now show
   for every authenticated user (book-only reaches the teaser pages); the star
   is the only tier differentiator in the space-constrained nav. */
/* Circular star badge — mirrors the .tier-badge--pro palette (amber star on a
   light-yellow disc) so the nav marker reads like the on-page Pro tags.
   The star is a CENTERED SVG background-image of the disc, so the star and the
   circle belong to ONE element's raster and snap to the pixel grid together —
   they can never drift apart. (The old approach centered a ★ text glyph inside a
   flex box; the glyph rounded to the device-pixel grid independently of the disc,
   so depending on the badge's fractional x in the line the star landed up to 1px
   off-center — visibly different between the two nav teasers.)
   The disc is an atomic inline-block, so the active nav link's text-decoration
   underline SKIPS it (no line under the icon). `vertical-align: middle` + the
   1px top nudge keep the disc optically centered on the 14px nav text. */
.nav-tier-star {
  display: inline-block;
  width: 12px; height: 12px; margin-left: 4px;
  border-radius: 50%;
  background: var(--color-warning-bg) url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="%2392400e" d="M6 1.7 7.01 4.61 10.09 4.67 7.64 6.53 8.53 9.48 6 7.72 3.47 9.48 4.36 6.53 1.91 4.67 4.99 4.61Z"/></svg>') center / 12px 12px no-repeat;
  vertical-align: middle;
  position: relative; top: -1px;
  text-decoration: none;
}

/* Tier upsell teaser. lockSection() in utils.js applies .pro-locked (grayed +
   inert) to a section and pins a .tier-badge to the far right of its header.
   One kind: a gold "Pro" pill. (A silver "Standard" pill and a two-pill group
   existed for list-management features while either add-on unlocked them;
   Standard List was removed 2026-07-18 — docs/standard-list-removal-plan.md.)
   Used by /pro/email-list, /dashboard, /pro/share, /add-content. */
/* Dim per direct child (skipping the branch that holds the tier badge) rather
   than on .pro-locked itself: opacity on the section would flatten the badge
   too — a child's opacity can't exceed its parent's — washing the Pro /
   Standard tags out. :has keeps the badge (and its header) full strength. */
.pro-locked { pointer-events: none; }
.pro-locked > *:not(:has(.tier-badge)) { opacity: 0.55; }
.pro-locked input, .pro-locked textarea, .pro-locked button, .pro-locked .btn-copy,
.pro-locked .btn-action, .pro-locked a { pointer-events: none; }
/* …but keep the teaser's NAVIGATION affordances live so a user without the
   feature can still go learn about / explore it: the "Go to …" action buttons
   (.btn-action) and the "how it works" / "manage" card-links (.card-link,
   .card-cta — e.g. the all-tier "Manage approved senders" CTA inside the
   Pro-locked Broadcast teaser). Clickable + full opacity (they
   read as enabled); the teaser's own controls — Copy buttons, inputs — stay
   disabled. Mirrors the .tier-badge--action re-enable below. Opacity override
   targets any direct child that IS or CONTAINS such a link, since some sit
   inside a wrapper div (so the dim rule above flattens the whole branch). */
.pro-locked .btn-action, .pro-locked .card-link, .pro-locked .card-cta { pointer-events: auto; cursor: pointer; }
.pro-locked > .btn-action, .pro-locked > .card-link, .pro-locked > .card-cta,
.pro-locked > *:has(.btn-action), .pro-locked > *:has(.card-link), .pro-locked > *:has(.card-cta) { opacity: 1; }
/* The re-enable above is for nav CTAs (anchors). A locked feature's OWN action
   buttons are disabled <button>s (e.g. /share's Post + Download Custom Flyer) —
   keep them inert so they don't read as clickable (no hover, no pointer). */
.pro-locked .btn-action:disabled { pointer-events: none; cursor: default; }
/* …but the preview thumbnails stay clickable so a no/standard-list user can
   still open the lightbox to see the full-size flyer / story image. */
.pro-locked #storyThumb, .pro-locked #printableThumb { pointer-events: auto; cursor: pointer; }
/* When the tier tag IS the upsell trigger (lockSection given an `upsell`), it
   renders as a <button>. Re-enable clicks inside the otherwise-inert teaser
   (must come after the .pro-locked pointer-events:none rules above) and strip
   native button chrome so it matches the span badge. */
.pro-locked .tier-badge--action { pointer-events: auto; }
.tier-badge--action { cursor: pointer; -webkit-appearance: none; appearance: none; border: 0; font-family: inherit; }
.tier-badge--action.tier-badge--pro { border: 1px solid var(--color-warning-border-2); }
.tier-badge--action:hover { filter: brightness(0.96); }
.tier-badge--action:focus-visible { outline: 2px solid var(--focus-ring-blue); outline-offset: 1px; }
/* Missionary "ask the owner" hint — informational, kept readable (full opacity)
   inside the dimmed teaser. Must come after the .pro-locked dim rule. */
.owner-hint { margin-top: 14px; color: var(--color-gray-500); font-size: 13px; font-style: italic; }
.pro-locked > .owner-hint { opacity: 1; }

/* Add-a-book modal (multi-book). Injected + toggled via the `hidden` attribute
   by book-create.js; [hidden] must override the flex display. Mirrors the
   upsell-modal structure. */
.bc-modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; }
.bc-modal[hidden] { display: none; }
.bc-modal__backdrop { position: absolute; inset: 0; background: var(--overlay-scrim); }
/* Modal max-heights use dvh with a vh fallback: 100vh is iOS Safari's LARGE
   viewport, so a content-tall dialog's bottom action row hides behind the
   dynamic toolbar; dvh tracks the visible viewport. (Same on the two dialogs
   below and the lightbox.) */
.bc-modal__dialog { position: relative; background: #fff; border-radius: var(--radius-xl); padding: 24px; max-width: 560px; width: calc(100% - 32px); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); overflow-y: auto; box-shadow: var(--shadow-modal); }
.bc-modal__close { position: absolute; top: 10px; right: 14px; background: none; border: none; font-size: 24px; line-height: 1; color: var(--color-gray-400); cursor: pointer; }
.bc-modal__title { margin: 0 24px 0 0; font-size: 20px; }
.bc-modal__subtitle { color: var(--color-gray-500); font-size: 14px; margin-top: 4px; }
/* Two labeled sections mirroring /book/layout: "Printed book type & quality"
 * (the 4 Standard/Premium SKUs) and "Digital book type". */
.bc-section { margin-top: 16px; }
.bc-section__label { display: block; font-size: 13px; color: var(--color-gray-700); font-weight: 600; margin: 0 0 6px; }
.bc-modal__grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
/* Product cards mirror the Layout page's .bk-product-card chrome (2px border,
 * uppercase kind eyebrow, specs list, black-accented selection) so "Add a book"
 * reads as the same product picker the user meets again on /book/layout. */
.bc-card { position: relative; display: flex; flex-direction: column; gap: 6px; text-align: left; border: 2px solid var(--color-gray-200); border-radius: var(--radius-lg); padding: 16px; background: #fff; cursor: pointer; transition: border-color var(--transition-fast), background var(--transition-fast); }
/* Re-declare the hover background: .bc-card is a <button>, so without this the
   global `button:hover { background:var(--color-ink-soft) }` leaks through and darkens the card.
   Exclude .is-selected so the chosen card keeps its var(--color-ink) black-border emphasis. */
.bc-card:hover:not(.is-disabled):not(.is-selected) { border-color: var(--color-gray-400); background: var(--color-gray-50); }
.bc-card.is-selected { border-color: var(--color-ink); background: var(--color-gray-50); }
.bc-card.is-disabled { opacity: 0.55; cursor: not-allowed; }
.bc-card__kind { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-gray-500); }
.bc-card.is-premium .bc-card__kind { color: var(--color-warning-text); }
.bc-card__title { font-weight: 700; font-size: 15px; color: var(--color-ink); }
.bc-card__price { font-size: 13px; font-weight: 700; color: var(--color-ink); }
.bc-card__desc { font-size: 13px; color: var(--color-gray-600); line-height: 1.4; }
.bc-card__specs { margin: 0; font-size: 12px; line-height: 1.5; color: var(--color-gray-600); }
.bc-card__specs dt { display: inline; font-weight: 600; color: var(--color-gray-500); }
.bc-card__specs dd { display: inline; margin: 0 0 0 4px; }
.bc-card__specs dd::after { content: '\A'; white-space: pre; }
.bc-card__tag { position: absolute; top: 10px; right: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-warning-text); background: var(--color-warning-bg); padding: 2px 7px; border-radius: var(--radius-pill); }
.bc-modal__actions { display: flex; align-items: center; gap: 10px; justify-content: flex-end; margin-top: 18px; }
.bc-modal__error { color: var(--color-danger-strong); font-size: 13px; margin-right: auto; }

/* Add-on upsell modal (dashboard). Toggled via the `hidden` attribute by
   upsell-modal.js; [hidden] must override the flex display. */
.upsell-modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; }
.upsell-modal[hidden] { display: none; }
.upsell-modal__backdrop { position: absolute; inset: 0; background: var(--overlay-scrim); }
.upsell-modal__dialog { position: relative; background: #fff; border-radius: var(--radius-xl); padding: 24px; max-width: 520px; width: calc(100% - 32px); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); overflow-y: auto; box-shadow: var(--shadow-modal); }
.upsell-modal__close { position: absolute; top: 10px; right: 14px; background: none; border: none; font-size: 24px; line-height: 1; color: var(--color-gray-400); cursor: pointer; }
.upsell-modal__subtitle { color: var(--color-gray-500); font-size: 14px; margin-top: 4px; }
.upsell-modal__offers { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; }
.upsell-modal__offer { width: 100%; }
/* In-modal Standard-vs-Pro comparison (upsell-modal.js renderComparison). Two
   columns for a book-only owner choosing a tier; one column ("what you gain with
   Pro") for a Standard owner or a Pro-only feature. Collapses to one column on
   narrow screens. */
.upsell-modal__compare { margin-top: 16px; }
.upsell-modal__compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.upsell-modal__compare-grid--single { grid-template-columns: 1fr; }
.upsell-modal__compare-col { position: relative; border: 1px solid var(--color-gray-200); border-radius: var(--radius-lg); padding: 14px 12px; }
.upsell-modal__compare-col--popular { border-color: #f59e0b; }
.upsell-modal__compare-badge { position: absolute; top: -10px; right: 10px; background: var(--color-warning-bg); color: var(--color-warning-text); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px; border-radius: var(--radius-pill); }
.upsell-modal__compare-head { font-weight: 700; font-size: 15px; }
.upsell-modal__compare-tag { color: var(--color-gray-500); font-size: 12px; margin-top: 2px; }
.upsell-modal__compare-list { list-style: none; padding: 0; margin: 10px 0 0; }
.upsell-modal__compare-list li { font-size: 13px; line-height: 1.45; padding-left: 18px; position: relative; margin-bottom: 6px; }
.upsell-modal__compare-list li::before { content: '\2713'; position: absolute; left: 0; color: var(--color-success); font-weight: 700; }
@media (max-width: 480px) { .upsell-modal__compare-grid { grid-template-columns: 1fr; } }
/* Centered confirmation modal — shared by the content-upload pages (pdf/photo/
   social-group "remove this item?" dialogs). Toggle via the [hidden] attribute
   on .modal. The Cancel/confirm buttons inside opt out of the black-pill default
   via .btn-cancel / .btn-danger. */
.modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; }
.modal-backdrop { position: fixed; inset: 0; background: var(--overlay-scrim); }
.modal-content { position: relative; background: #fff; border-radius: var(--radius-lg); padding: 20px 24px; max-width: 420px; width: calc(100% - 32px); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); overflow-y: auto; box-shadow: var(--shadow-modal); }
.modal-content h2 { margin: 0 0 12px; font-size: 18px; }
.modal-content p { color: var(--color-gray-600); font-size: 14px; line-height: 1.5; margin: 0 0 16px; }
.modal-content .btn-group { display: flex; gap: 8px; justify-content: flex-end; }
.modal-content .btn-cancel { background: var(--color-gray-75); color: var(--color-gray-700); }
.modal-content .btn-cancel:hover { background: var(--color-gray-200); }
.modal-content .btn-danger { background: var(--color-danger); color: #fff; }
.modal-content .btn-danger:hover { background: var(--color-danger-strong); }
.modal-content .btn-danger:disabled { background: var(--color-danger-border); cursor: not-allowed; }
/* Transient confirmation toast (utils.js showToast). Hidden until the --show
   class is added; fades in/out. z-index above the upsell modal so a post-
   purchase toast is visible while the page reloads. */
.mml-toast { position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%) translateY(12px); z-index: 1100; background: #111827; color: #fff; font-size: 14px; font-weight: 600; padding: 12px 18px; border-radius: var(--radius-pill); box-shadow: var(--shadow-toast); opacity: 0; pointer-events: none; transition: opacity var(--transition-base) ease, transform var(--transition-base) ease; max-width: calc(100% - 32px); text-align: center; }
.mml-toast--show { opacity: 1; transform: translateX(-50%) translateY(0); }
.tier-badge {
  display: inline-flex; align-items: center; gap: 4px; flex: none;
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; padding: 3px 8px; border-radius: var(--radius-pill);
  white-space: nowrap; vertical-align: middle; line-height: 1;
}
/* Inline-in-running-text badges (e.g. dashboard/add-content section copy):
   drop the pill slightly so it sits near the text's optical center. middle
   reads high next to mixed-case body text; the earlier -0.26em overcorrected
   and read low, so -0.12em splits the difference. */
.section-desc .tier-badge { vertical-align: -0.12em; }
.tier-badge--pro { background: var(--color-warning-bg); color: var(--color-warning-text); border: 1px solid var(--color-warning-border-2); }
/* Header row holding an <h2>/<h3> with its .tier-badge trailing just to the
   right of the heading text (not pinned to the far edge of the row).
   line-height: 1 collapses the heading's leading so the (descender-free)
   heading text sits at the line-box center — without it the default ~1.33
   leading floats the text high in the box and align-items:center drops the
   small badge visibly low next to it. */
/* flex-wrap so the trailing badge drops below the heading on very narrow
   viewports (e.g. a long /share section title at 320px) instead of forcing
   horizontal overflow. row-gap keeps the wrapped badge tight to the heading. */
.tier-locked-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 6px 12px; line-height: 1; }
/* align-items:center centers the badge on the heading's line box, but the box
   carries ~0.07em of ascender leading above the caps, so the badge floats
   slightly high above the (descender-free) heading text. Nudge it down to the
   caps' optical center. */
.tier-locked-header .tier-badge { transform: translateY(0.03em); }

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: #fff;
  /* dvh + vh fallback: with plain 100vh, mobile Safari's dynamic URL bar
     leaves the footer just below the initial visual viewport. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.main { flex: 1; padding: 48px 20px; }
.main--centered { display: flex; align-items: flex-start; justify-content: center; }

.container {
  background: white;
  padding: 48px;
  border-radius: var(--radius-2xl);
  border: 1px solid var(--color-gray-200);
  box-shadow: 0 1px 4px rgba(0,0,0,0.04);
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
}
.container--centered { text-align: center; }
.container--narrow { max-width: 480px; }

h1 { font-size: 26px; color: var(--color-ink); font-weight: 800; margin-bottom: 32px; }
/* The compact h1 — utility / lifecycle / account pages (design-system README §2
   sanctions 24/800 there). It was blessed in the docs at the round-2
   consolidation but never given a component, so ~20 surfaces hand-rolled it:
   11 identical inline styles in payment-success.js plus 8 page-local rules with
   five different margins. A documented-but-unimplemented variant is how drift
   regrows, so it lives here now.
   Specificity note: this is (0,1,1), which TIES with a page's `.page h1`
   override and BEATS a bare `h1` one. Every consumer links common.css before
   its own <style> block, so a page that needs a different margin pins it as
   `h1.compact { margin-bottom: … }` and wins on source order. */
h1.compact { font-size: 24px; margin-bottom: 12px; }
/* .tight is the MARGIN modifier and .compact the SIZE modifier; they are
   orthogonal and compose (`class="compact tight"`). .tight is declared AFTER
   .compact deliberately — they tie on specificity, so this source order is what
   lets a margin modifier beat the component's own default margin. */
h1.tight { margin-bottom: 8px; }
h2 { font-size: 17px; color: var(--color-ink); font-weight: 700; margin-bottom: 16px; }
p { color: var(--color-body); margin-bottom: 24px; line-height: 1.5; }
/* Inline <code> had NO rule anywhere, so it fell back to the UA's generic
   `monospace` — whose metrics Chromium resolves differently during a fullPage
   screenshot (captureBeyondViewport) than during normal painting. Each <code>
   shifted 1px and reflowed its containing block by 2px, so /faq's three <code>
   elements moved the page height by exactly 6px between consecutive captures
   and its visual baseline could never settle (see e2e/tests/visual/).
   Pinning a real font stack + an explicit line-height makes the box height
   independent of that resolution — and gives inline code consistent typography
   site-wide, which it never had. em-relative size so it tracks its context
   (monospace faces run visually larger than the body face at the same px). */
code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  font-size: 0.92em;
  line-height: 1.5;
}

.field-label { display: block; text-align: left; font-size: 14px; font-weight: 600; color: var(--color-gray-700); margin-bottom: 8px; }
.field-label--top { margin-top: 16px; }
.field-hint { display: block; text-align: left; font-size: 14px; color: var(--color-gray-500); margin-top: -4px; margin-bottom: 16px; padding-left: 16px; }
.field-error { color: var(--color-danger); font-size: 14px; display: block; margin-top: -4px; margin-bottom: 4px; }
.field-error:empty { margin: 0; }

input, textarea {
  width: 100%;
  padding: 14px 16px;
  border: 1.5px solid var(--color-gray-300);
  border-radius: var(--radius-lg);
  font-size: 16px;
  margin-bottom: 16px;
  outline: none;
  font-family: inherit;
  transition: border-color var(--transition-base);
}
input:focus, textarea:focus { border-color: var(--color-ink); }
.form-tight input, .form-tight textarea { margin-bottom: 6px; }
input[readonly] { background: var(--color-gray-50); color: var(--color-gray-700); cursor: default; border-color: var(--color-gray-200); }
input[readonly]:focus { border-color: var(--color-gray-200); }
input::placeholder { color: var(--color-gray-400); }
/* In-field password show/hide toggle (injected by wirePasswordToggle). The
   wrapper is a bare position:relative block so the input's bottom margin
   collapses through it and vertical layout is unchanged; the button overlays
   the input's right edge and the input gets right padding to clear it. */
.pw-field { position: relative; }
.pw-field input { padding-right: 46px; }
/* Edge injects its own native reveal eye (::-ms-reveal) into every password field
   and a clear (×) button (::-ms-clear) into text fields — with our own toggle that
   would show a second, redundant control (the eye when hidden, the × once flipped to
   text). Suppress Edge's, scoped to .pw-field so other inputs keep theirs. Inert on
   Chrome/Firefox/Safari, which don't render these -ms- pseudo-elements. */
.pw-field input::-ms-reveal,
.pw-field input::-ms-clear { display: none; }
/* Rules are scoped under .pw-field (specificity 0,2,0) so they beat the app's
   primary-button styling that this <button> would otherwise inherit — the global
   `button:hover` fill (0,1,1) and page-inline `.main button { min-width: 200px }`
   (0,1,1). Hence the explicit min-width/background/margin resets: without them the
   button stretches to 200px and paints a dark fill on hover.
   The input keeps its own bottom margin (base 16px; 6px under .form-tight) and it
   does NOT collapse out of this wrapper, so the abs-positioning container spans the
   input box PLUS that margin. Pull the button up by the margin via `bottom` so it
   centers on the input box, not the input-plus-margin box. */
.pw-field .pw-toggle {
  position: absolute; top: 0; bottom: 16px; right: 6px;
  width: 36px; min-width: 0; margin: 0; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: var(--radius-md);
  color: var(--color-gray-500); cursor: pointer;
}
.form-tight .pw-toggle { bottom: 6px; }
.pw-field .pw-toggle:hover { color: var(--color-gray-700); }
.pw-field .pw-toggle:focus-visible { outline: 2px solid var(--color-ink); outline-offset: 2px; }
input.invalid { border-color: var(--color-danger); }

button {
  padding: 12px 24px;
  background: var(--color-ink);
  color: white;
  border: none;
  border-radius: var(--radius-lg);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--transition-base);
}
button:hover { background: var(--color-ink-soft); }
button:focus-visible { outline: 2px solid var(--color-ink); outline-offset: 2px; }
button:disabled { background: var(--color-gray-350); cursor: not-allowed; }

.btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
.section { margin-bottom: 32px; }
.section:last-child { margin-bottom: 0; }
.section-title { font-size: 17px; font-weight: 700; color: var(--color-ink); margin-bottom: 16px; }
.subtitle { font-size: 15px; color: var(--color-body); line-height: 1.5; margin-bottom: 32px; }
.section-desc { font-size: 14px; color: var(--color-body); margin-bottom: 12px; line-height: 1.5; }
/* Centered footnote closing a section — the cross-sell/"read our tips" line on
   the marketing and tips pages. Follows the .pill shared-tone / scoped-geometry
   pattern: the base carries only tone + type, because the two families that use
   it sit in containers of different widths (760px on the tips pages, full-bleed
   on the marketing ones). --wide adds the marketing geometry. */
.section-note { text-align: center; color: var(--color-gray-600); font-size: 15px; }
.section-note--wide { max-width: 640px; margin: 32px auto 8px; padding: 0 24px; }
/* Persistent "Contact us" recourse line on the order-status view. */
.order-contact-note { font-size: 13px; color: var(--color-gray-500); line-height: 1.5; margin-top: 16px; }
/* Digital-download exclusion acknowledgments on the order-status view. Shown only
   when the download route 409s on unacknowledged exclusions; every box must be
   checked before the download button re-enables. The explicit width/height +
   zeroed margin/padding on the checkbox override the GLOBAL `input` rule
   (width:100% + padding + bottom margin), which would otherwise stretch a native
   checkbox across the row — the same reset the proof page's ack blocks needed. */
.download-exclusions {
  background: var(--color-warning-bg); border: 1px solid var(--color-warning-border-2);
  border-radius: 8px; padding: 12px 14px; margin: 12px 0;
  font-size: 14px; line-height: 1.5;
}
.download-exclusions p { margin: 0 0 8px; }
.download-ack {
  display: flex; gap: 10px; align-items: flex-start;
  margin: 8px 0 0; font-size: 14px; line-height: 1.6; cursor: pointer;
}
.download-ack input[type="checkbox"] {
  flex-shrink: 0; margin: 3px 0 0; padding: 0;
  width: 18px; height: 18px; cursor: pointer;
}
.divider { border: none; border-top: 1px solid var(--color-gray-200); margin: 32px 0; }
/* When a .divider introduces a preview block (the content separator above the
 * previews on /book/cover + /book/title-pages), let the divider's own 32px
 * margin set the gap — zero the preview's top margin so they don't double up. */
.divider + .cover-previews-row,
.divider + .cover-preview,
.divider + .tp-preview-card { margin-top: 0; }
.back-link { display: inline-block; padding: 12px 0; margin-bottom: 12px; color: var(--color-body); text-decoration: none; font-size: 14px; }
.back-link::before { content: ''; display: inline-block; width: 6px; height: 6px; border-left: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg); margin-right: 6px; vertical-align: middle; position: relative; top: -1px; }
.muted-link { font-size: 14px; color: var(--color-gray-500); text-decoration: none; font-weight: 500; background: none; border: none; padding: 0; font-family: inherit; cursor: pointer; }
button.muted-link:hover { background: none; color: var(--color-ink); }
.reset-link { font-size: 14px; color: var(--color-gray-500); text-decoration: none; background: none; border: none; padding: 0; font-family: inherit; cursor: pointer; }
.reset-link:hover { text-decoration: underline; background: none; }
.btn-text { background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; text-align: inherit; text-decoration: none; }
.btn-text:hover { background: none; text-decoration: underline; }
.signup-link { font-size: 14px; color: var(--color-body); margin-bottom: 0; }
.signup-link a { color: var(--color-ink); font-weight: 600; text-decoration: none; }
.signup-link a:visited { color: var(--color-ink); }
.signup-link a:hover { text-decoration: underline; }
.howitworks-link, .howitworks-link:visited { color: var(--color-ink); font-weight: 600; text-decoration: none; }
.howitworks-link:hover { text-decoration: underline; }
/* Section navigation / "how it works" text links (dashboard cards). Promoted
   here from dashboard's inline style so the class is fully defined where its
   .pro-locked clickability rules already live (near the top of this file). */
.card-link, .card-link:visited { display: inline-block; margin-top: 12px; font-size: 13px; font-weight: 600; color: var(--color-ink); text-decoration: none; }
.card-link:hover { text-decoration: underline; }
/* Bordered icon CTA used inside cards/sections: the upload how-to cards on
   /add-content and the "Manage approved senders" CTAs on /add-content +
   /pro/email-list. Promoted from /add-content's inline style so it's reusable;
   kept live + full-opacity inside .pro-locked teasers via the rules near the
   top of this file (the email-list Broadcast CTA sits in a Pro-locked block
   but approved senders is all-tier). */
.card-cta, .card-cta:visited {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 16px;
  background: #fff; color: var(--color-gray-700); border: 1px solid var(--color-gray-300); border-radius: var(--radius-md);
  padding: 6px 14px; font-size: 14px; font-weight: 500; line-height: 1; text-decoration: none;
}
.card-cta:hover { background: var(--color-gray-50); color: var(--color-ink); }
.card-cta svg { display: block; flex: 0 0 auto; }
/* Disabled card-cta <button> (e.g. "Send test broadcast" inside a Pro-locked
   Broadcast teaser for a non-Pro list — lockSection disables it): faded text +
   border instead of the global button:disabled var(--color-gray-350) box, and no hover shift. */
.card-cta:disabled, .card-cta:disabled:hover { background: #fff; color: var(--color-gray-400); border-color: var(--color-gray-200); cursor: default; }
.info-msg { background: var(--color-success-bg); border: 1px solid var(--color-success-border); border-radius: var(--radius-lg); padding: 12px 16px; font-size: 14px; color: var(--color-success); margin-bottom: 16px; line-height: 1.5; }
.back-link:visited { color: var(--color-body); }
.back-link:hover { color: var(--color-ink); }
.btn-action { display: inline-block; padding: 12px 24px; background: var(--color-ink); color: white; border-radius: var(--radius-lg); font-size: 15px; font-weight: 600; text-decoration: none; transition: background var(--transition-base); }
.btn-action:visited { color: white; }
.btn-action:hover { background: var(--color-ink-soft); }
/* Save / Cancel button pair on the book editor save points (layout, settings,
   cover, title pages): both start disabled and enable on the first edit in
   their section (2026-06-22 — replaced the "Unsaved changes" hint text + the
   "Reset to last saved values" link). .btn-action carries its own :hover, so an
   explicit disabled rule is needed or a disabled Save would darken on hover.
   The btn-link Cancel is muted when disabled (the gray button:disabled box
   doesn't apply to btn-link). */
.btn-action:disabled, .btn-action:disabled:hover { background: var(--color-gray-350); cursor: not-allowed; }
.btn-link.btn-cancel-changes:disabled { color: var(--color-gray-400); cursor: default; }
.btn-outline { display: inline-block; padding: 8px 16px; background: white; color: var(--color-ink); border: 1.5px solid var(--color-ink); border-radius: var(--radius-lg); font-size: 14px; font-weight: 600; text-decoration: none; transition: background var(--transition-base); }
.btn-outline:hover { background: var(--color-gray-75); }
.btn-edit { background: var(--color-gray-500); }
.btn-edit:hover { background: var(--color-gray-700); }
.btn-danger { background: var(--color-danger); color: white; }
.btn-danger:hover { background: var(--color-danger-strong); }
.btn-danger:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-outline-danger { background: white; color: var(--color-danger); border: 1.5px solid var(--color-danger); }
.btn-outline-danger:hover { background: var(--color-danger-bg); }
/* Explicit primary — identical to the base <button> / .btn-action anchor. Use to
   state intent (e.g. on a <button> whose class list would otherwise read as
   "unstyled") or to get the primary treatment on a non-<button> element. */
.btn-primary { display: inline-block; padding: 12px 24px; background: var(--color-ink); color: #fff; border: none; border-radius: var(--radius-lg); font-size: 15px; font-weight: 600; text-decoration: none; cursor: pointer; transition: background var(--transition-base); }
.btn-primary:visited { color: #fff; }
.btn-primary:hover { background: var(--color-ink-soft); }
.btn-primary:disabled, .btn-primary:disabled:hover { background: var(--color-gray-350); cursor: not-allowed; }
/* Subtle secondary — white fill, hairline gray border, ink text. The quieter
   counterpart to .btn-outline (strong ink border). Formalized from the
   admin/affiliate/order "ghost / --alt / --secondary / --revise" buttons. */
.btn-secondary { display: inline-block; padding: 8px 16px; background: #fff; color: var(--color-ink); border: 1px solid var(--color-gray-300); border-radius: var(--radius-lg); font-size: 14px; font-weight: 600; text-decoration: none; cursor: pointer; transition: background var(--transition-base); }
.btn-secondary:hover { background: var(--color-gray-50); }
.btn-secondary:disabled, .btn-secondary:disabled:hover { color: var(--color-gray-400); border-color: var(--color-gray-200); background: #fff; cursor: not-allowed; }
/* Full-width button modifier (composes with any button class or a bare <button>). */
.btn-block { width: 100%; }

/* Approved-senders control — SHARED by /pro/email-list and /add-content
   (the same control over the same data). The section header matches whatever
   heading level the host page uses (h2 on email-list, h3 on add-content). */
.sender-section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.sender-section-header h1, .sender-section-header h2, .sender-section-header h3 { margin-bottom: 0; }
.sender-row { display: flex; gap: 8px; align-items: stretch; margin-bottom: 8px; }
.sender-row .btn-outline-danger { display: flex; align-items: center; min-height: 44px; }
.sender-row .sender-item { flex: 1; margin-bottom: 0; }
.sender-item { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 14px 16px; border: 1px solid var(--color-gray-200); border-radius: var(--radius-lg); margin-bottom: 8px; font-size: 14px; color: var(--color-ink); }
/* Let a long sender email shrink + wrap instead of pushing the badge past the
   viewport (caught by the responsive overflow guard at 320px). */
.sender-item > span:first-child { min-width: 0; overflow-wrap: anywhere; }
.sender-item .sender-badge { flex-shrink: 0; }
.sender-badge { font-size: 14px; background: var(--color-gray-75); color: var(--color-gray-500); border-radius: var(--radius-sm); padding: 2px 8px; }
/* flex-wrap: at <=400px the input's intrinsic minimum plus the two nowrap
   buttons exceed the row — wrap instead of clipping the Cancel button. */
.sender-add { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.sender-add input { margin-bottom: 0; flex: 1; font-size: 16px; padding: 10px 14px; }
.sender-add button { white-space: nowrap; padding: 10px 18px; font-size: 14px; }
.sender-toggle { background: none; border: none; color: var(--color-body); font-size: 14px; font-weight: 600; cursor: pointer; padding: 4px 0; margin-top: 8px; }
.sender-toggle:hover { color: var(--color-ink); background: none; }
/* "How archive discovery works" numbered list — shared by /add-content and
   /pro/email-list (formerly duplicated inline on both). */
.archive-discovery-steps { padding-left: 24px; margin: 12px 0 20px; color: var(--color-gray-700); font-size: 14px; line-height: 1.7; }
.archive-discovery-steps li { margin-bottom: 6px; }
/* Shrink + center the approved-sender fields and the add/toggle controls so
   the list reads as a contained, centered group on /approved-senders.
   margin-left/right (not shorthand) preserves each rule's top gap. */
#approvedSendersList,
#approvedSendersBlock .sender-add { max-width: 420px; margin-left: auto; margin-right: auto; }
/* Breathing room between the section description and the first sender row. */
#approvedSendersList { margin-top: 24px; }
#approvedSendersBlock .sender-toggle { display: block; width: fit-content; margin-left: auto; margin-right: auto; }
.hint { font-size: 14px; color: var(--color-gray-500); margin-top: 8px; line-height: 1.5; }
/* Dense contexts — 13px is already a sanctioned step on the type scale (README
   §2 "small / labels: 14, 13"), and ~15 sites were reaching for it inline with
   no component to name it. A variant, not a new size. */
.hint--sm { font-size: 13px; }

.error { color: var(--color-danger); font-size: 14px; margin-top: 8px; }
.success { color: var(--color-success); font-size: 14px; margin-top: 8px; }
p.error:empty, p.success:empty { display: none; }

.result {
  margin-top: 20px;
  padding: 16px;
  border-radius: var(--radius-lg);
  display: none;
  /* Long unbreakable tokens (broadcast addresses like
     elder-x@list.sepiabooks.com) must wrap, not escape the box. */
  overflow-wrap: anywhere;
  font-size: 14px;
  line-height: 1.5;
  text-align: left;
}
.result.success { background: var(--color-success-bg); border: 1px solid var(--color-success-border); color: var(--color-success); }
.result.success a { color: var(--color-success); font-weight: 600; }
.result.success a:visited { color: var(--color-success); }
.result.error { background: var(--color-danger-bg); border: 1px solid var(--color-danger-border); color: var(--color-danger); }
.result.warning { background: var(--color-warning-bg); border: 1px solid var(--color-warning-border); color: var(--color-warning-text); }

/* Home newsletter signup band (sits between §6 and the final dark CTA). */
.newsletter-band { background: #fff; border-top: 1px solid var(--color-gray-200); padding: 56px 20px; }
.newsletter-inner { max-width: 640px; margin: 0 auto; text-align: center; }
.newsletter-title { font-size: 28px; font-weight: 800; color: var(--color-ink); margin: 0 0 8px; }
.newsletter-sub { color: var(--color-body); font-size: 16px; margin: 0 0 24px; }
.newsletter-form { display: flex; gap: 10px; justify-content: center; align-items: center; flex-wrap: wrap; }
/* margin:0 cancels the global `input{margin-bottom:16px}` — in this
   align-items:center flex row that phantom margin centers the input's margin
   box, shoving its visible box up so the button looks taller and overhangs. */
.newsletter-input { flex: 1 1 280px; max-width: 360px; margin: 0; padding: 12px 14px; font-size: 16px; font-family: inherit; border: 1px solid var(--color-gray-300); border-radius: var(--radius-lg); }
.newsletter-btn { white-space: nowrap; cursor: pointer; border: none; font-family: inherit; }
/* Honeypot: off-screen + zero-size, hidden from humans; aria-hidden hides it
   from screen readers. NOT display:none (some bots skip those) and NOT
   .sr-only (that exposes it to assistive tech). */
/* Off-screen honeypot field — a bot fills it, a human never sees it. Shared by
   the newsletter signup and the affiliate application (which hand-rolled a
   byte-identical .afa-hp until round 3). */
.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.newsletter-fineprint { color: var(--color-gray-350); font-size: 13px; margin: 16px 0 0; }
.newsletter-fineprint a { color: var(--color-gray-350); text-decoration: underline; }
#newsletterResult { margin: 16px auto 0; max-width: 360px; }
.badge-success { display: inline-block; background: var(--color-success-bg); color: var(--color-success); border-radius: var(--radius-sm); padding: 2px 8px; font-size: 12px; font-weight: 600; }
.badge-failed { display: inline-block; background: var(--color-danger-bg); color: var(--color-danger); border-radius: var(--radius-sm); padding: 2px 8px; font-size: 12px; font-weight: 600; }

/* ── Skeleton loading ───────────────────────────────────────────────────────── */
@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.skeleton {
  background: linear-gradient(90deg, var(--color-gray-75) 25%, var(--color-gray-200) 50%, var(--color-gray-75) 75%) !important;
  background-size: 200% 100% !important;
  animation: shimmer 1.5s infinite linear;
  color: transparent !important;
  pointer-events: none !important;
  cursor: default !important;
  border-color: var(--color-gray-200) !important;
}
.skeleton::after { display: none !important; }
.skeleton * { color: transparent !important; }
/* Respect prefers-reduced-motion: freeze the shimmer to a static tint so the
   placeholder still reads as "loading" without the continuous animation. */
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none !important; background: var(--color-gray-200) !important; }
}

/* Empty skeleton placeholders for first-paint loading states.
   Pair with .skeleton for the shimmer animation. Pages add their own
   width/height inline where shape needs to differ from defaults. */
.skel-line { display: block; height: 14px; border-radius: var(--radius-sm); margin-bottom: 8px; }
.skel-line--sm { height: 11px; }
.skel-line--lg { height: 22px; }
.skel-line--xl { height: 32px; margin-bottom: 16px; }
.skel-block { display: block; border-radius: var(--radius-lg); margin-bottom: 16px; }

/* .main horizontal padding is monotonic as the screen shrinks (20→16→16→12) so
   the smallest phones never get LESS usable width than mid-size ones. */
@media (max-width: 600px) { .container { padding: 32px; } .main { padding: 32px 16px; } }
@media (max-width: 480px) { .container { padding: 24px; } .main { padding: 32px 16px; } }
@media (max-width: 360px) { .container { padding: 16px; } .main { padding: 24px 12px; } }

/* ── Checkbox label ─────────────────────────────────────────────────────────── */
.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500; color: var(--color-gray-700); cursor: pointer; min-height: 44px; }
.checkbox-label input[type="checkbox"] { width: auto; margin: 0; accent-color: var(--color-ink); cursor: pointer; }
/* A checkbox whose input is `disabled` — the label must LOOK unavailable, not
   merely refuse the click. Mirrors the .bc-card.is-disabled convention
   (opacity 0.55 + not-allowed). Used by PaymentFlow.applySurpriseSuppression. */
.checkbox-label--disabled { opacity: 0.55; cursor: not-allowed; }
.checkbox-label--disabled input[type="checkbox"] { cursor: not-allowed; }
.checkbox-label--right { width: fit-content; margin-left: auto; margin-bottom: 20px; }
.checkbox-label--center { width: fit-content; margin-left: auto; margin-right: auto; margin-bottom: 20px; }

/* ── Pagination ─────────────────────────────────────────────────────────────── */
.lookup-section { margin-bottom: 32px; }
.empty { color: var(--color-gray-400); font-style: italic; text-align: center; padding: 32px 0; }
.page-info { color: var(--color-gray-500); font-size: 14px; }
.pagination { display: flex; align-items: center; justify-content: space-between; margin-top: 16px; gap: 8px; }
.pagination button { padding: 12px 16px; font-size: 14px; font-weight: 500; width: auto; min-height: 44px; }
.pagination button:disabled { background: var(--color-gray-200); color: var(--color-gray-400); cursor: not-allowed; }

/* ── Copy components ────────────────────────────────────────────────────────── */
.address-box {
  position: relative;
  background: var(--color-gray-50);
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-lg);
  padding: 12px 16px;
  /* Cap copyable address fields at a uniform, tidy width site-wide instead of
     stretching to fill their row; longer addresses wrap inside the box. */
  max-width: 400px;
  font-weight: 600;
  color: var(--color-ink);
  overflow-wrap: anywhere;
  font-size: 14px;
  cursor: pointer;
  user-select: none;
  overflow: visible;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}
.address-box:hover { border-color: var(--color-gray-350); background: var(--color-gray-75); }
.broadcast-row { display: flex; gap: 8px; align-items: stretch; flex-wrap: wrap; }
.broadcast-row .address-box { flex: 1; min-width: 0; margin-bottom: 0; }
.url-text {
  flex: 1;
  min-width: 0;
  position: relative;
  background: var(--color-gray-50);
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-lg);
  padding: 12px 16px;
  font-size: 14px;
  color: var(--color-ink);
  overflow-wrap: anywhere;
  line-height: 1.4;
  cursor: pointer;
  user-select: none;
  overflow: visible;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}
.url-text:hover { border-color: var(--color-gray-350); background: var(--color-gray-75); }
.address-box::after,
.url-text::after,
.qr-wrapper::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-ink);
  color: white;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-fast);
}
.address-box:hover::after,
.url-text:hover::after,
.qr-wrapper:hover::after { opacity: 1; }
/* opacity: 1 — .copied is class-driven with a timed revert (utils.js), so
   forcing the tooltip visible makes the "Copied!" confirmation show on TAP:
   on touch devices :hover alone may never fire, or sticks after the tap. */
.address-box.copied::after,
.url-text.copied::after,
.qr-wrapper.copied::after { background: var(--color-success); opacity: 1; }
.btn-copy { font-size: 15px; padding: 12px 16px; white-space: nowrap; }
.btn-copy.copied { background: var(--color-success); }
.qr-wrapper { position: relative; display: inline-block; cursor: pointer; border-radius: var(--radius-lg); flex-shrink: 0; transition: opacity var(--transition-fast); }
.qr-wrapper:hover { opacity: 0.85; }

@media (max-width: 480px) {
  .broadcast-row .address-box { min-width: 100%; }
}

/* ── Contact / Support form ─────────────────────────────────────────────────── */
.form-contact input, .form-contact textarea { margin-bottom: 20px; }
.form-contact textarea { resize: vertical; min-height: 120px; }
.form-contact button { min-width: 200px; display: block; margin: 8px auto 0; }
.form-contact .result { margin-top: 24px; padding: 20px; }

/* ── FAQ content ────────────────────────────────────────────────────────────── */
.faq-item { margin-bottom: 28px; }
.faq-item:last-child { margin-bottom: 0; }
.faq-q { font-size: 15px; font-weight: 700; color: var(--color-ink); margin-bottom: 6px; margin-top: 0; }
.faq-a { font-size: 15px; color: var(--color-body); line-height: 1.65; }
p a { color: var(--color-ink); font-weight: 600; }
p a:visited { color: var(--color-ink); }
.faq-a a { color: var(--color-ink); font-weight: 600; }
.faq-a a:visited { color: var(--color-ink); }
.faq-a a:hover { text-decoration: underline; }

/* ── Pro tier pill ──────────────────────────────────────────────────────────── */
/* Small green badge marking a Pro-tier surface (Pro-only sections, page titles
   on Pro lists). Inline-block so it sits inside an h1/h2 next to the title text. */
.pro-pill {
  display: inline-block;
  background: var(--color-success);
  color: white;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 3px 10px;
  border-radius: var(--radius-2xl);
  vertical-align: middle;
  margin-left: 8px;
  position: relative;
  top: -2px;
}

/* ── Coming Soon pill ───────────────────────────────────────────────────────── */
/* Neutral counterpart to .pro-pill for future-availability hints. Pair with
   .coming-soon-note when the accompanying copy may wrap, so the text doesn't
   slide under the pill on the second line. */
.coming-soon-pill {
  display: inline-block;
  background: var(--color-comingsoon-bg);
  color: var(--color-comingsoon-text);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 3px 10px;
  border-radius: var(--radius-2xl);
  vertical-align: middle;
  margin-right: 6px;
}
.coming-soon-note {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-top: 12px;
  margin-bottom: 16px;
}
.coming-soon-note .coming-soon-pill {
  flex-shrink: 0;
  margin-right: 0;
  margin-top: 1px;
}
.coming-soon-note p {
  margin: 0;
  font-size: 13px;
  color: var(--color-gray-500);
}

/* ── Beta pill ──────────────────────────────────────────────────────────────── */
/* ── Cancelled-list banner ──────────────────────────────────────────────────── */
/* Shown above <main> on authenticated pages when the active list has
   is_active=false. Amber tone so it reads as an inert state notice rather
   than a destructive error. Paired with cancelled-banner.js, which also
   disables form inputs and submit buttons inside <main> (opt-out via the
   .allow-when-cancelled class — used by Delete Account, broadcast PDF
   downloads, and any read-only navigation buttons). */
.cancelled-banner {
  background: var(--color-warning-bg);
  color: var(--color-warning-deep);
  border-bottom: 1px solid var(--color-warning-border);
  padding: 14px 24px;
  text-align: center;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.5;
}
.cancelled-banner strong { font-weight: 700; }
.cancelled-banner a {
  color: var(--color-warning-deep);
  text-decoration: underline;
  font-weight: 700;
  margin-left: 6px;
}
.cancelled-banner a:visited { color: var(--color-warning-deep); }
/* Disabled-state styling for form fields and buttons inside <main> when
   cancelled-banner.js has flipped them. Keeps them visible but obviously
   inactive — most browsers gray out the chrome already; this just nudges
   the cursor/opacity to match. */
.cancelled-banner ~ main input:disabled,
.cancelled-banner ~ main select:disabled,
.cancelled-banner ~ main textarea:disabled,
.cancelled-banner ~ main button:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

/* ── Proof-mode lock banner ─────────────────────────────────────────────────── */
/* Shown by proof-lock.js above <main> when the active book (book-config pages)
   or any book on the list (mission-archive pages) is in proof. Sticky so it
   stays visible while the customer scrolls the (now read-only) page. Blue tone
   to read as an informational "paused" state distinct from the amber cancelled
   banner and red errors. Paired with .proof-locked, which inert-s + dims the
   page's [data-proof-lock] content region. */
.proof-lock-banner {
  position: sticky;
  top: 0;
  z-index: 50;
  background: #e0f2fe;
  color: #075985;
  border-bottom: 1px solid #bae6fd;
  padding: 14px 24px;
  text-align: center;
  font-size: 15px;
  line-height: 1.5;
  font-weight: 600;
}
.proof-lock-banner strong { font-weight: 700; display: block; }
.proof-lock-banner em { font-style: normal; opacity: 0.8; }
/* Short viewports (landscape phones): the sticky banner's multi-line content
   (name on its own line + the link/Cancel button below) can eat a large share
   of the vertical space. Trim padding + font so it stays a compact cue. */
@media (max-height: 560px) {
  .proof-lock-banner { padding-top: 8px; padding-bottom: 8px; font-size: 14px; }
}
.proof-lock-banner-link {
  display: inline-block;
  margin-top: 6px;
  color: #075985;
  font-weight: 700;
  text-decoration: underline;
  white-space: nowrap;
}
/* "Cancel order" escape on the awaiting-payment lock banner — a quiet bordered
   button in the banner's own palette, beside the Finish-payment link. */
.proof-lock-banner-cancel {
  display: inline-block;
  margin-top: 6px;
  margin-left: 10px;
  background: none;
  border: 1px solid #075985;
  color: #075985;
  font-weight: 700;
  font-size: 14px;
  padding: 4px 12px;
  border-radius: var(--radius-md);
  cursor: pointer;
  white-space: nowrap;
}
/* Re-declare hover bg: a single-class button (0,1,0) loses to the global
   button:hover (0,1,1) and would darken to near-black otherwise. */
.proof-lock-banner-cancel:hover { background: rgba(7, 89, 133, 0.08); }
.proof-lock-banner-cancel:disabled { opacity: 0.6; cursor: default; }

/* Transient "Saved ✓" confirmation toast (Cover + Title Pages — L3.3). Fixed to
   the viewport so it survives a wholesale form re-render; fades in/out. */
.saved-toast {
  position: fixed; left: 50%; bottom: 24px;
  transform: translateX(-50%) translateY(8px);
  background: var(--color-success-deep); color: #fff;
  padding: 8px 18px; border-radius: var(--radius-pill);
  font-size: 14px; font-weight: 700; box-shadow: var(--shadow-card);
  opacity: 0; pointer-events: none; z-index: 200;
  transition: opacity var(--transition-base) ease, transform var(--transition-base) ease;
}
.saved-toast.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }
/* The locked content region: inert already removes interaction + focus; this
   just signals "read-only" visually (dim + not-allowed cursor). Kept off the
   book-picker, which sits outside [data-proof-lock] so book-switching stays
   live. */
.proof-locked {
  opacity: 0.6;
  pointer-events: none;
  cursor: not-allowed;
  user-select: none;
}

/* Twemoji <img class="emoji"> tags inserted by the server-side
 * replaceEmojiWithImg pass. Sized to surrounding text via 1em height,
 * baseline-aligned. Matches the book pipeline rule in
 * lib/book/profiles/book-base.css so web + print render identically.
 *
 * Selector uses `.emoji.emoji` (doubled class) on purpose: it bumps
 * the specificity to (0,2,1) so any page-level inline <style> rule
 * shaped `<scope> img { width/height: ... }` (specificity 0,1,1)
 * cannot clobber it via source-order. Without the bump, a rule like
 * `.email-body-card img { height: auto }` rendered emoji SVGs at
 * their natural 36-72px size. The book pipeline solves this with
 * !important at book-base.css:171; the web side uses the specificity
 * trick instead, which is reachable without sledgehammering everything
 * via !important. */
img.emoji.emoji {
  height: 1em;
  width: auto;
  vertical-align: -0.1em;
  display: inline-block;
  margin: 0 0.05em;
}

/* BETA pill — used on /book/* surfaces that haven't reached GA yet
 * (Book Structure, Cover, Title Pages, Print). Small uppercase pill in
 * a muted neutral palette so it reads as metadata next to an H1 or
 * sub-nav tab, not as competing UI. Removal is mechanical: drop the
 * <span class="beta-tag">BETA</span> markup at each site (per the
 * post-launch BETA-removal operation in docs/book-restructure-plan.md). */
.beta-tag {
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-gray-500);
  background: var(--color-gray-100);
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-sm);
  padding: 3px 6px;
  margin-left: 8px;
  vertical-align: middle;
}

/* OPTIONAL pill — same visual vocabulary as .beta-tag, used on the 4
 * non-gated cards inside /book/title-pages (Parents' Message, Mission
 * Call, Area Maps, LOA) to signal that those items are not required to
 * order. Title Page does NOT carry the tag (it's the order-gated
 * singleton). Round 16 UX polish — reduces "completion theater". */
.optional-tag {
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-gray-500);
  background: var(--color-gray-100);
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-sm);
  padding: 3px 6px;
  margin-left: 8px;
  vertical-align: middle;
}

/* /book/* sub-nav (docs/book-subnav-redesign-plan.md; supersedes the
 * original docs/book-restructure-plan.md Phase A 5-tab strip). Rendered
 * on every /book/* page by index.js's static-page middleware between
 * <!-- AUTH-NAV --> and <main> as a full-width row mirroring AUTH_NAV's
 * 0 32px horizontal padding. 5 equal-weight tabs (Add Content / Cover /
 * Title Pages / Archive / Print) center-grouped within the strip.
 * Active tab is signalled by `<body data-book-tab="...">` + CSS sibling
 * selectors; no JS.
 *
 * Visual treatment (Pick B, 2026-06-05): the strip gets a `var(--color-gray-50)`
 * background band so the sub-nav reads as a distinct header region, and
 * the active tab gets a white background + 3px black underline — much
 * stronger "you are here" signal than the previous color-only treatment
 * (round-3 user feedback: prior active state was invisible on /book/preview
 * specifically, and the whole strip was too quiet against the page below).
 *
 * Layout: the row centers the [picker + tab nav] group with
 * justify-content:center, and the optional book picker is glued to the LEFT of
 * the first tab (Add Content) — mirroring how the main nav's list switcher sits
 * just left of its links. With a single book the picker is hidden, so only the
 * nav remains and its tabs stay centered exactly as before. The nav sizes to
 * content but may shrink (flex:0 1 auto + min-width:0) so on narrow screens the
 * group fills the bar and the tabs scroll inside the nav (overflow-x), instead
 * of the whole centered group sliding off-screen. The picker stays a SIBLING of
 * the nav (not inside it) so the nav's overflow-x scroll never clips the
 * picker's absolutely-positioned dropdown. */
/* Row wrapping the optional book picker (left) + the centered tab nav. The
 * bar's background/border/margin live on the row so they span the full width.
 * See lib/head.js BOOK_SUBNAV. */
.book-subnav-row {
  display: flex;
  justify-content: center;
  align-items: stretch;
  /* Match the inter-tab gap (.book-subnav gap below) so the picker sits the
   * same distance from Add Content as the tabs sit from each other. */
  gap: 4px;
  padding: 0 16px;
  background: var(--color-gray-50);
  border-bottom: 1px solid var(--color-gray-200);
  margin: 0 0 24px 0;
}
.book-subnav {
  display: flex;
  gap: 4px;
  /* Bar height + vertical-centering of the tabs. Previously inherited from the
     global `nav { height:108px; align-items:center }` in nav.css; that rule is
     now scoped to the classless chrome nav (nav:not([class])), so this <nav>
     sets them explicitly. Heights mirror the chrome nav's responsive steps
     (108 / 80 / 64) so the sub-bar stays the same height as before. */
  height: 108px;
  align-items: center;
  /* Size to content but allow shrink+scroll: keeps the group centered on
   * desktop and lets the tabs scroll within the bar on narrow screens. */
  flex: 0 1 auto;
  min-width: 0;
  margin: 0;
  /* The bar color lives on .book-subnav-row. This inner element is a <nav>,
   * so it must explicitly opt OUT of the global `nav { background:var(--color-ink) }`
   * rule (nav.css) — otherwise the inner nav paints itself black and covers
   * the row's var(--color-gray-50), turning the whole strip black. */
  background: transparent;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.book-subnav a {
  flex: 0 0 auto;
  padding: 12px 16px;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-gray-500);
  text-decoration: none;
  border-bottom: 3px solid transparent;
  white-space: nowrap;
}
.book-subnav a:hover { color: var(--color-ink); }
.book-subnav a:visited { color: var(--color-gray-500); }
body[data-book-tab="layout"] .book-subnav a[data-tab="layout"],
body[data-book-tab="cover"] .book-subnav a[data-tab="cover"],
body[data-book-tab="title-pages"] .book-subnav a[data-tab="title-pages"],
body[data-book-tab="settings"] .book-subnav a[data-tab="settings"],
body[data-book-tab="preview"] .book-subnav a[data-tab="preview"],
body[data-book-tab="manage-books"] .book-subnav a[data-tab="manage-books"] {
  color: var(--color-ink);
  font-weight: 600;
  border-bottom-color: var(--color-ink);
  background: #fff;
}
/* Dirty dot — same loss-AWARENESS cue as the Cover tertiary tabs
   (.cover-tab-dot): the active page's JS toggles .is-dirty on its own sub-nav
   tab while the page has unsaved edits, clearing it on Save/Cancel. Rendered as
   a pseudo-element so the shared server markup (lib/head.js) stays untouched. */
.book-subnav a.is-dirty::after {
  content: "";
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-left: 6px;
  border-radius: 50%;
  background: var(--color-warning);
  vertical-align: middle;
}
/* Responsive bar height — mirrors the chrome nav steps in nav.css (now that the
   sub-nav sets its own height; see .book-subnav above). */
@media (max-width: 900px) { .book-subnav { height: 80px; } }
@media (max-width: 768px) {
  .book-subnav {
    height: 64px;
    /* Scroll cue: all 6 tabs overflow the bar on phones (they scroll via the
       overflow-x:auto above). Fade the right edge so it's visually obvious
       that more tabs (Print / Manage) exist past the viewport, instead of the
       strip looking like it simply ends at whatever tab is last on screen. */
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
  }
  /* The book picker is the most-tapped sub-nav control on phones; give it a
     44px touch target here (desktop keeps its compact 6px-padding pill). */
  .book-picker-btn { min-height: 44px; }
}
@media (max-width: 480px) {
  .book-subnav-row { padding: 0 12px; }
}

/* Operator MARKETING admin sub-nav (lib/head.js ADMIN_MARKETING_SUBNAV).
 * Same structure/behavior as the /book/* sub-nav above: a centered tab strip
 * whose active tab is signalled by `<body data-admin-tab="...">`. Groups the
 * separate marketing tools (Attribution/ROAS, Affiliates, future) into one
 * space. Shorter bar than the book sub-nav — these pages have no picker. */
.admin-subnav-row {
  display: flex;
  justify-content: center;
  align-items: stretch;
  gap: 4px;
  padding: 0 16px;
  background: var(--color-gray-50);
  border-bottom: 1px solid var(--color-gray-200);
  margin: 0 0 24px 0;
}
.admin-subnav {
  display: flex;
  gap: 4px;
  height: 60px;
  align-items: center;
  flex: 0 1 auto;
  min-width: 0;
  margin: 0;
  /* Opt out of the global `nav { background:var(--color-ink) }` (nav.css) like .book-subnav. */
  background: transparent;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
/* Scroll cue on phones — the marketing tools overflow the centered strip; fade
   the right edge (like .book-subnav) so it's visually clear more tabs scroll
   past the viewport rather than the strip simply ending. */
@media (max-width: 768px) {
  .admin-subnav {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
  }
}
.admin-subnav a {
  flex: 0 0 auto;
  padding: 12px 16px;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-gray-500);
  text-decoration: none;
  border-bottom: 3px solid transparent;
  white-space: nowrap;
}
.admin-subnav a:hover { color: var(--color-ink); }
.admin-subnav a:visited { color: var(--color-gray-500); }
body[data-admin-tab="attribution"] .admin-subnav a[data-tab="attribution"],
body[data-admin-tab="affiliates"] .admin-subnav a[data-tab="affiliates"],
body[data-admin-tab="book-orders"] .admin-subnav a[data-tab="book-orders"],
body[data-admin-tab="quote"] .admin-subnav a[data-tab="quote"],
body[data-admin-tab="synthesize"] .admin-subnav a[data-tab="synthesize"] {
  color: var(--color-ink);
  font-weight: 600;
  border-bottom-color: var(--color-ink);
  background: #fff;
}

/* Book picker (left of the sub-nav tabs). Mirrors the nav list-switcher
 * (public/css/nav.css) but styled for the light sub-nav bar. Hidden until
 * public/js/book-nav.js reveals it (list has >1 book). */
/* align-self:center so the picker sizes to the button (not the stretched row
 * height) — otherwise the absolutely-positioned dropdown anchors to the bottom
 * of the full-height row and floats with a gap instead of sitting snug under
 * the button. The button stays vertically centered in the row either way. */
.book-picker { position: relative; display: flex; align-items: center; flex: 0 0 auto; align-self: center; }
.book-picker[hidden] { display: none; }
.book-picker-btn {
  background: #fff;
  border: 1px solid var(--color-gray-200);
  color: var(--color-ink);
  padding: 6px 12px;
  border-radius: var(--radius-lg);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  max-width: 220px;
  font-family: inherit;
}
.book-picker-btn:hover { background: var(--color-gray-100); }
.book-picker-btn:focus-visible { outline: 2px solid var(--color-ink); outline-offset: 2px; }
#bookPickerName { overflow: hidden; text-overflow: ellipsis; }
.book-picker-chevron { display: inline-block; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid currentColor; opacity: 0.7; flex-shrink: 0; margin-top: 1px; }
.book-picker-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  background: #fff;
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-2xl);
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  min-width: 240px;
  z-index: 200;
  overflow: hidden;
}
.book-picker-dropdown.open { display: block; }
.book-picker-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  font-size: 14px;
  color: var(--color-ink);
  cursor: pointer;
  border-bottom: 1px solid var(--color-gray-200);
  gap: 12px;
}
.book-picker-item:last-child { border-bottom: none; }
.book-picker-item:hover { background: var(--color-gray-50); }
.book-picker-item:focus-visible { outline: 2px solid var(--color-ink); outline-offset: -2px; }
.book-picker-item-name { font-weight: 500; flex: 1; }
.book-picker-item.active .book-picker-item-name { font-weight: 700; }
.book-picker-check { color: var(--color-ink); font-size: 14px; }
.book-picker-dropdown .book-picker-manage {
  display: block;
  padding: 10px 16px;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-gray-500);
  text-decoration: none;
  border-top: 1px solid var(--color-gray-200);
  background: var(--color-gray-50);
}
.book-picker-dropdown .book-picker-manage:hover { background: var(--color-gray-100); color: var(--color-ink); }

/* Heading variant — the picker rendered ABOVE the page <h1> (per-book settings
 * pages). A left-aligned block whose button reads like a small heading; the
 * dropdown still anchors under the button. */
.book-picker--heading {
  align-self: flex-start;
  margin: 0 0 20px;
}
/* Extra breathing room below the picker on the per-book editing pages that
 * lead straight into an <h1> (Layout / Cover / Title Pages / Settings / Print).
 * Archive is deliberately excluded — its picker is immediately followed
 * by the photo-quality notice, which supplies its own spacing. */
body[data-book-tab="layout"] .book-picker--heading,
body[data-book-tab="cover"] .book-picker--heading,
body[data-book-tab="title-pages"] .book-picker--heading,
body[data-book-tab="settings"] .book-picker--heading,
body[data-book-tab="preview"] .book-picker--heading {
  margin-bottom: 36px;
}
.book-picker--heading .book-picker-btn {
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-lg);
  padding: 4px 10px 4px 4px;
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.01em;
  /* Cap at the container width + allow wrapping so a long book label doesn't
   * overflow off the right edge on narrow screens (it can't ellipsize at this
   * size — a heading reads fine wrapped). */
  max-width: 100%;
  white-space: normal;
  text-align: left;
  gap: 8px;
}
.book-picker--heading .book-picker-btn:hover { background: var(--color-gray-100); border-color: var(--color-gray-200); }
.book-picker--heading .book-picker-chevron {
  border-left-width: 5px;
  border-right-width: 5px;
  border-top-width: 7px;
}

/* Static single-book heading — the title is shown but the picker is inert
 * (one book = no choice to make). Kill the hover/cursor affordances so it reads
 * as a heading, not a menu button. The chevron is hidden inline by
 * public/js/book-nav.js#renderStaticTitle. */
.book-picker--static .book-picker-btn { cursor: default; pointer-events: none; }
.book-picker--static .book-picker-btn:hover { background: transparent; border-color: transparent; }

/* /book/title-pages 5-card UI (docs/book-restructure-plan.md Phase B).
 * Vertical stack of cards mirroring the printed-book front-matter order.
 * Same visual vocabulary as .structure-card for cross-page consistency. */
.tp-card {
  background: #fff;
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-2xl);
  padding: 24px 24px 20px;
  margin: 0 0 16px;
}
.tp-card header { margin-bottom: 16px; }
.tp-card h2 {
  margin: 0 0 4px;
  font-size: 18px;
  font-weight: 700;
  color: var(--color-ink);
}
.tp-card .section-desc {
  margin: 0;
  font-size: 14px;
  color: var(--color-body);
}
.tp-card-body { display: block; }
.tp-card .empty {
  margin: 0 0 12px;
  font-size: 14px;
  color: var(--color-gray-350);
}
.tp-card--inactive { opacity: 0.5; }

.tp-inclusion { margin: 16px 0 24px; }
.tp-inclusion h2 {
  font-size: 16px;
  font-weight: 700;
  margin: 0 0 8px;
  color: var(--color-ink);
}
.tp-inclusion-list {
  list-style: none;
  padding: 0 0 0 14px; /* indent the checkbox rows under their heading */
  margin: 0;
}
/* Block <li> per row guarantees one-per-line; default inline layout
 * inside the <label> keeps the checkbox + text immediately adjacent
 * regardless of any inherited flex/grid styles from ancestors. */
.tp-inclusion-list li {
  display: block;
  margin: 0;
  padding: 4px 0;
}
.tp-inclusion-list label {
  /* Flex row so the checkbox is vertically centered against the label text
   * regardless of font metrics — more reliable than inline vertical-align,
   * which left the box sitting slightly high relative to the text. */
  display: flex;
  align-items: center;
  cursor: pointer;
  font-size: 14px;
  color: var(--color-ink);
  line-height: 1.4;
}
.tp-inclusion-list input[type="checkbox"] {
  /* Override the global `input, textarea { width: 100% }` rule
   * (line 51) — without this the checkbox stretches to fill the row
   * and pushes the label text onto the next line. Also strip the
   * global 14px padding + 16px bottom margin which gave the checkbox
   * an oversized hit-box and a stray gap below each row. */
  width: auto;
  padding: 0;
  flex-shrink: 0; /* keep the native checkbox size inside the flex label row */
  vertical-align: middle;
  margin: 0 10px 0 0;
}
.tp-inclusion-list .tp-inclusion-label-text {
  vertical-align: middle;
}
/* Locked (always-included) inclusion row — e.g. the Title Page: muted + a small
   "Always included" tag, mirroring the title-page include list. */
.tp-inclusion-list .tp-inclusion-locked { color: var(--color-gray-500); cursor: default; }
.tp-inclusion-list .tp-inclusion-locked input[type="checkbox"] { cursor: default; }
.tp-inclusion-always {
  margin-left: 8px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-gray-400);
  vertical-align: middle;
}
.tp-inclusion-error {
  margin: 8px 0 0;
  color: var(--color-danger-strong);
  font-size: 13px;
}
.tp-inclusion-saved {
  margin: 8px 0 0;
  color: var(--color-success);
  font-size: 13px;
}

.tp-form { display: block; }
.tp-field {
  display: block;
  margin: 0 0 12px;
}
/* Default <fieldset> styling adds a 2px groove border + horizontal
 * padding + 2px inline margin + a quirky min-inline-size that breaks
 * the field's flex/grid math. Strip all of it so a fieldset.tp-field
 * (used by array fields like missions/languages) looks identical to a
 * div.tp-field. */
fieldset.tp-field {
  border: 0;
  padding: 0;
  margin-inline: 0;
  min-inline-size: 0;
}
.tp-field label,
.tp-field legend {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-ink);
  margin: 0 0 4px;
  padding: 0;
}
.tp-field input,
.tp-field textarea {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  font-size: 14px;
  font-family: inherit;
  box-sizing: border-box;
}
.tp-field textarea { resize: vertical; min-height: 80px; }
/* Checkbox fields (Personal Message options) — override the global/.tp-field
   `input { width:100%; padding }` rules so the box stays its natural size and
   sits inline before its label instead of stretching to a full-width block
   that pushes the text onto the next line. */
.tp-field--checkbox label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 500;
  cursor: pointer;
}
.tp-field--checkbox input[type="checkbox"] {
  width: auto;
  margin: 0;
  padding: 0;
  flex: 0 0 auto;
  accent-color: var(--color-ink);
  cursor: pointer;
}

/* The "Include on the title page" list reuses the .tp-inclusion markup/styles
   (h2 + ul of labelled checkboxes), matching the book-level "include these
   pages" list. .tp-include-on-title just trims the top margin since it sits at
   the top of the form, right under the section description. */
.tp-include-on-title { margin-top: 0; }

/* Per-field opt-in extra top margin — used by the Mission Start row on
 * title_page to put visual breathing room between the Mission(s)
 * fieldset above and the Mission Dates pair. */
.tp-field--space-above { margin-top: 24px; }
/* The title_page Mission Dates fieldset reuses .cover-field (whose `margin`
 * shorthand zeroes margin-top); this compound selector out-specifies it to
 * keep the space-above gap below the Mission(s) fieldset. */
.cover-field.tp-field--space-above { margin-top: 24px; }

/* Sub-option of the field above it (e.g. "We will sign our name(s) by hand
 * instead" under Sign Off): indent it and pull it up tight so it reads as a
 * modifier of that field rather than a separate control. --tight trims the
 * 12px bottom-margin gap of the field above. */
.tp-field--indent { padding-left: 24px; }
/* Pull the sub-option checkbox up so it sits directly beneath the field above
 * it — cancels almost all of that field's 12px bottom-margin gap, leaving a
 * hair of separation so it reads as "right below" rather than floating. */
.tp-field--tight { margin-top: -10px; }

/* The "write the whole page by hand" opt-out (Personal Message) gets extra
 * breathing room above + below so it reads as a distinct choice, set apart from
 * the message fields below it. */
.tp-field--blank-page { margin-top: 28px; margin-bottom: 28px; }
/* Pull a field up to tighten the gap above it (Message + Sign Off on the
 * Personal Message form sit closer to the field above). */
.tp-field--tight-top { margin-top: -6px; }

/* Indent "+ Add another mission" / "+ Add another language" and pull it up
 * tight against the input rows so it reads as a sub-action of the fieldset. */
.tp-field [data-tp-array-add] { margin-top: -4px; padding-left: 14px; }
/* Extra whitespace below the optional array fieldset (Language(s) on the title
 * page) so the "+ Add another language" action isn't crowded by the next block
 * (the headshot upload). Only the optional array carries data-tp-array-optional. */
.tp-field[data-tp-array-optional] { margin-bottom: 28px; }

/* Array-field rows (missions / languages on title_page) — flex layout
 * mirroring /book/cover's .cover-array-row so the remove × (and the
 * first-language Clear) sit on the right of the input and align to the
 * existing cover styling. Replaces the prior unstyled stacked layout
 * where the × wrapped below the full-width input. */
.tp-array-row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin: 0 0 8px;
}
.tp-array-row:last-child { margin-bottom: 0; }
.tp-array-row input[type="text"] { flex: 1; }
.tp-array-remove {
  width: 44px;
  height: 44px;
  padding: 0;
  background: #fff;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  color: var(--color-body);
  flex-shrink: 0;
  /* See .cover-array-remove — match the input margin so the × centers, not 8px low. */
  margin-bottom: 16px;
}
.tp-array-remove:hover { background: var(--color-gray-100); color: var(--color-ink); }
.tp-array-clear {
  /* Match the language input's height exactly: stretch to the flex row's
   * cross-size instead of a fixed 44px that stood taller than the field.
   * inline-flex centers the label vertically. The margin-bottom:16px mirrors the
   * input's global margin so stretch resolves to the input's OWN height (not the
   * margin-inflated line cross-size) and the button aligns instead of sitting
   * 16px taller + low. */
  align-self: stretch;
  display: inline-flex;
  align-items: center;
  padding: 0 14px;
  background: #fff;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  color: var(--color-body);
  flex-shrink: 0;
  margin-bottom: 16px;
}
.tp-array-clear:hover { background: var(--color-gray-100); color: var(--color-ink); }

/* Each area_map slot is just a renderUploadBlock + a small Remove
 * link below — no boxed row chrome. Slots stack with the same gap
 * the inputs use. */
.tp-area-map-slot { margin: 0 0 12px; }

/* Per-section load failure — surfaces 429 / 5xx / network blips that
 * would otherwise render as an empty section indistinguishable from
 * a fresh untouched list. Hosts an inline Retry that re-runs the
 * loader and re-paints. */
.tp-load-error {
  margin: 0;
  padding: 8px 10px;
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border-soft);
  border-radius: var(--radius-md);
  color: var(--color-danger-deep);
  font-size: 13px;
}
.tp-load-error .tp-load-retry {
  margin-left: 4px;
  color: var(--color-danger-deep);
}
.tp-load-error .tp-load-retry:hover { color: #7f1d1d; }

.tp-actions {
  display: flex;
  gap: 12px;
  align-items: center;
  /* Generous whitespace above the Save button on the always-edit forms (Title
     Page + Personal Message), separating it from the last field. */
  margin-top: 40px;
}
.tp-error {
  margin: 8px 0 0;
  padding: 8px 10px;
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border-soft);
  border-radius: var(--radius-md);
  color: var(--color-danger-deep);
  font-size: 13px;
}
/* "Saved ✓" status line — green success sibling of the .tp-error treatment. */
.tp-saved {
  margin: 8px 0 0;
  color: var(--color-success);
  font-size: 13px;
}
/* Non-blocking heads-up (amber, distinct from the red .tp-error) — e.g. the
 * Title Page tab's "end date before start date" notice. The save still goes
 * through; this only nudges the customer to double-check. */
.tp-date-warning {
  margin: 8px 0 0;
  padding: 8px 10px;
  background: #fffbeb;
  border: 1px solid var(--color-warning-border);
  border-radius: var(--radius-md);
  color: var(--color-warning-text);
  font-size: 13px;
}
.tp-preview-block {
  margin: 0 0 8px;
  font-size: 14px;
  color: var(--color-ink);
}
.tp-preview-block strong { color: var(--color-ink); font-weight: 600; }
.tp-preview-body { margin-top: 4px; color: var(--color-ink-soft); }

/* Personal Message preview — a book-page-style mock of the saved note
 * (serif page card), shown on the parents_message card in place of the
 * generic tp-preview-block. */
/* Preview card — mirrors the /book/cover "Front Cover Preview" framing
   (gray rounded card + bold heading label) so the Personal Message + Title
   Page previews read as the same kind of book preview. The page INSIDE keeps
   the 6×9 interior-page trim. */
/* Shared preview frame width — the cover (front/back/spine) and the title-pages
 * (personal message / title page) previews all use it so every full-page preview
 * is the SAME physical size (same width → same height, since they share the
 * trim --book-aspect). Spine height derives from it too (see .spine-preview-pane).
 * Sized so TWO previews fit side-by-side in the 640px .container (544px content):
 * 2×250 + 28px gap = 528 ≤ 544. (Wider would wrap the spine+back / linen pairs to
 * a stack.) */
:root { --book-preview-w: 250px; }
.tp-preview-card {
  margin-top: 24px;
  padding: 16px;
  background: var(--color-gray-50);
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-2xl);
  /* Fixed width (was max-width) so the message/title previews don't shrink to
     their content — they must match the fixed-width .cover-preview exactly. */
  width: var(--book-preview-w, 280px);
  max-width: 100%;
}
.tp-preview-card h2 {
  margin: 0 0 12px;
  font-size: 16px;
  font-weight: 700;
  color: var(--color-ink);
  text-align: center;
}
.tp-message-preview-page {
  /* White to match the real printed book page (book-base.css body has no
     background → pages render white); the prior #fdfcf9 cream was preview-only. */
  background: #ffffff;
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-sm);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
  /* Interior-page proportions so the preview reads as a real book page.
     --book-aspect is set per page by JS from the selected book type
     (memory_book 6/9, photo_book 7.44/9.68). Defaults to 6/9. */
  width: 100%;
  aspect-ratio: var(--book-aspect, 6 / 9);
  box-sizing: border-box;
  overflow-y: auto;
  padding: 20px 22px;
  margin: 0;
  /* Positioning context for the absolutely-positioned .tp-preview-loading
     shimmer overlay (inset:0). Anchor it here on the BASE box so the overlay is
     always clamped to the preview page in EVERY state — scaled, rendered
     (.tp-titlepage-rendered), or blank — not only when .tp-preview-scaled is
     present. Without this, states lacking a positioned ancestor let inset:0
     resolve against the viewport and the shimmer covers the whole page. */
  position: relative;
}
.tp-message-preview-body {
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 15px;
  line-height: 1.75;
  color: #2a2a2a;
}
/* Greeting ("Dear Elder Smith,") — same book type as the body, one blank line
   above it. Mirrors .fm-greeting in the rendered book. */
.tp-message-preview-greeting {
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 15px;
  line-height: 1.75;
  color: #2a2a2a;
  margin-bottom: 1.1em;
}
.tp-message-preview-placeholder {
  color: var(--color-gray-400);
  font-size: 14px;
  /* Centered in the 6×9 page so the empty-state copy isn't stranded at
     the top of a tall page. */
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}
/* Personal Message preview variants (docs/personal-message-signoff-plan.md) —
   mirror the book renderer. The centered (sign-by-hand / no sign-off) layout now
   lives on .tp-preview-fit--centered (shared shrink-to-fit content layer). */
/* Right-aligned sign-off, one blank line below the message. padding-right is a
   scaled stand-in for the print page's 10ch inset (this preview box is narrow). */
.tp-message-preview-signoff {
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 15px;
  line-height: 1.75;
  color: #2a2a2a;
  text-align: right;
  padding-right: 1.5em;
  margin-top: 1.75em;
}
/* Disabled (grayed) Personal Message field when sign-by-hand / blank-page is
   checked. Value is retained; this only dims the field + its label. */
.tp-field--disabled {
  opacity: 0.5;
}

/* Attachment-card view — the single definition, used by /email-group and by
 * /book/title-pages (Mission Call / LOA / Area Map PDF cards + the headshot
 * photo card). /email-group carried a byte-identical inline copy until the
 * round-3 consolidation (2026-07-31) resolved this block's old "keep them in
 * sync" NOTE the other way, by deleting the copy; its page <style> now holds
 * only its genuine divergences (.attachment-card-body's flex basis, the
 * incompat/low-res accents).
 * Thumb slot is 240×240 fixed so the card row stays stable across image / PDF /
 * placeholder kinds, matching /photo-group + /pdf-group (owner preference:
 * thumbs prominent enough for at-a-glance recognition). */
.attachment-card {
  border: 1px solid var(--color-gray-200); border-radius: var(--radius-lg); padding: 10px 14px; background: #fff;
  display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap;
}
.attachment-card .attachment-thumb-wrapper { position: relative; flex: 0 0 auto; width: 240px; max-width: 100%; height: 240px; }
.attachment-card .attachment-thumb { width: 240px; max-width: 100%; height: 240px; object-fit: cover; border-radius: var(--radius-sm); background: var(--color-gray-100); display: block; }
.attachment-card .attachment-thumb-placeholder {
  width: 240px; max-width: 100%; height: 240px; box-sizing: border-box;
  border: 1px solid var(--color-gray-200); border-radius: var(--radius-sm);
  background: var(--color-gray-100); display: flex; flex-direction: column; gap: 12px;
  align-items: center; justify-content: center;
  color: var(--color-gray-400); font-size: 24px; font-weight: 600;
}
.attachment-card .attachment-thumb-regen {
  font-size: 13px; font-weight: 500; line-height: 1;
  padding: 7px 12px; border-radius: var(--radius-md);
  border: 1px solid var(--color-gray-300); background: #fff; color: var(--color-gray-700); cursor: pointer;
}
.attachment-card .attachment-thumb-regen:hover:not(:disabled) { background: var(--color-gray-100); color: var(--color-ink); }
.attachment-card .attachment-thumb-regen:disabled { color: var(--color-gray-400); cursor: default; }
.attachment-card-body { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.attachment-card .filename { font-size: 13px; color: var(--color-ink); font-weight: 500; word-break: break-word; }
/* Titled variant (headshot): title heading on top, image + controls below. */
.attachment-card--titled { flex-direction: column; align-items: stretch; }
.attachment-card--titled .attachment-card-heading { margin-bottom: 2px; }
.attachment-card--titled .attachment-card-row { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
.attachment-card .meta { font-size: 12px; color: var(--color-gray-500); }
.attachment-card .controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* First-page overlay controls on a filled PDF card (mission_call / loa /
   area_map / other_document). docs/pdf-first-page-title-overlay-plan.md. */
/* Own full-width row below the thumb + body (flex-basis:100% wraps it onto its
   own line within the flex-wrap .attachment-card), so the label never gets
   squeezed into the narrow body column and wrapped word-by-word. */
.tp-overlay-controls {
  flex-basis: 100%; display: flex; flex-direction: column; gap: 6px;
  margin-top: 8px; padding-top: 10px; border-top: 1px solid var(--color-gray-100);
}
.tp-overlay-label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; line-height: 1.3; color: var(--color-gray-600); cursor: pointer; user-select: none;
}
/* width/padding:auto/0 undo the global `input { width: 100% }` rule (line 239)
   that would otherwise stretch the checkbox across the whole label and shove
   the text off to the right. */
.tp-overlay-label input { width: auto; padding: 0; margin: 0; flex: none; cursor: pointer; }
.tp-overlay-title {
  font-size: 13px; padding: 4px 8px; border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-sm); max-width: 320px;
}
.tp-overlay-status { font-size: 12px; color: #059669; min-height: 14px; }
.tp-overlay-status.error { color: var(--color-danger-strong); }
.attachment-card .btn-open {
  width: 32px; height: 32px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--color-gray-300); background: #fff;
  color: var(--color-gray-700); border-radius: var(--radius-sm); text-decoration: none;
}
.attachment-card .btn-open:hover { background: var(--color-gray-100); }
.attachment-card .btn-open svg { display: block; }
/* Title-pages card text actions (Replace / Remove) — sized to the 32px icon
 * buttons so the controls row sits on one line. */
.attachment-card .tp-card-action {
  height: 32px; box-sizing: border-box; padding: 0 12px;
  display: inline-flex; align-items: center;
  font-size: 13px; font-weight: 500;
  border: 1px solid var(--color-gray-300); background: #fff; color: var(--color-gray-700);
  border-radius: var(--radius-sm); cursor: pointer; text-decoration: none;
}
.attachment-card .tp-card-action:hover { background: var(--color-gray-100); }
.attachment-card .tp-card-action--danger { border-color: var(--color-danger-border-soft); color: var(--color-danger-strong); }
.attachment-card .tp-card-action--danger:hover { background: var(--color-danger-bg); }

/* Shrink-to-fit preview box + content layer — shared by the Title Page and
   Personal Message previews (fitPreview() in singleton.js). The box is a fixed
   trim-proportioned page (it keeps .tp-message-preview-page's aspect-ratio) that
   never scrolls; padding is 0 so the content layer measures cleanly against it.
   The .tp-preview-fit layer is in-flow with min-height = the box, so it FILLS
   the page when content is short and grows taller when content needs it; the JS
   fitter then scales it from the top to fit. A transform doesn't change layout
   height, so the overflow is clipped by the box while the scaled content stays
   fully visible. */
.tp-preview-scaled { overflow: hidden; position: relative; padding: 0; }
.tp-preview-fit {
  min-height: 100%;
  box-sizing: border-box;
  padding: 20px 22px;
  transform-origin: top center;
}
/* Centered message variant (sign-by-hand / no sign-off) — greeting + message
   centered horizontally + vertically with 2 blank lines for a hand signature. */
.tp-preview-fit--centered {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
}

/* Title Page preview — book-page mock. Mirrors the rendered book: eyebrow +
   small-caps name + paper-plane divider + mission / language / dates (optional
   headshot below), with the Church attribution pinned to the bottom via
   space-between. Serif (Georgia) approximates the print Source Serif Pro. The
   box/scale behavior comes from .tp-preview-scaled / .tp-preview-fit above. */
.tp-titlepage-preview {
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-family: Georgia, 'Times New Roman', serif;
}
.tp-titlepage-main { padding-top: 22px; }
.tp-titlepage-eyebrow { text-transform: uppercase; letter-spacing: 0.2em; font-size: 11px; color: var(--color-body); margin-bottom: 8px; }
.tp-titlepage-name { font-variant: small-caps; font-weight: 700; font-size: 28px; line-height: 1.05; color: #2a2a2a; }
.tp-titlepage-divider { display: flex; align-items: center; justify-content: center; gap: 8px; width: 70%; margin: 10px auto 14px; }
.tp-titlepage-divider span { flex: 1 1 auto; height: 1px; background: var(--color-ink-soft); max-width: 60px; }
.tp-titlepage-plane { width: 16px; height: 16px; color: var(--color-ink-soft); flex: 0 0 auto; }
.tp-titlepage-mission { text-transform: uppercase; letter-spacing: 0.12em; font-weight: 700; font-size: 13px; color: #2a2a2a; margin-bottom: 6px; }
.tp-titlepage-langs { font-style: italic; font-size: 13px; color: var(--color-gray-500); margin-bottom: 6px; }
.tp-titlepage-dates { text-transform: uppercase; letter-spacing: 0.12em; font-size: 12px; color: var(--color-body); }
.tp-titlepage-photo { width: 84px; height: 84px; object-fit: cover; border-radius: 50%; margin: 16px auto 0; display: block; }
.tp-titlepage-church-pre { text-transform: uppercase; letter-spacing: 0.14em; font-size: 10px; color: #444; }
.tp-titlepage-church-name { font-variant: small-caps; font-weight: 700; font-size: 16px; line-height: 1.15; color: #2a2a2a; }
.tp-titlepage-church-post { text-transform: uppercase; letter-spacing: 0.1em; font-size: 9px; color: #444; }

/* Server-rendered front-matter preview (true WYSIWYG) — the faithful JPEG from
   the title_page / parents_message preview endpoints (singleton.js
   swapServerPreviewImage) replaces the client mock above (Title Page + Personal
   Message). The page box keeps its --book-aspect; the image (already the
   finished trim) fills it exactly. */
.tp-message-preview-page.tp-titlepage-rendered { overflow: hidden; }
.tp-titlepage-render-img { display: block; width: 100%; height: 100%; object-fit: contain; cursor: pointer; }

/* Loading shimmer shown OVER the instant client mock while the faithful server
   render is fetched + decoded (showPreviewLoading in singleton.js), so the
   rougher mock never flashes before the real render lands. Fills the page box
   (position:relative on .tp-message-preview-page — the base box, so the overlay
   is clamped in every state, not only .tp-preview-scaled); removed on success
   (image swap) or failure (mock revealed). Reuses the @keyframes shimmer from
   the skeleton loader above. */
.tp-preview-loading {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(90deg, var(--color-gray-75) 25%, var(--color-gray-200) 50%, var(--color-gray-75) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite linear;
}
.tp-preview-loading-label { color: var(--color-gray-400); font-size: 13px; }
@media (prefers-reduced-motion: reduce) {
  .tp-preview-loading { animation: none; background: var(--color-gray-100); }
}

.btn-link {
  background: none;
  border: none;
  padding: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-gray-500);
  cursor: pointer;
  text-decoration: underline;
}
/* Accent variant — blue action link (View / back-to-roster style controls).
   Blue here is the info/action accent, never a primary fill. */
.btn-link--accent { color: var(--color-info); font-size: 14px; }
/* Reset the global `button:hover { background: var(--color-ink-soft) }` — without this
 * a <button class="btn-link"> picks up the primary-button hover bg
 * on hover (Cancel + "+ Add another mission" on /book/cover both
 * had this). Matches button.muted-link:hover above. */
.btn-link:hover { color: var(--color-ink); background: none; }
/* A disabled .btn-link (e.g. "Don't want to Broadcast?" inside a locked
   teaser for a no/standard-list user) must stay a plain text link — without
   this, `button:disabled { background:var(--color-gray-350) }` paints a gray highlight box
   behind it. Higher specificity than button:disabled, so it wins. */
.btn-link:disabled { background: none; cursor: default; }

/* /book/cover customization form (docs/book-restructure-plan.md Phase B,
 * Phase 6 UI). 2-column layout on desktop (form left, preview right),
 * single column below 720px. */
.cover-form { display: block; }
/* Tertiary nav (Cover / Front Cover / Spine + Back Cover) — a segmented tablist
 * above the three panes (docs/cover-tertiary-nav-plan.md). Matches the /book/*
 * card vocabulary. */
/* The .tp-* twins (/book/title-pages tertiary tabs, docs/title-pages-tabs-plan.md)
 * share these exact rule blocks via grouped selectors — same segmented-tab chrome,
 * no duplicated styling, cover markup/classes untouched. */
.cover-tabs,
.tp-tabs {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin: 0 0 20px;
  border-bottom: 1px solid var(--color-gray-200);
}
.cover-tab,
.tp-tab {
  appearance: none;
  background: transparent; /* explicit — never inherit a dark button bg */
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  margin-bottom: -1px; /* overlap the row border so the active tab joins its pane */
  padding: 10px 16px;
  font: inherit;
  font-weight: 600;
  color: var(--color-gray-600);
  cursor: pointer;
}
/* Re-declare the :hover background — single-class button :hover otherwise leaks to
 * the global var(--color-ink-soft) hover (see feedback_button_hover_dark_leak). */
.cover-tab:hover,
.tp-tab:hover { background: var(--color-gray-100); color: #1f2937; }
.cover-tab.is-active,
.cover-tab.is-active:hover,
.tp-tab.is-active,
.tp-tab.is-active:hover {
  background: #fff;
  color: #1f2937;
  border-color: var(--color-gray-200);
  border-bottom-color: #fff;
}
.cover-tab:focus-visible,
.tp-tab:focus-visible { outline: 2px solid var(--color-info); outline-offset: 2px; }
/* Dirty dot — a loss-AWARENESS cue; hidden until the pane has unsaved edits. */
.cover-tab-dot,
.tp-tab-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-left: 6px;
  border-radius: 50%;
  background: var(--color-warning);
  vertical-align: middle;
  visibility: hidden;
}
.cover-tab--dirty .cover-tab-dot,
.tp-tab--dirty .tp-tab-dot { visibility: visible; }
/* Only the active pane shows (also covered by the global [hidden] rule;
 * kept for parity + intent). */
.cover-pane[hidden],
.tp-pane[hidden] { display: none; }
@media (max-width: 480px) {
  /* The tertiary tabs scroll horizontally here; fade the right edge as a scroll
     cue that more tabs exist past the viewport (matches .book-subnav). */
  .cover-tabs,
  .tp-tabs {
    flex-wrap: nowrap; overflow-x: auto;
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
  }
  .cover-tab,
  .tp-tab { white-space: nowrap; padding: 10px 12px; }
}

/* Progressive enhancement for the four scroll-fade sub-navs above (.book-subnav,
 * .admin-subnav, .cover-tabs, .tp-tabs). The always-on masks above fade the right
 * edge even when the strip FITS (CSS alone can't tell "scrollable" from "just fits"),
 * which slightly dims the last tab on a shrink-to-fit bar that doesn't overflow.
 * Where scroll-driven animations exist, drive the fade off the element's own inline
 * scroll: while the bar is genuinely scrollable the timeline is active and the
 * animation paints the fade; when the content fits, the scroll timeline is INACTIVE,
 * so the animation's effect is not applied and the base `mask-image: none` wins — no
 * false fade. Browsers without scroll-driven animations ignore this block entirely and
 * keep the always-on mask above (strict no-regression). Longhand (no `animation`
 * shorthand) so `animation-duration` stays at its `auto` initial — a scroll timeline
 * fills its whole range, which a shorthand-reset 0s would break. */
@keyframes subnav-scroll-fade {
  from, to {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--subnav-fade, 24px)), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - var(--subnav-fade, 24px)), transparent);
  }
}
@supports (animation-timeline: scroll()) {
  @media (max-width: 768px) {
    .book-subnav,
    .admin-subnav {
      --subnav-fade: 24px;
      -webkit-mask-image: none;
      mask-image: none;
      animation-name: subnav-scroll-fade;
      animation-fill-mode: both;
      animation-timeline: scroll(self x);
    }
  }
  @media (max-width: 480px) {
    .cover-tabs,
    .tp-tabs {
      --subnav-fade: 20px;
      -webkit-mask-image: none;
      mask-image: none;
      animation-name: subnav-scroll-fade;
      animation-fill-mode: both;
      animation-timeline: scroll(self x);
    }
  }
}

/* Each /book/cover section is its own card, matching the .tp-card visual
 * vocabulary used on /book/title-pages. */
.cover-card {
  background: #fff;
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-2xl);
  padding: 24px 24px 20px;
  margin: 0 0 16px;
}
/* Staged (picked-but-unsaved) cover-photo thumbnail — a local object-URL
 * preview shown until Save commits it. Constrained so a large source image
 * doesn't blow out the card. */
.cover-photo-staged { margin: 10px 0 4px; }
.cover-photo-staged img {
  max-width: 160px;
  max-height: 200px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-gray-200);
  display: block;
}
/* The Spine card sits below the previews row — add breathing room so the
 * spine section visually separates from the previews rather than abutting
 * them. */
.cover-previews-row + .cover-card { margin-top: 32px; }
/* Two previews sit side-by-side (spine + back, or the linen pair). flex-start
 * so the panes top-align under their identical h2s. wrap so on narrow widths
 * the second pane stacks under the first. The row also breathes away from the
 * Save actions above it, and stacked rows (dust-jacket pair, then the linen
 * pair on Premium 6×9) are spaced from each other. */
.cover-previews-row {
  display: flex;
  align-items: flex-start;
  gap: 28px;
  flex-wrap: wrap;
  margin-top: 24px;
}
.cover-previews-row + .cover-previews-row { margin-top: 32px; }
.cover-previews-row .cover-preview { margin-top: 0; }
.cover-card-title {
  margin: 0 0 16px;
  font-size: 18px;
  font-weight: 700;
  color: var(--color-ink);
}
/* Sub-heading inside a card (e.g. "Spine foil lettering color" within the
 * Linen spine text card on /book/cover). Smaller than the card title. */
.cover-card-subtitle {
  margin: 0 0 6px;
  font-size: 16px;
  font-weight: 700;
  color: var(--color-ink);
}
.cover-field { margin: 0 0 32px; }
/* Extra breathing room above the "Lettering vertical placement" slider so it
 * reads as its own control, separated from the lettering-color swatches +
 * contrast warning above it. Compound selector out-specifies the `margin`
 * shorthand's margin-top:0. */
.cover-field.cover-lettering-placement { margin-top: 24px; }
/* Same fieldset reset as .tp-field above — applies to the Mission(s)
 * + Mission Dates fieldsets on /book/cover. */
fieldset.cover-field {
  border: 0;
  padding: 0;
  margin-inline: 0;
  min-inline-size: 0;
}
/* Toggled by handleCoverInclusionToggle on /book/cover when the
 * matching inclusion checkbox is unchecked. Opacity-only — mirrors
 * .tp-card--inactive (line 517). Fields stay editable. */
.cover-field--inactive { opacity: 0.5; }

/* /book/cover inclusion checkbox list — mirrors the /book/title-pages
 * tp-inclusion-list styling, shifted to the cover namespace. */
.cover-inclusion { margin: 0 0 32px; }
.cover-inclusion h3 {
  font-size: 16px;
  font-weight: 700;
  margin: 0 0 8px;
  color: var(--color-ink);
}
.cover-inclusion-list {
  list-style: none;
  padding: 0 0 0 14px; /* indent the checkbox rows under their heading */
  margin: 0;
}
.cover-inclusion-list li {
  display: block;
  margin: 0;
  padding: 4px 0;
}
.cover-inclusion-list label {
  display: inline-block;
  cursor: pointer;
  font-size: 14px;
  color: var(--color-ink);
  line-height: 1.4;
}
.cover-inclusion-list input[type="checkbox"] {
  /* Override the global `input, textarea { width: 100% }` rule the same
   * way .tp-inclusion-list does — without this the checkbox stretches
   * across the row and pushes the label text onto a second line. */
  width: auto;
  padding: 0;
  vertical-align: middle;
  margin: 0 10px 0 0;
}
.cover-inclusion-error {
  margin: 8px 0 0;
  color: var(--color-danger-strong);
  font-size: 13px;
}
.cover-field > label,
.cover-field > legend,
.cover-field > .cover-field-label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-ink);
  margin: 0 0 6px;
}
.cover-field input[type="text"],
.cover-field input[type="month"] {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  font-size: 14px;
  font-family: inherit;
  box-sizing: border-box;
}
.cover-helper {
  margin: 0 0 12px;
  font-size: 12px;
  color: var(--color-gray-500);
}
/* Card-level help copy (JS-injected across the cover editor). */
.cover-card-help {
  margin: 0 0 12px;
  font-size: 12px;
  color: var(--color-gray-500);
}
/* Low-res photo warning: amber, per the semantic-accent rules (warnings are
   never muted gray). Declared after .cover-helper so it wins on elements
   carrying both classes. */
.cover-photo-warning { color: var(--color-warning-text); }
/* Indent helper text + the array "Add another" action so they sit under their
   input/label rather than flush to the field's left edge. */
.cover-helper--indent { padding-left: 14px; }
.cover-array-add { padding-left: 14px; }
/* End Date "Clear" — sits flush-right of the End Date label, opposite it. */
.cover-subfield-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.cover-date-clear { font-size: 12px; flex: 0 0 auto; }

/* /book/cover color picker — 6-swatch grid populated from
 * lib/book/cover-options.js (Lulu hardcover linen wrap palette).
 * Each swatch is a hidden radio input + a visible label wrapping a
 * colored thumbnail and the color name; clicking anywhere on the
 * label selects the radio. Selected state shown by border + ring
 * around the thumbnail. Layout: 3 columns at desktop, 2 at narrow
 * widths so each thumbnail stays a usable target. */
.cover-swatch-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}
@media (max-width: 480px) {
  .cover-swatch-grid { grid-template-columns: repeat(2, 1fr); }
}
.cover-swatch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px;
  border: 2px solid transparent;
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: border-color var(--transition-fast), background var(--transition-fast);
  /* Anchor the absolutely-positioned hidden radio to the swatch itself.
   * Without this, the radio's containing block is the viewport and its
   * inherited width: 100% (global input rule) becomes 100vw, leaking a
   * page-wide ghost input that scrolls into blank whitespace. */
  position: relative;
}
.cover-swatch:hover { background: var(--color-gray-50); }
.cover-swatch input[type="radio"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  /* Override the global `input, textarea { width: 100% }` rule the same
   * way .cover-inclusion-list does — otherwise the hidden radio stretches
   * to 100% of its containing block and overflows the page. */
  width: auto;
}
.cover-swatch-visual {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  max-width: 72px;
  border-radius: var(--radius-md);
  border: 1px solid rgba(0, 0, 0, 0.1);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
}
/* Unavailable swatch — shown but disabled with a soft X (e.g. White cover, or
   Navy/Gray lettering, on a linen Premium: Lulu has no white linen / navy-gray
   foil). The radio also carries `disabled` so it can't be picked. The X is two
   soft diagonal lines drawn over a light veil so it reads on light AND dark
   swatches. */
.cover-swatch--unavailable { cursor: default; }
.cover-swatch--unavailable:hover { background: none; }
.cover-swatch--unavailable .cover-swatch-label { color: var(--color-gray-400); }
.cover-swatch--unavailable .cover-swatch-visual::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
  background:
    linear-gradient(to top right, transparent 46%, rgba(107, 114, 128, 0.85) 46%, rgba(107, 114, 128, 0.85) 54%, transparent 54%),
    linear-gradient(to bottom right, transparent 46%, rgba(107, 114, 128, 0.85) 46%, rgba(107, 114, 128, 0.85) 54%, transparent 54%),
    rgba(255, 255, 255, 0.5);
}
.cover-swatch-label {
  font-size: 13px;
  font-weight: 500;
  color: var(--color-gray-700);
}
/* "No Box" chip (2026-07-02, photo cover box picker) — a transparent/empty
   well: a light checkerboard with a soft diagonal slash so it reads as "none"
   rather than a color. Mirrors the .cover-swatch-visual box shape. */
.cover-swatch-visual--nobox {
  background:
    linear-gradient(to top right, transparent 46%, rgba(107, 114, 128, 0.7) 46%, rgba(107, 114, 128, 0.7) 54%, transparent 54%),
    conic-gradient(var(--color-gray-100) 0 25%, var(--color-gray-200) 0 50%, var(--color-gray-100) 0 75%, var(--color-gray-200) 0) 0 0 / 16px 16px;
}
/* Front box transparency slider (photo covers). Width + margin now come from
 * the shared .cover-range rule below, so this slider is full-width and visually
 * identical to the Lettering-vertical-placement slider (no 320px cap). */
.cover-box-transparency { margin-top: 24px; }
.cover-box-transparency .cover-card-subtitle { display: block; }

/* Custom range sliders on /book/cover (Lettering vertical placement + Box
 * transparency). Native sliders inset the thumb by half its width at each end,
 * so 0% / 100% never look like they reach the track ends. This restyle draws a
 * full-width track with a fill driven by --range-pct (kept in sync in
 * book-cover.js syncRangeFill) so the fill runs edge-to-edge, plus a thumb that
 * rests flush against the ends. Both sliders carry .cover-range. */
.cover-range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 18px;            /* pointer/hit area; the 6px track sits centered */
  margin: 6px 0 2px;
  background: transparent;  /* the track pseudo-element paints the bar */
  cursor: pointer;
}
.cover-range:focus { outline: none; }
/* Track + edge-to-edge fill — WebKit/Blink (Chrome, Edge, Safari) */
.cover-range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background-color: var(--color-gray-300);
  background-image: linear-gradient(var(--color-info), var(--color-info));
  background-repeat: no-repeat;
  background-size: var(--range-pct, 0%) 100%;
}
.cover-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  margin-top: -5px;        /* center the 16px thumb on the 6px track */
  border-radius: 50%;
  background: #ffffff;
  border: 2px solid var(--color-info);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}
.cover-range:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--color-info);
  outline-offset: 2px;
}
/* Track + native fill (::-moz-range-progress) — Firefox */
.cover-range::-moz-range-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-gray-300);
}
.cover-range::-moz-range-progress {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-info);
}
.cover-range::-moz-range-thumb {
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  border: 2px solid var(--color-info);
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}
.cover-range:focus-visible::-moz-range-thumb {
  outline: 2px solid var(--color-info);
  outline-offset: 2px;
}
.cover-swatch:has(input[type="radio"]:checked) {
  border-color: var(--color-ink);
  background: var(--color-gray-100);
}
.cover-swatch:has(input[type="radio"]:checked) .cover-swatch-label {
  color: var(--color-ink);
  font-weight: 600;
}
.cover-swatch:has(input[type="radio"]:focus-visible) {
  outline: 2px solid var(--color-info);
  outline-offset: 2px;
}

/* Custom color picker (2026-07-03). The visible swatch is a trigger button that
   opens OUR OWN popover picker (saturation/value box + hue slider + hex field) —
   NOT the native OS picker, so the hex field lives inside the picker. Only the
   hidden radio carries the value. */
.cover-swatch--custom { position: relative; }
.cover-custom-trigger {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  /* Square to match the preset .cover-swatch-visual chips. */
  width: 72px;
  height: 72px;
  max-width: 100%;
  margin: 0;
  padding: 0;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
  cursor: pointer;
}
/* The picker popover, anchored under the swatch. Absolutely positioned so it
   floats over the grid; starts hidden until the trigger opens it. */
.cover-custom-pop {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  width: 200px;
  padding: 12px;
  background: #fff;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-xl);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
  cursor: default;
}
/* Little pointer triangle at the top of the popover. */
.cover-custom-pop::before {
  content: "";
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 7px solid transparent;
  border-bottom-color: #fff;
  filter: drop-shadow(0 -1px 0 var(--color-gray-300));
}
/* Saturation (x) / value (y) box. Its layered gradient background is set in JS
   per current hue. A round thumb marks the current s/v. */
.cover-custom-pop .cc-sv {
  position: relative;
  width: 100%;
  height: 130px;
  border-radius: var(--radius-md);
  border: 1px solid rgba(0, 0, 0, 0.12);
  cursor: crosshair;
  touch-action: none;
}
.cover-custom-pop .cc-sv-thumb {
  position: absolute;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
/* Hue bar — a rainbow track with a percent-positioned thumb (NOT a native range
   input, whose thumb is inset by half its width and drifts off the gradient). The
   thumb can sit at 0%/100% via translateX(-50%), so it lands exactly on its hue. */
.cover-custom-pop .cc-hue {
  position: relative;
  width: 100%;
  height: 12px;
  margin: 12px 0 10px;
  border-radius: var(--radius-md);
  background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
  cursor: pointer;
  touch-action: none;
}
.cover-custom-pop .cc-hue:focus-visible {
  outline: 2px solid var(--color-info);
  outline-offset: 2px;
}
.cover-custom-pop .cc-hue-thumb {
  position: absolute;
  top: 50%;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.35);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
/* Hex row — a preview chip + the hex text field, inside the picker. */
.cover-custom-pop .cc-hexrow {
  display: flex;
  align-items: center;
  gap: 8px;
}
.cover-custom-pop .cc-preview {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-md);
  border: 1px solid rgba(0, 0, 0, 0.15);
}
.cover-custom-pop .cc-hexrow input[type="text"] {
  flex: 1 1 auto;
  box-sizing: border-box;
  /* min-width:0 lets the input shrink inside the flex row — without it the input's
     intrinsic width keeps it from shrinking and it overflows the popover. */
  min-width: 0;
  width: auto;
  height: 30px;
  margin: 0;
  padding: 0 8px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  text-transform: lowercase;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
}
.cover-custom-pop .cc-hexrow input[type="text"]:focus-visible {
  outline: 2px solid var(--color-info);
  outline-offset: 1px;
}
.cover-custom-pop .cc-hexrow input.cover-custom-hex--invalid {
  border-color: var(--color-danger);
  outline-color: var(--color-danger);
}
/* R/G/B number fields — three equal columns under the hex row. */
.cover-custom-pop .cc-rgbrow {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.cover-custom-pop .cc-rgb {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1 1 0;
  min-width: 0;
  font-size: 12px;
  font-weight: 600;
  color: var(--color-gray-500);
}
.cover-custom-pop .cc-rgb input[type="number"] {
  box-sizing: border-box;
  min-width: 0;
  width: 100%;
  height: 28px;
  margin: 0;
  padding: 0 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  font-weight: 400;
  text-align: center;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  /* Drop the spinner arrows — they steal width and clip 3-digit values (255). */
  -webkit-appearance: none;
  -moz-appearance: textfield;
  appearance: textfield;
}
.cover-custom-pop .cc-rgb input[type="number"]::-webkit-outer-spin-button,
.cover-custom-pop .cc-rgb input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.cover-custom-pop .cc-rgb input[type="number"]:focus-visible {
  outline: 2px solid var(--color-info);
  outline-offset: 1px;
}
.cover-custom-note {
  margin-top: 10px;
}

/* Cover Style chooser — two CSS-drawn book thumbnails (Photo / Solid color).
 * The radio is visually hidden; selection shows as a highlighted card. */
.cover-style-thumbs {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
}
.cstyle-thumb {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 12px;
  border: 2px solid var(--color-gray-200);
  border-radius: var(--radius-xl);
  cursor: pointer;
  background: #fff;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}
.cstyle-thumb:hover { background: var(--color-gray-50); }
.cstyle-thumb input[type="radio"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.cstyle-thumb.is-selected,
.cstyle-thumb:has(input[type="radio"]:checked) {
  border-color: var(--color-ink);
  background: var(--color-gray-100);
}
.cstyle-thumb:has(input[type="radio"]:focus-visible) {
  outline: 2px solid var(--color-info);
  outline-offset: 2px;
}
/* The mini book cover (2:3-ish) — frames the inline SVG mockup. */
.cstyle-thumb-art {
  position: relative;
  display: block;
  width: 86px;
  height: 116px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.cstyle-thumb-svg {
  display: block;
  width: 100%;
  height: 100%;
}
.cstyle-thumb-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--color-gray-700);
}
.cstyle-thumb.is-selected .cstyle-thumb-label,
.cstyle-thumb:has(input[type="radio"]:checked) .cstyle-thumb-label { color: var(--color-ink); }

.cover-array-row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin: 0 0 8px;
}
/* Tighten the gap between the last mission input and the helper text
 * below it — keeps between-row spacing at 8px but drops the trailing
 * gap so the helper sits closer to the input. */
.cover-array-row:last-child { margin-bottom: 0; }
.cover-array-row input[type="text"] { flex: 1; }
/* 44×44 to match its twin .tp-array-remove — a comfortable touch target and
   consistent with the identical remove control on the Title Pages array rows. */
.cover-array-remove {
  width: 44px;
  height: 44px;
  padding: 0;
  background: #fff;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  color: var(--color-body);
  flex-shrink: 0;
  /* Match the input's global margin-bottom:16px so align-items:center aligns the
   * × on equal margin-boxes — otherwise the marginless button parks ~8px low. */
  margin-bottom: 16px;
}
.cover-array-remove:hover { background: var(--color-gray-100); color: var(--color-ink); }

.cover-grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
@media (max-width: 480px) {
  .cover-grid-2 { grid-template-columns: 1fr; }
}

/* Sub-label inside the Mission Dates fieldset — smaller weight than the
 * fieldset legend so the visual hierarchy reads as one date range, not
 * two unrelated date inputs. */
.cover-subfield > label {
  display: block;
  font-size: 12px;
  font-weight: 500;
  color: var(--color-gray-700);
  margin: 0 0 4px;
}
.cover-subfield input[type="month"] {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  font-size: 14px;
  font-family: inherit;
  box-sizing: border-box;
}

/* /book/cover mission-date picker: keeps native <input type="month">
 * (and its picker) but layers a custom-formatted overlay ("June 2024")
 * over it. The input is opacity:0 + absolute over the entire field, so
 * clicks pass through to it and open the native picker; the overlay
 * span renders the visible value via .cover-date-display, kept in sync
 * by syncDateDisplay() in book-cover.js. Placeholder for empty state
 * comes from .cover-date-display:empty::before — data-placeholder
 * attribute on the span sets the copy. */
.cover-subfield .cover-date-field {
  position: relative;
  width: 100%;
  background: #fff;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  padding: 8px 10px;
  box-sizing: border-box;
  cursor: pointer;
  min-height: 38px;
  display: flex;
  align-items: center;
  font-size: 14px;
  font-family: inherit;
}
.cover-subfield .cover-date-field:focus-within { border-color: var(--color-ink); }
.cover-date-display {
  color: var(--color-ink);
  pointer-events: none;
}
.cover-date-display:empty::before {
  content: attr(data-placeholder);
  color: var(--color-gray-400);
}
.cover-subfield .cover-date-field input[type="month"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  border: 0;
  padding: 0;
  margin: 0;
  background: transparent;
  font: inherit;
}
.cover-date-error {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--color-danger-strong);
}

/* The save-row under a form: action button(s) plus an inline status line.
   Consolidated 2026-07-31 (round 3, R3-19) — this exact four-declaration rule
   existed under THREE names, `.cover-actions` here plus `.layout-actions` and
   `.settings-actions` hand-rolled in /book/layout and /book/settings. Named for
   what it is rather than where it started, so the next form composes it instead
   of minting a fourth name. */
.form-actions {
  display: flex;
  gap: 12px;
  align-items: center;
  margin-top: 32px;
}
.cover-error {
  margin: 12px 0 0;
  padding: 8px 10px;
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border-soft);
  border-radius: var(--radius-md);
  color: var(--color-danger-deep);
  font-size: 13px;
}

.cover-preview {
  margin-top: 24px;
  padding: 16px;
  background: var(--color-gray-50);
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-2xl);
  /* Fixed frame width (border-box → 248px content) so every preview is the
   * SAME size regardless of heading length. Without this, each .cover-preview
   * flex item sizes to its content's intrinsic width — and since the panes are
   * width:100% (zero intrinsic contribution), the heading text alone drove the
   * frame width, making "Linen Back Cover Preview" smaller than the longer
   * "Dust Jacket Back Cover Preview" and the spine frames mismatch row-to-row.
   * max-width:100% keeps it responsive (shrinks equally on narrow viewports).
   * --book-preview-w is shared with .tp-preview-card so the two pages' previews
   * are the same physical size. */
  width: var(--book-preview-w, 280px);
  max-width: 100%;
}
.cover-preview h2 {
  margin: 0 0 12px;
  font-size: 16px;
  font-weight: 700;
  color: var(--color-ink);
  text-align: center;
}
.cover-preview-placeholder {
  padding: 24px 12px;
  border: 1px dashed var(--color-gray-300);
  border-radius: var(--radius-lg);
  text-align: center;
  color: var(--color-gray-500);
  font-size: 13px;
}
/* Muted caption beneath a preview pane (e.g. the linen front cover note). */
.cover-preview-note {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--color-gray-500);
}
/* The pane shape tracks the CHOSEN layout's trim via --book-aspect (set by
 * syncBookAspect() in book-cover.js from the selected book type + trim), so the
 * front/back panes and the spine preview (which derives its height from the
 * matching --book-hw, see .spine-preview-pane below) all share one trim source
 * and line up — the img, placeholder, and solid-panel states included. */
.cover-preview-pane {
  /* --book-aspect: memory_book us_trade 6/9, crown_quarto 7.44/9.68,
     photo_book 7/10 (Executive). Defaults to 6/9. */
  aspect-ratio: var(--book-aspect, 6 / 9);
  position: relative;       /* anchor the absolutely-filled preview img */
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
/* Cover preview img is the small thumb; click opens the lightbox (JS wires
 * the click handler when the img is ready; the hand cursor signals it). It's
 * absolutely filled + object-fit:contain so the rendered cover (whose ratio
 * may differ slightly from the trim) CANNOT stretch the pane taller — the pane
 * stays a strict trim-aspect box, exactly matching the solid Linen Front Cover
 * Preview beside it. */
.cover-preview-pane img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.cover-disclaimer {
  margin: 12px 0 0;
  font-size: 12px;
  color: var(--color-gray-500);
  text-align: center;
}

/* /book/cover Spine card — radio row for the orientation toggle. The
 * global `input { width: 100% }` rule on line 51 stretches checkboxes
 * across the row; same width: auto override as .cover-inclusion-list
 * to keep radios next to their labels. */
.cover-radio-row { display: flex; gap: 16px; flex-wrap: wrap; }
.cover-radio {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  font-size: 14px;
  color: var(--color-ink);
}
.cover-radio input[type="radio"] {
  width: auto;
  padding: 0;
  margin: 0 6px 0 0;
  vertical-align: middle;
}
/* Mirrors the .cover-swatch:has(input[type="radio"]:focus-visible) rule
 * — keyboard users get a visible focus ring on the orientation radios
 * matching the rest of the cover page (the browser default is easy to
 * miss). */
.cover-radio:has(input[type="radio"]:focus-visible) {
  outline: 2px solid var(--color-info);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Spine preview aside — shares .cover-preview's 280px/16px frame so the
 * two previews read as a matched pair. The pane itself stays narrow
 * (a spine is intrinsically thin) but its height matches the rendered
 * front-cover img so the silhouettes line up. */
.spine-preview-pane {
  /* Mini spine strip — width is the "mini strip" visual; height derives
   * from the cover-preview-pane content width (248px after 16px padding
   * each side on a 280px-max-width .cover-preview) at the us_trade 6 × 9 in
   * trim AR — so the spine silhouette lines up with the 6×9 front-cover
   * preview. Single source of truth for trim — change the numbers in one
   * place if the trim ever shifts. */
  background: #363c48;     /* default body slate; per-color rules below override */
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-sm);
  width: 58px;
  /* Height = the front/back PANE's content height so the spine lines up exactly
     with (and is the same height as) the other previews. The pane content width
     is the frame (--book-preview-w) minus its 1px border ×2 + 16px padding ×2
     (= 34px), and pane height = that × the trim's height/width ratio (--book-hw,
     set by JS: 6×9 → 1.5; 7.44×9.68 → ~1.301). */
  height: calc((var(--book-preview-w, 280px) - 34px) * var(--book-hw, 1.5));
  /* No top offset — top-aligns with the back pane in the same row (both sit
   * directly under their identical h2), so the spine + back silhouettes line
   * up. Horizontally centered as a thin "mini strip" in the 280px frame. */
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  position: relative;      /* anchor the server-rendered spine image overlay */
  cursor: pointer;         /* whole pane is click-to-expand (image or swatch) */
}
/* Server-rendered spine preview (true WYSIWYG) — the faithful PNG from
   POST /api/book/cover/preview/spine (book-cover.js refreshSpinePreview) is
   OVERLAID on the swatch (the .spine-text stays underneath, covered). The spine
   is intrinsically thin, so it's small inline + click-to-zoom (lightbox). The
   contain letterbox bars show the pane's swatch color (= the spine color) so
   they blend. Used by BOTH the dust-jacket (#spine-preview-pane) and linen
   (#linen-spine-preview-pane) spines — same geometry, matched dimensions. */
.spine-preview-pane img.spine-preview-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  cursor: pointer;
}
.spine-preview-pane .spine-text {
  writing-mode: vertical-rl;
  font-family: 'Source Sans 3', Arial, Helvetica, sans-serif;
  font-size: 16px;
  font-weight: 600;
  color: #f4f1ea;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* Orientation is fixed — the base writing-mode: vertical-rl reads top-to-bottom
   (rotated), matching Lulu's linen foil. The old sideways/top_down modifiers are
   removed so the dust-jacket, cover, and linen spine previews all match. */
.spine-preview-pane--hidden .spine-text { visibility: hidden; }

/* Per-color tinting of the spine preview — parallel set to the
 * server-side COVER_COLOR_BG_RULES in lib/book/cover.js. Hand-authored
 * to keep the customer-facing /book/cover page self-contained; if the
 * server palette grows beyond 6 entries, switch to JS-injection.
 * Keep in sync with COVER_COLORS in public/js/book-cover.js +
 * lib/book/cover-options.js. */
/* Per-color tint applied directly to the spine pane. The attribute lives
 * on the pane itself (not a parent) so it only updates when Save spine
 * fires — keeping the spine preview a frozen snapshot of saved values
 * while the cover color radio + spine checkbox/radio can be edited
 * freely without the preview reacting. */
.spine-preview-pane[data-cover-color="navy"]  { background: #242b42; }
.spine-preview-pane[data-cover-color="black"] { background: #252628; }
.spine-preview-pane[data-cover-color="green"] { background: #3e5d4a; }
.spine-preview-pane[data-cover-color="gray"]  { background: #484a4c; }
.spine-preview-pane[data-cover-color="red"]   { background: #7a2c2c; }
.spine-preview-pane[data-cover-color="tan"]   { background: #969183; }
/* White is a rendered-only jacket color (no linen cloth). Needed so a white
   spine's pane bg matches the server-rendered white spine — otherwise the
   overlay's letterbox bars fall back to the slate default and don't blend. */
.spine-preview-pane[data-cover-color="white"] { background: #ffffff; }
/* Tan is the only color light enough to need dark spine text — matches
 * COVER_TAN_TEXT_RULES in lib/book/cover.js for the print render. */
.spine-preview-pane[data-cover-color="tan"] .spine-text { color: #2a2620; }

/* Back Cover Preview pane (2026-06-23) — a full-cover-shaped solid panel that
 * sits next to the spine preview in .cover-previews-row. The aspect-ratio +
 * --book-aspect var match the front-cover preview so the silhouette lines up.
 * Background + text color are painted inline by book-cover.js (slug-or-hex →
 * palette swatch, with a readable text color), so there are no per-color CSS
 * rules here (unlike the spine pane). The back text sits bottom-center, mirroring
 * the renderer's .cover-back-text placement. */
.back-preview-pane {
  aspect-ratio: var(--book-aspect, 6 / 9);
  width: 100%;
  background: #242b42;     /* overridden inline; default navy */
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-sm);
  position: relative;
  overflow: hidden;
  cursor: pointer;         /* whole pane is click-to-expand (image or swatch) */
}
.back-preview-pane .back-preview-text {
  position: absolute;
  bottom: 8%;
  left: 10%;
  right: 10%;
  text-align: center;
  font-family: 'Source Sans 3', Arial, Helvetica, sans-serif;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: #f4f1ea;          /* overridden inline */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Back-cover LOGO placeholder — the instant client swatch shows the Sepia Books badge (bottom-center, like the rendered back) when the back is on its
   default branding; the server PNG below swaps in the faithful render. Sized to
   ~60% of the pane width and centered on the lower third to echo the print. */
.back-preview-pane .back-preview-logo {
  position: absolute;
  bottom: 8%;
  left: 50%;
  transform: translateX(-50%);
  width: 60%;
  height: auto;
  display: block;
}
/* Server-rendered back-cover preview (true WYSIWYG) — the faithful PNG from
   POST /api/book/cover/preview/back (book-cover.js refreshBackPreview) replaces
   the inline swatch above. Absolutely filled + object-fit:contain so the
   rendered back panel (trim-width × full cover height, like the front preview)
   can't stretch the strict trim-aspect pane — mirrors .cover-preview-pane img. */
#back-preview-pane img.back-preview-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

/* Linen previews (Premium 6×9, 2026-06-23) — the foil-stamped cloth case under
 * the dust jacket. They REUSE .spine-preview-pane / .back-preview-pane for all
 * layout and paint their cloth bg + foil-color spine text INLINE (book-cover.js
 * repaintLinenPanes / render markup), so no per-color rules are needed here. The
 * .linen-*-preview-pane classes exist only as hooks; the linen spine still hides
 * via .spine-preview-pane--hidden. The linen back is a plain cloth panel (no
 * text), so it carries no .back-preview-text child. */

/* Universal click-to-expand affordance. attachImageLightbox (utils.js) stamps
 * role="button" on every preview <img> it wires, so this single rule gives the
 * hand cursor to ALL lightbox thumbnails — present and future — with no
 * per-selector upkeep. Scoped to `img` on purpose: the app's other
 * role="button" elements are <span>/<div> (inline-edit fields, switcher cards),
 * which this does not touch. */
img[role="button"] { cursor: pointer; }

/* Generic lightbox overlay — mirrors the /welcome lightbox rules in
 * share.css so /book/cover can reuse the same UX without pulling in
 * share.css. If a 3rd consumer arrives, dedupe by moving share.css's
 * rules out of share.css entirely. */
.lightbox-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.88);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
  cursor: zoom-out;
  animation: lightbox-fade-in 0.15s ease-out;
}
@keyframes lightbox-fade-in { from { opacity: 0; } to { opacity: 1; } }
.lightbox-overlay img {
  max-width: min(95vw, 540px);
  max-height: 95vh;
  max-height: 95dvh;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lightbox);
  cursor: default;
  display: block;
}
/* Enlarged cloth/color SWATCH clone (book-cover.js openLightboxSwatch) — for the
 * linen front/back + the spine/back swatch fallback, which have no server image.
 * The clone is sized to the pane's natural box + scaled via CSS transform (its
 * spine/back text scales too); flex-centered in the overlay. */
.lightbox-overlay .lightbox-swatch {
  flex: 0 0 auto;
  cursor: default;
  box-shadow: var(--shadow-lightbox);
}
.lightbox-close {
  position: absolute;
  top: 16px;
  right: 16px;
  background: rgba(255, 255, 255, 0.15);
  color: white;
  border: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.lightbox-close:hover { background: rgba(255, 255, 255, 0.25); }

/* /book/preview digital proof flow. The digital book is handled inside the main
 * order flow (#step-digital): book-configure.js renders the proof state + actions
 * into .db-state / .db-actions. Proof is free; the clean final costs $50. */
.db-state {
  margin: 0 0 16px;
  font-size: 14px;
  color: var(--color-ink-soft);
}
.db-state p { margin: 0 0 8px; }
.db-state p:last-child { margin: 0; }
.db-actions {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 16px;
}
.db-error {
  margin: 4px 0 0;
  padding: 8px 10px;
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border-soft);
  border-radius: var(--radius-md);
  color: var(--color-danger-deep);
  font-size: 13px;
}

/* /book/title-pages upload UI (docs/book-restructure-plan.md Phase 7).
 * Inline-with-card-body — same visual weight as the text-field block
 * above it. Thumbnail sits left, upload button right, status + error
 * below. */
.tp-upload-block {
  margin: 16px 0 0;
  padding: 12px;
  background: var(--color-gray-50);
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-lg);
}
.tp-upload-thumb {
  margin: 0 0 12px;
}
.tp-upload-thumb img {
  display: block;
  max-width: 200px;
  max-height: 200px;
  border: 1px solid var(--color-gray-300);
  border-radius: var(--radius-md);
  background: #fff;
}
.tp-upload-thumb-placeholder {
  padding: 12px 14px;
  background: #fff;
  border: 1px dashed var(--color-gray-300);
  border-radius: var(--radius-md);
  font-size: 13px;
  color: var(--color-gray-500);
  max-width: 240px;
}
.tp-upload-meta {
  margin-top: 4px;
  font-size: 12px;
  color: var(--color-gray-500);
}
.tp-upload-label {
  display: inline-block;
  cursor: pointer;
}
.tp-upload-label .btn-action {
  cursor: pointer;
}
.tp-upload-status {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--color-gray-500);
}
.tp-upload-error {
  margin: 8px 0 0;
  padding: 8px 10px;
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border-soft);
  border-radius: var(--radius-md);
  color: var(--color-danger-deep);
  font-size: 13px;
}

/* "Add content" icon row — reusable card-row vocabulary used on two
 * surfaces today:
 *   * /dashboard — decorative <div> tiles ("here's what you can add"
 *     above the Go to Add Content CTA)
 *   * /add-content — interactive <a> tiles that jump to the
 *     relevant section anchor on the same page
 * 4 tiles in a row at desktop, wrapping to 2-up below 480px. Visual
 * rhythm mirrors .quick-link on /dashboard (neutral border, light
 * hover) so the two card vocabularies feel related. */
.add-content-icons {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 8px;
}
.add-content-icon-item {
  flex: 1;
  min-width: 120px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 16px 12px;
  background: #fff;
  border: 1px solid var(--color-gray-200);
  border-radius: var(--radius-2xl);
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-gray-700);
}
.add-content-icon-item svg {
  color: var(--color-gray-500);
  flex: 0 0 auto;
}
/* Labels size to their actual line count (no 2-line reservation), so
 * the icon+label group sits visually centered inside each tile rather
 * than top-shifted by an empty second line. Trade-off: the icon on the
 * "Emails + Attachments" tile (which wraps to 2 lines) is slightly
 * higher than the icons on the single-line tiles, since flex-row
 * stretch equalizes tile heights and the wrapping tile's larger group
 * pushes its icon up to keep the group centered. */
.add-content-icon-item span {
  display: block;
  line-height: 1.3;
}
/* Link variant — used on /add-content where each tile jumps to a
 * section on the same page. Hover lifts the tile via border + bg shift
 * to match the .quick-link interaction pattern. */
a.add-content-icon-item {
  text-decoration: none;
  color: var(--color-gray-700);
  transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
}
a.add-content-icon-item:visited { color: var(--color-gray-700); }
a.add-content-icon-item:hover {
  border-color: var(--color-ink);
  background: var(--color-gray-50);
  color: var(--color-ink);
}
a.add-content-icon-item:hover svg { color: var(--color-ink); }
a.add-content-icon-item:focus-visible {
  outline: 2px solid var(--color-info);
  outline-offset: 2px;
}
@media (max-width: 480px) {
  .add-content-icon-item { min-width: calc(50% - 6px); }
}

/* iOS Safari auto-zooms the viewport whenever a focused form control renders
   below 16px, and never fully zooms back out — leaving the page magnified
   after the first tap on a field. Many controls are intentionally denser than
   16px on desktop (account fields, the print-flow ledger/promo/typography
   inputs, the cover hex/date inputs, admin tools, the inline-edit inputs on
   the content-group pages, etc.), scattered across this file, book-flow.css,
   and ~14 per-page inline <style> blocks. Rather than lift each one (and miss
   future pages), clamp every form control to the 16px focus-zoom floor at
   <=768px (phones + tablet portrait) only. `!important` is required to win
   over the more-specific class/id rules in the later-loading inline styles;
   it is scoped to the mobile breakpoint so desktop sizing is byte-for-byte
   unchanged. Verified safe: nothing sizes a form control >=17px or inside a
   media query anywhere, so this clamps nothing intentional. */
@media (max-width: 768px) {
  input, select, textarea { font-size: 16px !important; }
}

/* Responsive-table primitive. Wrap a wide <table> in
   <div class="table-scroll"> so it scrolls horizontally within its column
   instead of forcing the whole page wider than a phone viewport. The block
   wrapper is required because a <table> can't scroll its own overflow.
   contain: inline-size is REQUIRED, not decoration: body is a column flex
   container, and a wide table's min-content otherwise propagates up through
   the wrapper and inflates <main> past the viewport — overflow-x:auto alone
   does NOT contain it (verified empirically 2026-07-16; min-width:0 /
   max-width:100% on the ancestors don't help either). Desktop is
   unaffected — the wrapper still fills its column and only scrolls when the
   content is actually wider. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; contain: inline-size; }

/* ═════════════════════════════════════════════════════════════════════════════
 * DESIGN-SYSTEM COMPONENTS (adopted from docs/design-system)
 * Shared, tokenized primitives — the canonical source of truth for components
 * that were previously duplicated (often with drift) in per-page inline <style>
 * blocks. Pages still carrying an inline copy win by source order until their
 * inline rule is removed; new work should use these directly. Full catalog +
 * accent rules: docs/design-system/README.md.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── Inline text-color helpers ─────────────────────────────────────────────── */
.text-error   { color: var(--color-danger); }
.text-success { color: var(--color-success); }
.text-warning { color: var(--color-warning); }

/* ── Info note (blue informational banner — the sanctioned INFO callout, the
   counterpart to the green success .info-msg). Blue = info, never success. ── */
.info-note { display: block; background: var(--color-info-bg); border: 1px solid var(--color-info-border); border-radius: var(--radius-lg); padding: 12px 14px; font-size: 14px; line-height: 1.5; color: var(--color-info-text); }
.info-note strong { color: var(--color-info-text); font-weight: 700; }
.info-note a { color: var(--color-info-link); font-weight: 600; }
.info-note--icon { display: flex; align-items: flex-start; gap: 8px; }
.info-note__icon, .info-note--icon > svg { flex: 0 0 auto; color: var(--color-info); line-height: 0; margin-top: 1px; }
.info-note--tight { padding: 9px 14px; font-size: 13px; }
.info-note--tight.info-note--icon { align-items: center; }

/* ── JS-toggled message box (empty until a class + message is set) ─────────── */
.msg-box { padding: 12px 16px; border-radius: var(--radius-lg); font-size: 14px; line-height: 1.5; text-align: left; display: none; }
.msg-box.error   { display: block; background: var(--color-danger-bg);  border: 1px solid var(--color-danger-border);  color: var(--color-danger); }
.msg-box.success { display: block; background: var(--color-success-bg); border: 1px solid var(--color-success-border); color: var(--color-success); }

/* ── Static red-tinted callout box (always visible) ───────────────────────── */
.callout-danger { background: var(--color-danger-bg); border: 1px solid var(--color-danger-border); border-radius: var(--radius-lg); padding: 20px 24px; }
/* Amber sibling — the static warning callout the catalog previously lacked
   (pages kept re-rolling it). Same geometry contract as .callout-danger;
   per-surface padding/margin pins are fine. */
.callout-warning { background: var(--color-warning-bg); border: 1px solid var(--color-warning-border); border-radius: var(--radius-lg); padding: 20px 24px; color: var(--color-warning-text); }

/* ── Small inline flag pills (blue) ───────────────────────────────────────── */
.badge-info { display: inline-block; font-size: 11px; font-weight: 600; color: var(--color-info); background: var(--color-info-bg); border: 1px solid var(--color-info-border); border-radius: var(--radius-sm); padding: 0 5px; vertical-align: 1px; }
.badge-highlight { display: inline-block; background: var(--color-info-bg); color: var(--color-info-text); font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--radius-pill); border: 1px solid var(--color-info-border); vertical-align: middle; }

/* ── Order-status tone pills ──────────────────────────────────────────────────
   The canonical status-colour vocabulary for the book order lifecycle, shared by
   /order-status, /print-books and the dashboard status strip. These five
   MODIFIERS were byte-identical duplicates in two page-local <style> blocks;
   they are consolidated here per the design-system rule (identical → inherit).

   Only the modifiers are shared. There is deliberately NO unscoped `.pill` base:
   each surface scopes its own geometry (`.order-status-page .pill`,
   `.pb-table .pill`, `.stage-card .pill`) because the sizes genuinely differ, and
   a global base would leak any property those scoped rules DON'T set into pages
   that render correctly today. ─────────────────────────────────────────────── */
.pill--pending  { background: var(--color-gray-100); color: var(--color-gray-700); }
.pill--progress { background: var(--color-warning-bg); color: var(--color-warning-text); }
.pill--action   { background: var(--color-info-bg-strong); color: var(--color-info-text); }
.pill--success  { background: var(--color-success-bg-strong); color: var(--color-success-deep); }
.pill--terminal { background: var(--color-danger-bg-strong); color: var(--color-danger-deep); }

/* ── Content-type category badges (email=blue · photo=green · pdf=amber ·
   social=purple). Canonical taxonomy; retheme via the --cat-* tokens. ─────── */
.type-badge, .type-email, .type-photo, .type-pdf, .type-social { display: inline-block; padding: 3px 10px; border-radius: var(--radius-sm); font-size: 12px; font-weight: 600; }
.type-email  { background: var(--cat-email-bg);  color: var(--cat-email-fg); }
.type-photo  { background: var(--cat-photo-bg);  color: var(--cat-photo-fg); }
.type-pdf    { background: var(--cat-pdf-bg);    color: var(--cat-pdf-fg); }
.type-social { background: var(--cat-social-bg); color: var(--cat-social-fg); }

/* ── Quiet, bordered header-action button/anchor (page control rows). Opts out
   of the black-primary <button> default via appearance:none. ──────────────── */
.btn-header-action {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface); color: var(--color-gray-700);
  border: 1px solid var(--color-gray-300); border-radius: var(--radius-md);
  padding: 10px 14px; font-size: 14px; font-weight: 500; font-family: inherit;
  line-height: 1; text-decoration: none; cursor: pointer;
  appearance: none; -webkit-appearance: none;
}
.btn-header-action:hover { background: var(--surface-sunken); color: var(--color-ink); }
.btn-header-action svg { display: block; flex: 0 0 auto; }
.btn-header-action--manage { background: transparent; border-color: transparent; color: var(--color-gray-500); }
.btn-header-action--manage:hover { background: var(--surface-sunken); border-color: var(--color-gray-200); color: var(--color-ink); }
.btn-header-danger { color: var(--color-danger-strong); border-color: var(--color-danger-border-soft); }
.btn-header-danger:hover { background: var(--color-danger-bg); color: var(--color-danger-deep); }

/* ── Generic card primitive (adopted from the export catalog §4; the shared
   surface for the page-local card families). Per-surface geometry (padding /
   radius / margin) may be pinned by a composed family class — same
   shared-tone / scoped-geometry pattern as the .pill modifiers. ─────────── */
.card { background: var(--surface); border: 1px solid var(--color-gray-200); border-radius: var(--radius-2xl); padding: 24px 24px 20px; margin: 0 0 16px; }

/* ── Upsell / promo CTA card (bordered surface + title/sub + black CTA anchor) ── */
.cta-card { margin-top: 32px; padding: 24px 20px; background: var(--surface-sunken); border: 1px solid var(--color-gray-200); border-radius: var(--radius-2xl); text-align: center; }
.cta-card-title { font-size: 17px; font-weight: 700; color: var(--color-ink); margin: 0 0 6px; }
.cta-card-sub { font-size: 14px; color: var(--color-body); line-height: 1.5; margin: 0 0 14px; }
.cta-card-link, .cta-card-link:visited { display: inline-block; padding: 10px 22px; background: var(--color-ink); color: #fff; border-radius: var(--radius-lg); font-size: 14px; font-weight: 700; text-decoration: none; transition: background var(--transition-base); }
.cta-card-link:hover { background: var(--color-ink-soft); color: #fff; text-decoration: none; }

/* ── Full-page status/result page (centered) ──────────────────────────────── */
.status-page { max-width: 560px; margin: 0 auto; padding: 48px 16px; text-align: center; }
.status-page h1 { font-size: 26px; margin: 0 0 12px; }
.status-page p { color: var(--color-body); line-height: 1.7; margin: 0 0 24px; }
.status-page .btn-group { justify-content: center; }

/* ─────────────────────────────────────────────────────────────────────────────
 * "Continue with Google" button + the auth-choice divider.
 *
 * DO NOT RETHEME. The fill/stroke/text colors below are prescribed by Google's
 * branding guidelines (light theme: #FFFFFF fill, #747775 stroke, #1F1F1F text)
 * and are deliberately NOT design tokens — swapping them for var(--color-*)
 * would put us off-spec the next time the palette moves. Same for the padding
 * (12px / 10px after the logo / 12px) and the untouched color "G" mark, whose
 * size and color Google forbids altering.
 *
 * The one deliberate deviation is the font: the spec asks for Google Sans
 * Medium, which would introduce the only webfont on the login page. These pages
 * run a pure system stack, so we inherit it. Compliance is a requirement for
 * OAuth app VERIFICATION, which only sensitive/restricted scopes trigger — we
 * request just email+profile, so we never enter that review. Owner-decided.
 *
 * Shared (not page-scoped) because /account reuses it for Connect Google.
 * ────────────────────────────────────────────────────────────────────────── */
.btn-google {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;                 /* Google spec: 10px between logo and label */
  padding: 0 12px;           /* Google spec: 12px leading / trailing     */
  min-height: 40px;
  width: 100%;
  background: #FFFFFF;
  border: 1px solid #747775;
  border-radius: var(--radius-md, 6px);
  color: #1F1F1F;
  font-size: 14px;
  line-height: 20px;
  font-weight: 500;
  font-family: inherit;      /* deviation from spec — see note above */
  cursor: pointer;
  transition: background-color 120ms ease;
}
/* Hover/focus tint only — the fill, stroke, and text stay on-spec. Deliberately
   NOT the app's #333 button-hover (which would invert this to a dark button and
   break the "G on white" requirement). */
.btn-google:hover:not(:disabled) { background: #f7f8f8; }
.btn-google:focus-visible { outline: 2px solid var(--color-ink); outline-offset: 2px; }
.btn-google:disabled { opacity: .6; cursor: default; }
.btn-google svg { flex: none; }

/* "or" divider between the password form and the Google button. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 24px 0 16px;
  color: var(--color-gray-500);
  font-size: 13px;
}
.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--color-gray-200);
}

/* Touch-target bump. These shared controls are sized for pointer precision
   (24-36px); on touch-primary devices (hover: none) give them the ~44px HIG
   floor. Desktop rendering is byte-for-byte unchanged. The close buttons'
   top/right offsets shrink so the enlarged box keeps the glyph in the same
   visual spot. */
@media (hover: none) {
  .btn-outline, .btn-secondary, .btn-header-action { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
  .card-cta { padding: 12px 16px; }
  .bc-modal__close, .upsell-modal__close { min-width: 40px; min-height: 40px; display: flex; align-items: center; justify-content: center; top: 2px; right: 6px; }
  .reset-link, .muted-link, .btn-link { padding: 10px 0; }
}
