
/* === 01-base.css === */
/* =========================================================================
   moco — stylesheet (01-base.css)
   -------------------------------------------------------------------------
   ARCHITECTURE: mobile-first.
     • Every default rule targets phones first.
     • Use `@media (min-width: 768px)` to ADD desktop styles. Never use
       `max-width` for new components — overriding desktop with smaller-
       viewport rules is the pattern that produced the long history of
       mobile bugs in this file.
     • Single breakpoint scale: 768px (tablet/desktop), 1024px (wide).
     • Inputs use --m-fs-input (16px) to prevent iOS zoom-on-focus.
     • Primary tap targets ≥48px on mobile.

   FILE LAYOUT (web/static/styles/, concatenated at server startup):
     01-base.css            — tokens, reset, theme variants, skip-link, focus
     02-layout.css          — page/reader shells, topbars, brand, topnav,
                              buttons, hero, public-shelf, audience bands
     03-cards-and-forms.css — cards, badges/chips, forms, upload dropzone,
                              surface cards, progress bar
     04-library.css         — dashboard library grid, book panels,
                              book detail / share page
     05-reader.css          — PDF/EPUB/MD reader UI, toasts, modal,
                              reader sidebar overlay, responsive overrides
     06-pages.css           — highlights/quotes, tags on cards, stats grid,
                              popover color row, in-book search, settings
                              hero, utilities, phase-2 status notes

   Browser sees one stylesheet — buildCombinedCSS in server.go concatenates
   these alphabetically at startup and serves them at /static/styles.css.
   ========================================================================= */

:root {
  --bg: #f6f0e7;
  --bg-strong: #efe5d6;
  --panel: rgba(255, 251, 245, 0.78);
  --panel-strong: #fffaf2;
  --line: rgba(85, 66, 43, 0.14);
  --line-strong: rgba(85, 66, 43, 0.24);
  --text: #30261d;
  --text-soft: #46382c;
  --muted: #6f6255;
  --accent: #ba7d4a;
  --accent-dark: #905f36;
  --accent-soft: rgba(186, 125, 74, 0.18);
  --danger: #9d4d3f;
  --danger-soft: rgba(157, 77, 63, 0.14);
  --success: #4a7d56;
  --success-soft: rgba(74, 125, 86, 0.16);
  --focus-ring: rgba(186, 125, 74, 0.55);
  --shadow: 0 24px 60px rgba(70, 46, 22, 0.12);
  --shadow-soft: 0 6px 18px rgba(70, 46, 22, 0.08);
  --radius-lg: 28px;
  --radius-md: 20px;
  --radius-sm: 14px;
  --tap: 44px;

  /* Mobile design tokens — used in the consolidated mobile layer at the
     end of this file. Kept separate from desktop tokens so each surface
     has its own scale rather than scaling desktop values down. */
  --m-pad-page: 14px;            /* horizontal padding on the page-shell */
  --m-pad-card: 16px;            /* card interior padding */
  --m-gap-section: 18px;         /* gap between major sections */
  --m-gap-card: 10px;            /* gap inside cards */
  --m-radius-card: 18px;         /* card corner radius */
  --m-radius-input: 14px;        /* input/select corner radius */
  --m-tap: 48px;                 /* min tap target for primary controls */
  --m-fs-h1: 1.6rem;             /* page-level headings on mobile */
  --m-fs-h2: 1.25rem;            /* section headings */
  --m-fs-body: 0.95rem;          /* body copy */
  --m-fs-meta: 0.85rem;          /* secondary/muted copy */
  --m-fs-input: 16px;            /* >=16px so iOS doesn't zoom on input focus */

  /* Single flat warm cream across the entire viewport so there's nothing
     for "darker side panel" perception to read against. Component cards
     (.book-panel, etc.) share the same base color and lean on borders +
     shadows for definition instead of contrast against the body. The
     reader keeps a faint gradient because the reading surface benefits
     from a subtle paper-like depth. */
  --page-bg: #faf4e8;
  --bg-grad: var(--page-bg);
  --reader-bg-grad: linear-gradient(180deg, #f7f0e5 0%, #f1e7d4 100%);
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; overflow-x: clip; }

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

body {
  margin: 0;
  min-height: 100vh;
  /* overflow-x: clip clips at the box edge without creating a new
     positioning context (which `hidden` does on some browsers and which
     can let fixed/absolute children escape the clip). */
  overflow-x: clip;
  color: var(--text);
  font-family: "Manrope", system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--bg-grad);
  background-attachment: fixed;
}

/* iOS Safari zooms when a user taps any input whose font-size is < 16px,
   which breaks paginated layouts. Set 16px globally on every text input
   so the zoom never triggers; visible scaling is handled per-component. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="url"],
input[type="tel"],
textarea,
select {
  font-size: var(--m-fs-input);
}

.reader-body {
  background: var(--reader-bg-grad);
  background-attachment: fixed;
}

/* Paginated reader (EPUB / PDF) — lock viewport scroll so the page is one
   fixed canvas instead of a scrollable document. Without this, mobile
   in-app browsers (Instagram, etc.) let the page scroll, defeating the
   page-turn UX. touch-action: manipulation also kills the iOS double-tap
   zoom that interferes with our tap-to-toggle-immersive interaction. */
.reader-body-paginated {
  overflow: hidden;
  height: 100dvh;
  touch-action: manipulation;
}
@supports not (height: 100dvh) {
  .reader-body-paginated { height: 100vh; }
}
.reader-body-paginated .reader-app {
  /* The reader-app already has min-height:100vh; cap it to dvh inside the
     paginated body so it never grows past the viewport. */
  min-height: 100dvh;
  height: 100dvh;
}
@supports not (height: 100dvh) {
  .reader-body-paginated .reader-app { min-height: 100vh; height: 100vh; }
}

/* Reader theme overrides — set via data-reader-theme on the body */
.reader-body[data-reader-theme="light"] {
  --bg: #f6f0e7;
  --text: #30261d;
  --text-soft: #46382c;
  --muted: #6f6255;
  --line: rgba(85, 66, 43, 0.14);
  --reader-bg-grad: linear-gradient(180deg, #f7f1e8 0%, #f1e7d8 100%);
}
/* Paper: warm off-white that mimics physical book paper. Lower contrast
   than Light, less yellow tint than Sepia — comfortable for long reads. */
.reader-body[data-reader-theme="paper"] {
  --bg: #ebe2cd;
  --text: #2a241c;
  --text-soft: #3c3225;
  --muted: #7a6a52;
  --line: rgba(85, 66, 43, 0.16);
  --reader-bg-grad: linear-gradient(180deg, #ece3ce 0%, #e2d7bc 100%);
}
.reader-body[data-reader-theme="sepia"] {
  --bg: #f3e8d4;
  --text: #4a3a25;
  --text-soft: #5b4830;
  --muted: #8c7858;
  --line: rgba(85, 66, 43, 0.18);
  --reader-bg-grad: linear-gradient(180deg, #f3e8d4 0%, #ebd8b6 100%);
}
.reader-body[data-reader-theme="dark"] {
  --bg: #16130f;
  --text: #f1e6d3;
  --text-soft: #e7dcc7;
  --muted: #b1a591;
  --line: rgba(232, 213, 184, 0.14);
  --reader-bg-grad: linear-gradient(180deg, #1a1612 0%, #100d09 100%);
}

input, button, textarea, select { font: inherit; color: inherit; }

a { color: inherit; text-decoration: none; }

/* ---------- Skip link & focus ---------- */
.skip-link {
  position: absolute;
  left: 12px;
  top: -100px;
  z-index: 100;
  padding: 12px 18px;
  border-radius: 999px;
  background: var(--text);
  color: #fff8f0;
  font-weight: 600;
  transition: top 0.18s ease;
}
.skip-link:focus, .skip-link:focus-visible { top: 12px; }

:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
  border-radius: 6px;
}
.button:focus-visible,
.text-link:focus-visible,
a.button:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}


/* === 02-layout.css === */
/* ---------- Layout shells ---------- */
/* ----- Page shell -----
   Mobile-first: edge-to-edge with internal padding (app-like, no horizontal
   scroll). Desktop centers a max-width container.
*/
.page-shell {
  width: 100%;
  max-width: 100%;
  padding: 14px var(--m-pad-page) calc(36px + env(safe-area-inset-bottom));
  margin: 0 auto;
  box-sizing: border-box;
  /* min-width: 0 prevents children with intrinsic min-content (long words,
     selects, grids with minmax floors) from blowing the shell past the
     viewport — defensive belt against the pattern that the highlights and
     discover pages hit on phones. */
  min-width: 0;
  overflow-x: clip;
}
/* margin-inline:auto lets this class work either as the outer shell
   (combined with .page-shell, used on auth) or as an inner content
   wrapper inside a wide .page-shell (used on settings to keep the
   topbar full-width while narrowing the form body). */
.page-shell-narrow { width: 100%; max-width: 760px; margin-inline: auto; }
@media (min-width: 768px) {
  /* Wider shell so content reaches closer to the viewport edges. Combined
     with the flat body color, this reduces the perception of empty
     "walls" outside the content area on wide monitors. */
  .page-shell {
    width: min(1440px, calc(100% - 32px));
    padding: 20px 0 48px;
  }
  .page-shell-narrow { width: min(760px, calc(100% - 24px)); }
}

/* ----- Reader shell -----
   Mobile-first: very tight top padding so the topbar (fixed-position) only
   visually overlaps the top of the rendered content when revealed. Desktop
   reserves more breathing room. The padding stays CONSTANT through immersive
   toggles — animating it would shift content position. */
.reader-app {
  min-height: 100dvh;
  padding: 14px 8px 8px;
  position: relative;
}
@supports not (height: 100dvh) {
  .reader-app { min-height: 100vh; }
}
@media (min-width: 768px) {
  .reader-app { padding: 80px 12px 12px; }
  /* Immersive mode: the topbar is translated off-screen but its reserved
     80px of padding-top stays, and the .reader-toolbar strip below it would
     also keep ~55px of empty space via opacity:0. Together that pinned the
     rendered EPUB page to the bottom of the viewport and left a huge gap up
     top. Collapse both so the page actually sits centered. */
  .reader-app.is-immersive { padding-top: 24px; }
  .reader-body-paginated .reader-app.is-immersive .reader-toolbar { display: none; }
}

main { display: grid; gap: 22px; }
@media (min-width: 768px) {
  main { gap: 28px; }
}

/* ----- Topbar (primary, dashboard chrome) -----
   Mobile-first: brand + actions on row 1, scrollable nav as full-width
   row 2 (via `order: 99` and `flex: 1 1 100%`). Desktop expands into a
   single horizontal pill.
*/
.topbar {
  position: sticky;
  top: 8px;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px 14px;
  margin-bottom: 18px;
  background: rgba(250, 244, 236, 0.96);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border: 1px solid var(--line);
  border-radius: 18px;
  box-shadow: var(--shadow);
  /* Mobile-only auto-hide on downward scroll. The class is toggled by
     setupScrollAwayTopbar in app.js. Slide off the top so the grid gets
     full viewport height while scrolling down; bring back instantly on
     scroll-up. Desktop ignores the class (the @media-gated translate
     below only fires below 768px). */
  transition: transform 220ms ease, opacity 220ms ease;
}
@media (max-width: 767px) {
  /* `:not(:focus-within)` keeps the topbar visible when a keyboard user
     tabs into it — otherwise focus could land on an invisible link with
     pointer-events: none and they'd have no idea where they are. */
  .topbar.is-tucked-away:not(:focus-within) {
    transform: translateY(calc(-100% - 16px));
    opacity: 0;
    pointer-events: none;
  }
}
.topbar > .brand,
.topbar > .topnav,
.topbar > .topbar-actions { min-width: 0; }
.topbar > .brand { flex: 0 0 auto; }
.topbar .topbar-actions { flex: 0 1 auto; gap: 6px; margin-left: auto; }
.topbar-actions .session-chip { display: none; }

@media (min-width: 768px) {
  .topbar {
    top: 16px;
    padding: 10px 12px 10px 22px;
    margin-bottom: 32px;
    background: rgba(250, 244, 236, 0.72);
    border-radius: 999px;
  }
  .topbar > .brand { flex: 0 1 auto; }
  .topbar .topnav { flex: 0 1 auto; }
  .topbar-actions .session-chip { display: inline-flex; }
}

/* ----- Reader topbar (floating overlay) -----
   Mobile-first: compact pill with icon-only toggles, brand-mark only.
   Desktop: full pill with all labels visible. */
.reader-topbar {
  position: fixed;
  top: 8px;
  left: 8px;
  right: 8px;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding: 6px 8px;
  background: rgba(247, 240, 230, 0.86);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: var(--shadow);
  transition: opacity 0.25s ease, transform 0.25s ease;
  flex-wrap: nowrap;
}
.reader-topbar .brand-name { display: none; }
.reader-topbar .session-chip,
.reader-topbar .visibility-chip,
.reader-topbar [data-fullscreen-toggle] { display: none; }
.reader-topbar .button { padding: 8px 10px; font-size: 0.85rem; min-height: 38px; }
.reader-topbar .reader-toggle { width: 38px; height: 38px; padding: 0; flex-shrink: 0; }
.reader-topbar .reader-toggle .reader-toggle-label { display: none; }
.reader-topbar .reader-toggle .icon { width: 18px; height: 18px; }
@media (min-width: 768px) {
  .reader-topbar {
    top: 12px;
    left: 12px;
    right: 12px;
    gap: 16px;
    padding: 10px 14px;
    border-radius: 22px;
  }
  .reader-topbar .brand-name { display: inline; }
  .reader-topbar .session-chip,
  .reader-topbar .visibility-chip,
  .reader-topbar [data-fullscreen-toggle] { display: inline-flex; }
  .reader-topbar .button { padding: 10px 14px; font-size: 0.95rem; min-height: var(--tap); }
  .reader-topbar .reader-toggle { width: auto; height: auto; padding: 10px 14px; }
  .reader-topbar .reader-toggle .reader-toggle-label { display: inline; }
}
.reader-app.is-immersive .reader-topbar {
  transform: translateY(calc(-100% - 24px));
  opacity: 0;
  pointer-events: none;
}
.reader-topbar.is-revealed { transform: none; opacity: 1; pointer-events: auto; }

.reader-topbar-left {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  flex: 1 1 auto;
}

.reader-toggle { display: inline-flex; }

.brand {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
}

.brand-mark {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  font-family: "Cormorant Garamond", serif;
  font-size: 1.6rem;
  font-weight: 700;
  background: linear-gradient(135deg, #d49b6d 0%, #f2dcc4 100%);
  color: #30261d;
}

.brand-name,
.hero h1,
.feature-band h1,
.reader-preview h2,
.notes-preview h2 {
  font-family: "Cormorant Garamond", serif;
}

.brand-name { font-size: 1.35rem; letter-spacing: 0.02em; }

.topnav, .topbar-actions, .hero-actions, .hero-meta, .library-toolbar, .reader-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
}

.text-link,
.hero-meta span,
.reader-toolbar span,
.library-toolbar .pill {
  color: var(--muted);
  font-size: 0.95rem;
}

/* Topbar nav.
   Mobile-first: full-width second row, horizontally scrollable so any
   number of links fits without breaking the layout. Desktop becomes
   inline pills sitting next to the brand.
*/
.topnav {
  display: flex;
  flex: 1 1 100%;
  order: 99;
  gap: 4px;
  margin: 4px 0 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.topnav::-webkit-scrollbar { display: none; }
.topnav a {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid transparent;
  white-space: nowrap;
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: none;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  -webkit-tap-highlight-color: transparent;
}
@media (min-width: 768px) {
  .topnav { flex: 0 1 auto; order: 0; margin: 0; overflow: visible; gap: 10px; }
  .topnav a { padding: 7px 14px; font-size: 0.95rem; }
}
.topnav a:hover {
  background: rgba(255, 250, 242, 0.85);
  border-color: var(--line);
  color: var(--text);
}
.topnav a:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
}
.topnav a[aria-current="page"] {
  background: var(--text);
  color: #fff8f0;
  font-weight: 600;
}
.topnav a[aria-current="page"]:hover {
  background: #1f1813;
  color: #fff8f0;
  border-color: transparent;
}

/* ----- Buttons -----
   Mobile-first: 48px primary tap target, 40px subtle. Desktop relaxes to
   the 44px desktop tap-target token.
*/
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--m-tap);
  padding: 10px 18px;
  border-radius: 999px;
  border: 1px solid transparent;
  font-weight: 600;
  font-size: 0.95rem;
  cursor: pointer;
  transition: transform 0.08s ease, background-color 0.18s ease, border-color 0.18s ease, opacity 0.18s ease;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
@media (min-width: 768px) {
  .button { min-height: var(--tap); }
}

.button.primary { background: var(--text); color: #fff8f0; }
.button.primary:hover { background: #1f1813; }

.button.secondary,
.button.subtle {
  background: rgba(255, 250, 242, 0.72);
  border-color: var(--line);
  color: var(--text);
}
.button.subtle:hover, .button.secondary:hover {
  background: rgba(255, 250, 242, 0.95);
  border-color: var(--line-strong);
}
/* Subtle variant on mobile: smaller tap target so it doesn't dominate. */
.button.subtle { min-height: 40px; padding: 8px 14px; font-size: 0.88rem; }
@media (min-width: 768px) {
  .button.subtle { min-height: var(--tap); padding: 10px 18px; font-size: 0.95rem; }
}

.button:active { transform: scale(0.98); }

.button[disabled],
.button:disabled,
.button.is-loading {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
}

.button.danger {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: rgba(157, 77, 63, 0.35);
}
.button.danger:hover { background: rgba(157, 77, 63, 0.22); }

.text-link {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 4px 2px;
  background: none;
  border: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(111, 98, 85, 0.4);
}
.text-link:hover { color: var(--text); }
.text-link.destructive { color: var(--danger); }

.spinner {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-right-color: transparent;
  animation: spin 0.7s linear infinite;
  display: inline-block;
}

/* Inline SVG icons in buttons */
.icon {
  display: inline-block;
  flex-shrink: 0;
  vertical-align: middle;
}
.button .icon { margin-right: -2px; }
.button.icon-only .icon { margin-right: 0; }
.reader-toggle, [data-fullscreen-toggle] {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ----- Hero (logged-out home page) -----
   Mobile-first: single column, tighter padding, smaller hero text.
   Desktop adds the two-column layout and breathing room.
*/
.hero {
  display: grid;
  grid-template-columns: 1fr;
  gap: 18px;
  padding: 16px 0 12px;
}

/* .feature-band intentionally excluded — the hero is now a plain layout
   container that sits flush against the page background. A border + shadow
   here rendered it as an outlined rectangle that read as "side walls" on
   wide screens. */
.hero-copy, .hero-panel, .library-card, .reader-card,
.toc-panel, .note-card, .notes-preview {
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
}

.hero-copy {
  padding: 22px;
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(255, 251, 245, 0.78), rgba(248, 239, 226, 0.88));
}

@media (min-width: 768px) {
  .hero {
    grid-template-columns: 1.15fr 0.85fr;
    gap: 28px;
    padding: 48px 0 40px;
  }
  .hero-copy {
    padding: 36px;
    border-radius: var(--radius-lg);
  }
}

.eyebrow, .section-label {
  margin: 0 0 12px;
  color: var(--accent-dark);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
@media (min-width: 768px) {
  .eyebrow, .section-label { font-size: 0.85rem; letter-spacing: 0.12em; }
}

.hero h1 {
  margin: 0;
  font-size: var(--m-fs-h1);
  line-height: 1.15;
  word-wrap: break-word;
  overflow-wrap: break-word;
}

.hero-text {
  width: 100%;
  margin: 14px 0 18px;
  color: var(--muted);
  font-size: var(--m-fs-body);
  line-height: 1.55;
}

@media (min-width: 768px) {
  .hero h1 {
    font-size: clamp(2.6rem, 6vw, 5.4rem);
    line-height: 1;
  }
  .hero-text {
    width: min(540px, 100%);
    margin: 18px 0 24px;
    font-size: 1.05rem;
    line-height: 1.7;
  }
}

/* Hero meta chips ("Highlights and reading progress" etc.) — wrap on mobile. */
.hero-meta { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 8px; font-size: 0.8rem; }
.hero-meta span {
  padding: 4px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(255, 250, 245, 0.68);
}
@media (min-width: 768px) {
  .hero-meta { margin-top: 18px; gap: 10px; font-size: 0.95rem; }
  .hero-meta span { padding: 8px 12px; }
}

.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; }
@media (min-width: 768px) {
  .hero-actions { gap: 12px; }
}

.hero-panel {
  padding: 18px;
  border-radius: var(--m-radius-card);
  background: linear-gradient(180deg, rgba(242, 231, 216, 0.9), rgba(255, 248, 240, 0.86));
}
@media (min-width: 768px) {
  .hero-panel { padding: 22px; border-radius: var(--radius-lg); }
}

.shelf-card {
  padding: 14px;
  border-radius: var(--m-radius-card);
  background: rgba(255, 251, 246, 0.74);
  border: 1px solid rgba(85, 66, 43, 0.08);
}
@media (min-width: 768px) {
  .shelf-card { padding: 18px; border-radius: 22px; }
}

.shelf-empty {
  margin: 0;
  padding: 18px 4px 8px;
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1.5;
}

.shelf-header, .library-grid, .reader-preview, .notes-preview { display: grid; }

.shelf-header {
  grid-template-columns: 1fr auto;
  margin-bottom: 14px;
  color: var(--muted);
  font-size: 0.9rem;
}
.shelf-header a {
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(111, 98, 85, 0.4);
}

/* Public-shelf row (home page).
   Mobile-first: 40×60 thumbnail. Desktop bumps to 44×66. */
.book-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-top: 1px solid rgba(85, 66, 43, 0.08);
  color: inherit;
  text-decoration: none;
}
.book-row:first-of-type { border-top: 0; }
.book-row:hover h3 { text-decoration: underline; text-underline-offset: 3px; }

.book-row-cover {
  flex: 0 0 40px;
  width: 40px;
  height: 60px;
  border-radius: 8px;
  overflow: hidden;
  background: rgba(85, 66, 43, 0.06);
  display: block;
}
@media (min-width: 768px) {
  .book-row { gap: 14px; padding: 12px 0; }
  .book-row-cover { flex-basis: 44px; width: 44px; height: 66px; }
}
.book-row-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.book-row-meta { min-width: 0; flex: 1 1 auto; }

.book-row h3, .book-tile h3 {
  margin: 0 0 4px;
  font-size: 1rem;
}

.book-row p, .book-tile p, .notes-copy p,
.feature-band p, .toc-list, .reader-page p {
  margin: 0;
  color: var(--muted);
}

.book-cover, .tile-cover { border-radius: 16px; background-size: cover; }
.book-cover { width: 56px; height: 74px; }
.tile-cover { width: 100%; height: 180px; margin-bottom: 16px; }

.moss { background: linear-gradient(160deg, #93a77c 0%, #65745b 100%); }
.clay { background: linear-gradient(160deg, #cb9368 0%, #9f6841 100%); }
.sand { background: linear-gradient(160deg, #d9c19e 0%, #b79768 100%); }
.ink  { background: linear-gradient(160deg, #657280 0%, #414a54 100%); }

/* Feature band (Discover hero, etc.).
   Mobile-first: single column, tight padding. Desktop: two-column with the
   layout-pattern rules in the @media (min-width: 768px) block below. */
/* Discovery heading band — plain editorial text on the body, no card
   chrome. Single-column on every viewport so the section-label, headline,
   and description read as one continuous block instead of two fragments
   separated by empty cream (which is what the old 2-column layout looked
   like once the wrapping banner was removed). The search/sort toolbar
   immediately below it completes the discovery block. */
.feature-band {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 4px 2px 6px;
  margin: 0 0 8px;
  background: transparent;
  max-width: 100%;
  min-width: 0;
}
.feature-band > * { min-width: 0; }
.feature-band > * > * { margin: 0; }
.feature-band p { font-size: var(--m-fs-body); line-height: 1.45; color: var(--muted); max-width: 60ch; }
@media (min-width: 768px) {
  .feature-band {
    gap: 6px;
    padding: 10px 2px 4px;
    margin: 0 0 10px;
  }
  .feature-band p { font-size: 0.95rem; line-height: 1.5; }
}

/* Audience bands (For readers / For writers) on the home page. Mobile:
   single-column stack. Desktop: two-up grid so both audiences are visible
   without scrolling. */
.audience-bands {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 14px;
  margin: 28px 0 0;
}
.audience-band {
  padding: 18px 20px;
  border-radius: var(--m-radius-card);
  background: rgba(255, 250, 243, 0.78);
  border: 1px solid var(--line);
  display: grid;
  gap: 8px;
  min-width: 0;
}
.audience-band h2 {
  margin: 0;
  font-size: 1.4rem;
  line-height: 1.2;
  font-family: "Cormorant Garamond", serif;
}
.audience-band p { margin: 0; color: var(--text-soft, var(--text)); line-height: 1.5; font-size: 0.98rem; }
@media (min-width: 768px) {
  .audience-bands { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; margin-top: 48px; }
  .audience-band { padding: 28px 30px; border-radius: var(--radius-lg); }
  .audience-band h2 { font-size: clamp(1.5rem, 2.4vw, 2rem); }
}

/* Cross-band heading scale.
   .feature-band is the discovery section heading on /discover — kept smaller
   so it doesn't dominate the page; the book grid is the focal point.
   .reader-preview / .notes-preview are landing-page bands where a bigger
   editorial headline still works. */
.feature-band h1, .reader-preview h2, .notes-preview h2 {
  margin: 0;
  font-size: var(--m-fs-h2);
  line-height: 1.2;
}
@media (min-width: 768px) {
  .feature-band h1 {
    font-size: clamp(1.4rem, 2.2vw, 1.9rem);
    line-height: 1.15;
    letter-spacing: -0.01em;
  }
  .reader-preview h2, .notes-preview h2 {
    font-size: clamp(1.9rem, 4vw, 3.2rem);
    line-height: 1.05;
  }
}


/* === 03-cards-and-forms.css === */
/* ---------- Cards / lists ---------- */
.library-card, .toc-panel, .reader-card, .notes-preview {
  background: rgba(255, 251, 246, 0.76);
  border-radius: var(--radius-lg);
}

.library-card { padding: 24px; overflow: hidden; }

.pill {
  flex: 0 0 auto;
  padding: 9px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(255, 248, 240, 0.84);
}
.pill.active { color: #fff9f2; background: var(--text); }

.library-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 18px;
  margin-top: 22px;
}

.book-tile {
  padding: 14px;
  border-radius: 22px;
  background: rgba(252, 247, 239, 0.9);
  border: 1px solid rgba(85, 66, 43, 0.08);
}

.reader-preview {
  grid-template-columns: 320px 1fr;
  gap: 20px;
  margin: 28px 0;
}

.toc-panel, .reader-card { padding: 24px; }

.toc-list {
  padding: 0;
  list-style: none;
  display: grid;
  gap: 10px;
  margin-top: 22px;
}

.toc-list li {
  padding: 0;
  border-radius: 16px;
  background: rgba(251, 245, 235, 0.88);
  border: 1px solid rgba(85, 66, 43, 0.08);
}

.toc-list a, .toc-list button {
  display: block;
  width: 100%;
  padding: 12px 14px;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
  border-radius: 16px;
  min-height: var(--tap);
  color: var(--text);
  font-size: 0.95rem;
}
.toc-list a:hover, .toc-list button:hover { background: rgba(186, 125, 74, 0.08); }

.toc-list .current,
.toc-list li.current {
  background: var(--accent-soft);
  border-color: rgba(186, 125, 74, 0.36);
}
.toc-list .current a, .toc-list .current button {
  color: var(--accent-dark);
  font-weight: 600;
}

/* Reader toolbar — small format-chip + progress strip below the topbar.
   Mobile-first: hidden inside paginated readers (saves vertical space; the
   user already sees percent-read in the bottom-bar progress). Desktop shows
   it as a slim header strip. */
.reader-toolbar {
  justify-content: space-between;
  padding-bottom: 14px;
  border-bottom: 1px solid rgba(85, 66, 43, 0.08);
  flex-wrap: wrap;
  gap: 8px;
  transition: opacity 0.25s ease;
}
.reader-body-paginated .reader-toolbar { display: none; }
@media (min-width: 768px) {
  .reader-body-paginated .reader-toolbar { display: flex; }
}
.reader-app.is-immersive .reader-toolbar { opacity: 0; pointer-events: none; }
.reader-app.is-immersive .highlight-actions { opacity: 0; pointer-events: none; transition: opacity 0.25s ease; }

.reader-page {
  max-width: 680px;
  padding: 22px 8px 8px;
}

.reader-page p {
  font-family: "Cormorant Garamond", serif;
  font-size: 1.4rem;
  line-height: 1.55;
  color: var(--text-soft);
}

.dropcap::first-letter {
  float: left;
  margin-right: 10px;
  font-size: 4.4rem;
  line-height: 0.86;
}

.inline-highlight {
  margin-top: 22px;
  padding: 16px 18px;
  border-left: 4px solid var(--accent);
  border-radius: 16px;
  background: rgba(224, 188, 146, 0.24);
  color: #503d2c;
}

.notes-preview {
  grid-template-columns: 0.8fr 1fr;
  gap: 24px;
  align-items: start;
  padding: 26px;
}

.notes-stack { display: grid; gap: 14px; }

/* Note cards (highlights / bookmarks panel + cross-book quotes list).
   Mobile-first: tighter padding + radius, smaller body text. */
.note-card {
  padding: 14px;
  border-radius: var(--m-radius-card);
  background: rgba(255, 249, 242, 0.92);
}
.note-card p {
  margin: 12px 0 0;
  font-family: "Cormorant Garamond", serif;
  font-size: 1.05rem;
  line-height: 1.4;
}
@media (min-width: 768px) {
  .note-card { padding: 20px; border-radius: 22px; }
  .note-card p { font-size: 1.35rem; line-height: 1.45; }
}
.note-card .note-meta {
  font-family: "Manrope", sans-serif;
  font-size: 0.85rem;
  color: var(--muted);
  font-weight: 400;
}

/* ---------- Badges / chips ---------- */
.note-tag, .session-chip, .book-format, .visibility-chip {
  display: inline-flex;
  align-items: center;
  font-weight: 600;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.note-tag {
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 0.78rem;
  text-transform: uppercase;
}
.note-tag.amber { background: rgba(214, 173, 104, 0.22); color: #7a5b1b; }
.note-tag.sage  { background: rgba(141, 155, 119, 0.22); color: #516142; }

.session-chip {
  padding: 8px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(255, 250, 245, 0.8);
  color: var(--muted);
  font-size: 0.9rem;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.book-format {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.66rem;
  font-weight: 700;
  color: var(--accent-dark);
  padding: 2px 6px;
  border-radius: 5px;
  background: transparent;
  border: 1px solid rgba(186, 125, 74, 0.35);
}

.visibility-chip {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: capitalize;
  letter-spacing: 0.04em;
}
.visibility-chip[data-visibility="public"] {
  color: var(--success);
}

/* ---------- Forms ---------- */
.dashboard-hero, .dashboard-grid, .stack-form, .reader-layout, .highlight-actions {
  display: grid;
}

/* Dashboard hero (library page intro + upload form).
   Mobile-first: single column, smaller gap. Desktop adds the side-by-side
   layout for the title + upload form panels. */
.dashboard-hero { grid-template-columns: 1fr; gap: 18px; align-items: start; }
.dashboard-hero > div, .dashboard-hero > form { min-width: 0; }
.dashboard-hero > div { display: grid; gap: 10px; align-content: start; }
.reader-layout { gap: 18px; }
@media (min-width: 768px) {
  .dashboard-hero {
    grid-template-columns: minmax(0, 1fr) minmax(320px, 420px);
    gap: 32px;
  }
  .dashboard-hero > div { gap: 12px; }
  .reader-layout { gap: 32px; }
}

/* Page-level headings.
   Mobile-first: tight, breakable. Desktop scales up via clamp. */
.dashboard-title, .auth-title {
  margin: 0;
  font-family: "Cormorant Garamond", serif;
  font-size: var(--m-fs-h1);
  line-height: 1.2;
  word-wrap: break-word;
  overflow-wrap: break-word;
}
@media (min-width: 768px) {
  .dashboard-title { font-size: clamp(2rem, 3.4vw, 2.8rem); line-height: 1.05; }
  .auth-title { font-size: clamp(2rem, 4vw, 3rem); line-height: 1.05; }
}

/* Stack form (sign-up, settings, etc.).
   Mobile-first: tighter gap, smaller font, ≥16px input font (iOS zoom). */
.stack-form { gap: 12px; }
.stack-form label {
  display: grid;
  gap: 8px;
  color: var(--muted);
  font-size: var(--m-fs-meta);
}
.stack-form input,
.stack-form select,
.stack-form textarea {
  width: 100%;
  padding: 12px 14px;
  border-radius: 16px;
  border: 1px solid var(--line);
  background: rgba(255, 251, 246, 0.9);
  color: var(--text);
  min-height: var(--m-tap);
  font-size: var(--m-fs-input);
}
@media (min-width: 768px) {
  .stack-form { gap: 14px; }
  .stack-form label { font-size: 0.95rem; }
  .stack-form input,
  .stack-form select,
  .stack-form textarea { padding: 14px 16px; min-height: var(--tap); font-size: inherit; }
}

.stack-form input[aria-invalid="true"],
.stack-form select[aria-invalid="true"] {
  border-color: var(--danger);
  background: var(--danger-soft);
}

.stack-form input[type="file"] {
  padding: 10px 12px;
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stack-form label { min-width: 0; }

.stack-form input::placeholder { color: var(--muted); opacity: 0.6; }

/* ---------- Upload drop zone ---------- */
.dropzone {
  position: relative;
  display: block;
  border: 1.5px dashed var(--line-strong);
  border-radius: 16px;
  background: rgba(255, 251, 246, 0.65);
  padding: 28px 22px;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}
.dropzone.is-dragging {
  background: var(--accent-soft);
  border-color: var(--accent-dark);
  transform: scale(1.005);
}
.dropzone:focus-visible {
  outline: 2px solid var(--accent-dark);
  outline-offset: 2px;
}
.dropzone-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
/* When a file is picked, the input must not capture clicks meant for the
   "Remove" button. */
.dropzone:has([data-dropzone-filled]:not([hidden])) .dropzone-input { display: none; }

.dropzone-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 8px;
  pointer-events: none;
  color: var(--muted);
}
/* `display: flex` above out-specifies the user-agent `[hidden] { display:none }`,
   so without these explicit rules `el.hidden = true` would have no visual
   effect and `applyFile()` wouldn't visibly swap empty<->filled states. */
.dropzone-empty[hidden] { display: none; }
.dropzone-icon { color: var(--accent-dark); }
.dropzone-headline { margin: 0; color: var(--text); font-size: 0.98rem; }
.dropzone-headline .text-link { font-weight: 600; }
.dropzone-sub { margin: 0; font-size: 0.85rem; }

.dropzone-filled { position: relative; z-index: 2; }
.dropzone-filled[hidden] { display: none; }
.dropzone-file {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
}
.dropzone-format {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 54px;
  padding: 8px 12px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  border-radius: 10px;
}
.dropzone-file-info { min-width: 0; }
.dropzone-filename {
  margin: 0;
  color: var(--text);
  font-weight: 600;
  font-size: 0.95rem;
  word-break: break-word;
  overflow-wrap: anywhere;
}
.dropzone-filesize {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}
.dropzone-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 50%;
  background: rgba(85, 66, 43, 0.06);
  color: var(--muted);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.dropzone-remove:hover { background: rgba(180, 60, 50, 0.12); color: var(--danger-text, #b04030); }

.upload-meta-fields {
  display: grid;
  gap: 14px;
}
.upload-meta-fields[hidden] { display: none; }

/* Convert-to-EPUB row inside the upload form */
.stack-form .checkbox-row {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 12px;
  padding: 12px 14px;
  background: var(--accent-soft);
  border: 1px solid rgba(85, 66, 43, 0.1);
  border-radius: 14px;
  cursor: pointer;
}
.stack-form .checkbox-row input[type="checkbox"] {
  margin: 4px 0 0;
  width: 18px;
  height: 18px;
  accent-color: var(--accent-dark);
  cursor: pointer;
}
.stack-form .checkbox-row strong {
  display: block;
  color: var(--text);
  font-weight: 600;
  font-size: 0.95rem;
}
.stack-form .checkbox-row small {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.4;
}
.stack-form .checkbox-row[hidden] { display: none; }

/* "When should I convert?" disclosure inside the convert checkbox row.
   Compact by default — just a muted summary line — expands inline when
   the user wants the full guidance. <summary> uses the system disclosure
   triangle (cross-platform, no JS, accessible). */
.convert-details {
  margin-top: 8px;
  font-size: 0.82rem;
  color: var(--muted);
}
.convert-details[hidden] { display: none; }
.convert-details > summary {
  cursor: pointer;
  display: inline-block;
  padding: 2px 0;
  color: var(--accent-dark, #6f4a2a);
  font-weight: 600;
  list-style-position: outside;
}
.convert-details > summary:hover { text-decoration: underline; }
.convert-details > summary:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 4px;
}
.convert-details[open] > summary { margin-bottom: 6px; }
.convert-details > div > p {
  margin: 0 0 6px;
  line-height: 1.45;
}
.convert-details > div > p:last-child { margin-bottom: 0; }
.convert-details strong { color: var(--text); font-weight: 600; }

/* Show/hide-password toggle inside a password input */
.input-with-action {
  position: relative;
  display: block;
}
.input-with-action input {
  padding-right: 72px;
}
.input-action {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  background: transparent;
  border: 0;
  padding: 8px 12px;
  border-radius: 999px;
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  min-height: 32px;
}
.input-action:hover { color: var(--text); background: rgba(85, 66, 43, 0.08); }
.input-action[aria-pressed="true"] { color: var(--accent-dark); }

/* Surface cards (auth, upload, empty-state, book-detail-card).
   Mobile-first: tighter padding + smaller radius. Desktop expands.
   .empty-state's own padding is set above; it's listed here only to share
   the surface treatment (border + bg + shadow). */
.auth-card, .upload-card, .empty-state, .book-detail-card {
  padding: 18px;
  border: 1px solid var(--line);
  border-radius: var(--m-radius-card);
  background: rgba(255, 251, 246, 0.82);
  box-shadow: var(--shadow);
  min-width: 0;
  overflow: hidden;
}
.auth-card { margin-top: 16px; padding: 22px; }
@media (min-width: 768px) {
  .auth-card, .upload-card, .empty-state, .book-detail-card {
    padding: 26px;
    border-radius: var(--radius-lg);
  }
  .auth-card { margin-top: 32px; padding: 26px; }
}

.auth-copy, .auth-footer, .form-message,
.book-time, .book-panel p { color: var(--muted); }

.form-message {
  min-height: 1.4em;
  margin: 0;
  font-size: 0.92rem;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.form-message.is-error { color: var(--danger); }

/* Google sign-in button. Rendered as a plain anchor (the start handler
   uses a GET redirect, no JS needed). Matches Google's brand guidelines
   for "Continue with Google" — white background, dark text, multi-color
   "G" mark. */
.button.google-signin {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 11px 16px;
  background: #ffffff;
  color: #1f1813;
  border: 1px solid rgba(85, 66, 43, 0.2);
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.95rem;
  text-decoration: none;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.button.google-signin:hover { background: #f7f1e8; border-color: rgba(85, 66, 43, 0.32); }
.google-signin-mark { flex-shrink: 0; }

/* "or sign in with email" divider — horizontal lines flanking centered text. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 14px 0 4px;
  color: var(--muted);
  font-size: 0.82rem;
}
.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: rgba(85, 66, 43, 0.15);
}
.form-message.is-success { color: var(--success); }

.auth-footer { margin: 0; }

/* Upload progress bar */
.upload-progress {
  display: none;
  height: 8px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}
.upload-progress.is-active { display: block; }
.upload-progress-bar {
  height: 100%;
  width: 0%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 0.18s ease;
}


/* === 04-library.css === */
/* ---------- Dashboard library grid ----------
   Mobile-first: tighter min column, smaller gap. Desktop relaxes both. */
.dashboard-grid {
  /* min(150px, 100%) keeps the grid-track floor from forcing horizontal
     overflow on viewports narrower than ~178px (150px + page-shell padding).
     At normal phone widths it behaves identically to a fixed 150px floor. */
  grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr));
  gap: 10px;
  align-items: stretch; /* equal-height cards across siblings */
  max-width: 100%;
  min-width: 0;
}
.dashboard-grid > * { min-width: 0; }
@media (min-width: 768px) {
  .dashboard-grid { grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); gap: 12px; }
}

.library-section { display: grid; gap: 16px; }

.section-row {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  flex-wrap: wrap;
}
.section-row .section-label { margin: 0; }
.section-row .session-chip {
  padding: 2px 10px;
  min-height: 0;
  max-width: none;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: normal;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border: 0;
}

/* Book panel (library card).
   Mobile-first: 12px padding, smaller title, --m-radius-card.
   Desktop bumps padding and radius. height:100% always (so margin-top:auto
   on the action row pushes consistently across siblings of different title
   lengths). */
.book-panel {
  padding: 10px 10px 14px;
  /* Almost-invisible border + transparent background so cards don't read
     as strong filled containers against the body. The cover and title do
     the visual work; the card outline only emerges on hover. */
  border: 1px solid transparent;
  border-radius: var(--m-radius-card);
  background: transparent;
  display: flex;
  flex-direction: column;
  gap: 4px;
  height: 100%;
  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.book-panel:hover, .book-panel:focus-within {
  transform: translateY(-3px);
  border-color: rgba(85, 66, 43, 0.14);
  box-shadow: 0 18px 32px -22px rgba(34, 28, 22, 0.25);
}
.book-panel h2 {
  font-size: 1.02rem;
  margin: 6px 0 2px;
  line-height: 1.22;
  font-weight: 600;
  letter-spacing: -0.005em;
  /* Clamp to 2 lines so a long title can't push the card height past its
     siblings in the dashboard grid. -webkit-line-clamp is widely supported
     and handles ellipsis automatically. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
/* Author line: smaller, muted, single-line truncation so card heights
   stay aligned across the grid regardless of author name length. */
.book-panel p {
  font-size: 0.82rem;
  margin: 0;
  color: var(--muted);
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Description snippet — only rendered on /discover cards, never on owner
   dashboard cards (owners know their books; strangers need the context).
   Two-line clamp keeps every card the same height so the grid stays neat
   regardless of how chatty the description is. Override the parent p's
   white-space:nowrap so wrapping actually happens. */
.book-panel .book-panel-description {
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--muted);
  margin: 6px 0 2px;
  white-space: normal;
  text-overflow: clip;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
@media (min-width: 768px) {
  .book-panel { padding: 12px 12px 16px; border-radius: 18px; }
  .book-panel h2 { font-size: 1.08rem; }
  .book-panel p { font-size: 0.85rem; }
}

.book-cover-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  margin: -4px 0 10px;
  border-radius: 10px;
  overflow: hidden;
  position: relative;
  /* Warm gradient backdrop. For plain extracted covers (a PDF first page
     that's nearly white) this shows softly through transparent edges so
     the cover doesn't read as an empty white rectangle. Real cover art
     (printed jpg/png) opaquely covers it, so there's no visual effect on
     books that already have proper art. */
  background: linear-gradient(135deg, #ece1cb 0%, #d9c7a4 100%);
  border: 1px solid rgba(85, 66, 43, 0.1);
  box-shadow: 0 1px 2px rgba(34, 28, 22, 0.04);
}
.book-cover-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.5s ease;
}
/* Book-spine illusion — a hair-thin darker stripe on the left edge of
   every cover. Subtle on real cover art (looks like the spine fold);
   gives plain covers a small designed detail so they don't look empty. */
.book-cover-thumb::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg, rgba(34, 28, 22, 0.1) 0%, rgba(34, 28, 22, 0.02) 3%, transparent 7%);
  border-radius: inherit;
}
.book-panel:hover .book-cover-thumb img,
.book-panel:focus-within .book-cover-thumb img { transform: scale(1.04); }
.book-panel.has-cover h2 { margin-top: 0; }

/* Quiet bookmark button on browse cards — keeps "Read" as the dominant
   CTA. Filled icon = wishlisted, outlined = available to save. Shown
   only for signed-in users (template gates it). */
.book-panel-wishlist {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 999px;
  border: 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease, transform 0.08s ease;
}
.book-panel-wishlist:hover {
  color: var(--accent-dark);
  background: rgba(186, 125, 74, 0.1);
}
.book-panel-wishlist[aria-pressed="true"] { color: var(--accent-dark); }
.book-panel-wishlist:active { transform: scale(0.94); }

.cover-upload input[type="file"] {
  padding: 10px 12px;
}
.cover-upload small { color: var(--muted); font-weight: 400; }

/* Auto-generated cover preview shown on the upload form. Mobile-first:
   stacked column with thumbnail centered. Desktop: thumbnail next to label
   and re-roll button. */
.cover-preview {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--m-radius-card);
  background: rgba(255, 251, 246, 0.8);
}
.cover-preview[hidden] { display: none; }
.cover-preview-img {
  width: 120px;
  height: 180px;
  object-fit: cover;
  border-radius: 10px;
  background: var(--accent-soft);
  border: 1px solid var(--line);
  align-self: center;
}
.cover-preview-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: stretch;
}
.cover-preview-label { margin: 0; color: var(--muted); font-size: 0.88rem; line-height: 1.4; }
.cover-preview-buttons {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: stretch;
}
.cover-preview-buttons > [hidden] { display: none !important; }
@media (min-width: 768px) {
  .cover-preview { flex-direction: row; align-items: center; gap: 16px; }
  .cover-preview-img { align-self: flex-start; }
  .cover-preview-actions { flex: 1 1 auto; }
}

.book-panel-top, .book-panel-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}
.book-panel-top { justify-content: space-between; }

/* Pin the actions row to the bottom of the card so siblings line up
   regardless of title height. Mobile-first: tighter gap + smaller buttons.
*/
.book-panel-actions {
  margin-top: auto;
  padding-top: 10px;
  border-top: 1px solid rgba(85, 66, 43, 0.08);
  /* Wrap when there's not enough room for Continue + icons on one line.
     Previously this was `nowrap` for owner cards (Continue + 3 icons),
     and the text button was set up to ellipsize — but on narrow grid
     columns above 768px the pill's border-radius clipped the trailing
     letter before the ellipsis could render, producing "Continu"
     instead of "Continue". Allowing wrap means on cramped columns the
     text button takes row 1 alone and the icons share row 2 — same
     behaviour public cards already had. */
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
}
.book-panel-actions .button:not(.icon-only) {
  /* min-width 110px ensures the label ("Continue", "Resume", "Read")
     always fits — no more mid-letter truncation. The button grows to
     fill remaining row space; if 110px doesn't fit alongside the
     icons, the row wraps so this button gets a full row to itself. */
  flex: 1 1 110px;
  min-width: 110px;
  padding: 8px 10px;
  font-size: 0.9rem;
  min-height: 36px;
  white-space: nowrap;
}
.book-panel-actions .button.icon-only {
  width: 34px;
  min-width: 34px;
  min-height: 34px;
}

@media (min-width: 768px) {
  .book-panel-actions { padding-top: 14px; gap: 8px; }
  .book-panel-actions .button:not(.icon-only) { padding: 10px 16px; font-size: 0.95rem; min-height: 40px; }
  .book-panel-actions .button.icon-only { width: 40px; min-width: 40px; min-height: 40px; }
}

/* Inside a card, the icon button's hover tooltip should drop BELOW the
   button — placing it above (the global default) covers the Read button
   on narrow phone cards where it sits very close. */
.book-panel-actions .button.icon-only[aria-label]::after {
  bottom: auto;
  top: calc(100% + 6px);
  transform: translateX(-50%) translateY(-4px);
}
.book-panel-actions .button.icon-only[aria-label]:hover::after,
.book-panel-actions .button.icon-only[aria-label]:focus-visible::after {
  transform: translateX(-50%) translateY(0);
}
/* Compact icon-only action buttons used on book cards. Keeps the action
   row to a single line ("Read [🔓] [↗] [🗑]") instead of three full-width
   stacked buttons. The `.book-panel-danger` modifier tints the trash icon
   red so a destructive action remains visually distinct from neutral ones. */
.button.icon-only {
  position: relative;
  width: 36px;
  min-width: 36px;
  min-height: 36px;
  padding: 0;
  flex: 0 0 auto;
  background: transparent;
  border: 1px solid var(--line);
  color: var(--muted);
}
.button.icon-only:hover { background: rgba(255, 250, 245, 0.9); color: var(--text); }
.button.icon-only.book-panel-danger { color: var(--danger, #b04030); border-color: rgba(176, 64, 48, 0.25); }
.button.icon-only.book-panel-danger:hover { background: rgba(176, 64, 48, 0.08); color: var(--danger, #b04030); }
@media (min-width: 768px) {
  .button.icon-only { width: 40px; min-width: 40px; min-height: 40px; }
}

/* Custom tooltip on hover/focus — shows the icon button's aria-label as a
   small dark pill above the button. Beats the browser's default title
   tooltip (slow to appear, inconsistent styling). Hidden on touch devices
   where hover doesn't apply naturally. */
.button.icon-only[aria-label]::after {
  content: attr(aria-label);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: 4px 8px;
  background: rgba(34, 28, 22, 0.92);
  color: #fff8f0;
  font-size: 0.75rem;
  font-weight: 500;
  white-space: nowrap;
  border-radius: 6px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease, transform 0.12s ease;
  z-index: 5;
}
.button.icon-only[aria-label]:hover::after,
.button.icon-only[aria-label]:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
@media (hover: none) {
  /* Touch devices: skip the custom tooltip — it would stick on tap. The
     aria-label still serves screen readers; long-press still surfaces the
     native title attribute. */
  .button.icon-only[aria-label]::after { display: none; }
}

/* Empty state.
   Mobile-first: tighter padding, --m-radius-card. Desktop expands. */
.empty-state {
  text-align: left;
  padding: 18px;
  background: transparent;
  border: 1px dashed var(--line-strong);
  border-radius: var(--m-radius-card);
  box-shadow: none;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
}
@media (min-width: 768px) {
  .empty-state { padding: 26px 28px 28px; }
}
.empty-state h2, .empty-state p { margin: 0; }
.empty-state .button { margin-top: 4px; }
.empty-state > * { min-width: 0; max-width: 100%; }
.empty-state .button { width: auto; align-self: flex-start; }

/* ---------- Book detail / share page ----------
   Compact card-modal layout. Mobile-first: single-column, centered, smaller
   cover and tighter spacing. Desktop: cover-on-left + metadata-on-right. */
.book-detail {
  display: grid;
  grid-template-columns: 1fr;
  justify-items: center;
  text-align: center;
  gap: 14px;
  align-items: start;
  max-width: 100%;
  margin: 16px auto 24px;
  padding: 18px;
  background: rgba(255, 251, 246, 0.92);
  border: 1px solid rgba(85, 66, 43, 0.1);
  border-radius: var(--m-radius-card);
  box-shadow: var(--shadow);
}
.book-detail-cover {
  width: 140px;
  aspect-ratio: 2 / 3;
  border-radius: 12px;
  overflow: hidden;
  background: rgba(85, 66, 43, 0.06);
  position: relative;
}
.book-detail-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Netflix-style progress sliver overlaid on the bottom edge of a cover.
   --pct comes from the template (0–100).
   The bar has to be readable against *any* cover color — dark reds,
   white pages, photos, illustrations — so we layer two contrast cues:
     1. A translucent-white track makes it pop against dark covers.
     2. A hair-line dark shadow ABOVE the bar makes it pop against
        covers whose bottom edge happens to be white/cream.
   The fill is a saturated warm orange (brighter than --accent-dark)
   so it stays visible against the red/brown covers where the original
   brown-on-brown blended invisibly. */
.cover-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 4px;
  background: rgba(255, 255, 255, 0.55);
  box-shadow: 0 -1px 0 rgba(34, 28, 22, 0.4);
  pointer-events: none;
  z-index: 2;
}
.cover-progress::after {
  content: "";
  position: absolute;
  inset: 0 calc(100% - var(--pct, 0%)) 0 0;
  background: #d97c3a;
}
.book-detail-meta { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.book-detail-meta h1 {
  font-family: "Cormorant Garamond", serif;
  font-size: 1.4rem;
  line-height: 1.2;
  margin: 4px 0 0;
}
@media (min-width: 768px) {
  .book-detail {
    grid-template-columns: 160px 1fr;
    justify-items: stretch;
    text-align: left;
    gap: 24px;
    max-width: 640px;
    margin: 32px auto 48px;
    padding: 24px;
    border-radius: 22px;
  }
  .book-detail-cover { width: 160px; }
  .book-detail-meta h1 { font-size: 1.7rem; line-height: 1.15; }
}
.book-detail-chips { display: flex; gap: 8px; flex-wrap: wrap; }
.book-detail-author { font-size: 0.95rem; margin: 0; color: var(--text); }
.book-detail-owner, .book-detail-time { margin: 0; font-size: 0.85rem; color: var(--muted); }
/* Inline metadata row on the book detail page: uploader · added · pages
   · reading time. Separator dots are inserted via ::before so we don't
   need brittle markup or conditional separators in the template. */
.book-detail-meta-line {
  list-style: none;
  padding: 0;
  margin: 4px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 0;
  font-size: 0.85rem;
  color: var(--muted);
}
.book-detail-meta-line li { display: inline-flex; align-items: center; }
.book-detail-meta-line li + li::before {
  content: "·";
  margin: 0 8px;
  color: rgba(85, 66, 43, 0.35);
}
.book-detail-tags { list-style: none; padding: 0; margin: 4px 0 0; display: flex; flex-wrap: wrap; gap: 6px; }
/* Full description on the book detail page — preserve paragraph breaks
   from the source (EPUB <dc:description>) without rendering as HTML. */
.book-detail-description {
  margin: 8px 0 0;
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
}
.book-detail-tags a {
  font-size: 0.78rem;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-dark);
  text-decoration: none;
}
.book-detail-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; justify-content: center; }
@media (min-width: 768px) {
  .book-detail-actions { justify-content: flex-start; }
}
.book-detail-share-hint {
  margin: 6px 0 0;
  font-size: 0.8rem;
  color: var(--accent-dark);
}

/* (Old `.book-detail` mobile block removed — the base rule above is now
   mobile-first and the desktop variant lives in `@media (min-width: 768px)`.) */
/* Primary empty-state (e.g. "Your shelf is empty.").
   Mobile-first: tighter padding, smaller emphasis. Desktop expands. */
.empty-state.is-primary {
  background: rgba(255, 251, 246, 0.82);
  border-style: solid;
  text-align: center;
  padding: 24px 18px;
  box-shadow: var(--shadow);
}
@media (min-width: 768px) {
  .empty-state.is-primary { padding: 36px 26px; }
}
.empty-state.is-primary .button { align-self: center; }
.empty-state h2, .book-panel h2 {
  margin: 6px 0 8px;
  font-family: "Cormorant Garamond", serif;
  font-size: 1.5rem;
  line-height: 1.2;
}
.empty-state:not(.is-primary) h2 { font-size: 1.1rem; font-family: "Manrope", sans-serif; }
.empty-state p { color: var(--muted); margin: 0; }
.empty-state.is-primary p { margin: 0 0 16px; }

.empty-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}


/* === 05-reader.css === */
/* ---------- Reader ---------- */
.reader-layout {
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}

.reader-layout-full {
  grid-template-columns: minmax(0, 1fr);
  /* dvh tracks the visible viewport on mobile — vh includes the collapsed
     URL bar, which makes the layout taller than what's actually visible. */
  min-height: calc(100dvh - 108px);
  gap: 0;
}

.reader-shell { min-height: 70vh; }

.reader-shell-full {
  min-height: calc(100dvh - 108px);
  display: grid;
  align-content: start;
}
@supports not (height: 100dvh) {
  .reader-layout-full, .reader-shell-full { min-height: calc(100vh - 108px); }
}

.reader-card.reader-shell-full {
  background: transparent;
  border: 0;
  box-shadow: none;
  padding: 12px 16px 0;
}

.reader-owner {
  color: var(--muted);
  margin-bottom: 10px;
  font-size: 0.92rem;
}

/* Paginated reader stage container.
   Mobile-first: tighter padding, dvh-based heights, safe-area-aware bottom
   reserve so the floating bottom-bar pill never overlaps the last text line.
   Desktop: more breathing room above. */
.ebook-stage { display: grid; gap: 0; padding-top: 0; }

/* The wrapper used to reserve a chunk of bottom padding for the floating
   button pill. Since v40 the page-nav uses side-arrows on desktop and a
   thin chip on mobile (both `position: fixed`), so the wrapper no longer
   needs to carry that reservation in flow — the explicit stage height
   already accounts for it. Leaving the padding in place pushed the layout
   ~90px past 100dvh, and reader-body's overflow:hidden clipped the result
   at the bottom (= no visible bottom margin around the canvas). */
.ebook-stage-full {
  min-height: 0;
  align-content: start;
  padding: 0;
}
@media (min-width: 768px) {
  .ebook-stage-full { min-height: 0; padding: 0; }
}

/* Reader pagination bar — purely an overlay so content never shifts when
   the reader topbar fades. Floating pill on desktop, full-width strip on
   phones (rules in @media at the bottom of this file). Always visible. */
/* Reader pagination bar — purely a floating overlay so content never
   shifts when the topbar fades. Mobile-first: low-profile pill above the
   home-indicator with safe-area padding. Desktop: same pill with slightly
   more breathing room. Always visible at all viewports.
*/
.ebook-stage-full .ebook-controls {
  position: fixed;
  bottom: calc(14px + env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
/* Desktop redesign: split the floating pill into edge-positioned arrows
   so the buttons don't visually overlap the page area. The container
   becomes transparent and pointer-events:none; each button positions
   itself independently. The page-jumper stays as a compact pill at the
   bottom center. */
@media (min-width: 768px) {
  .ebook-stage-full .ebook-controls {
    position: fixed;
    inset: 0;
    transform: none;
    bottom: auto;
    left: 0;
    right: 0;
    height: auto;
    padding: 0;
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border: 0;
    box-shadow: none;
    pointer-events: none;
    display: block;
  }
  .ebook-stage-full .ebook-controls > * { pointer-events: auto; }

  .ebook-stage-full .ebook-controls .reader-page-btn {
    position: fixed;
    top: 50%;
    transform: translateY(-50%);
    width: 56px;
    height: 56px;
    min-width: 56px;
    border-radius: 999px;
    background: rgba(247, 240, 230, 0.92);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid rgba(85, 66, 43, 0.16);
    box-shadow: 0 8px 24px rgba(34, 28, 22, 0.15);
    padding: 0;
    font-size: 1.4rem;
    font-weight: 600;
    transition: opacity 0.18s ease, background 0.18s ease;
    opacity: 0.85;
  }
  .ebook-stage-full .ebook-controls .reader-page-btn:hover { opacity: 1; }
  .ebook-stage-full .ebook-controls .reader-page-btn:disabled { opacity: 0.3; cursor: default; }
  .ebook-stage-full .ebook-controls .reader-page-btn[data-epub-prev],
  .ebook-stage-full .ebook-controls .reader-page-btn[data-pdf-prev] { left: 24px; }
  .ebook-stage-full .ebook-controls .reader-page-btn[data-epub-next],
  .ebook-stage-full .ebook-controls .reader-page-btn[data-pdf-next] { right: 24px; }
  /* Side arrows are icon-only — drop the text labels at this size. */
  .ebook-stage-full .ebook-controls .reader-page-btn .reader-page-btn-label { display: none; }

  .ebook-stage-full .ebook-controls .page-jumper {
    position: fixed;
    bottom: 18px;
    left: 50%;
    transform: translateX(-50%);
    padding: 6px 14px;
    background: rgba(247, 240, 230, 0.92);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid rgba(85, 66, 43, 0.16);
    border-radius: 999px;
    box-shadow: 0 6px 18px rgba(34, 28, 22, 0.12);
  }
}
.ebook-controls { display: flex; align-items: center; gap: 10px; }

/* Bottom-bar prev/next labels — icon-only on mobile, full text on desktop. */
.reader-page-btn-label,
.page-jumper-label { display: none; }
@media (min-width: 768px) {
  .reader-page-btn-label,
  .page-jumper-label { display: inline; }
}
.ebook-stage-full .ebook-controls .reader-page-btn {
  /* Bigger glyph + bolder weight so the unicode arrow doesn't render as
     a thin hairline in mobile in-app browsers (Instagram / FB / iOS Mail
     webviews) where the default font draws arrow chars at light weight.
     A 52px tap target also clears Apple's 44px minimum comfortably. */
  min-width: 52px;
  min-height: var(--m-tap);
  padding: 6px 14px;
  font-size: 1.45rem;
  line-height: 1;
  font-weight: 800;
  flex: 0 0 auto;
  border-radius: 999px;
  background: rgba(247, 240, 230, 0.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(85, 66, 43, 0.16);
  box-shadow: 0 8px 24px rgba(34, 28, 22, 0.15);
}
.ebook-stage-full .ebook-controls .page-jumper input { width: 48px; }
@media (min-width: 768px) {
  .ebook-stage-full .ebook-controls .reader-page-btn {
    min-height: var(--tap);
    padding: 10px 18px;
    font-size: 0.95rem;
  }
  .ebook-stage-full .ebook-controls .page-jumper input { width: 56px; }
}

/* Inline page jumper — input blends into the pill. Fades with immersive
   mode (matches the topbar) while prev/next arrows stay visible. opacity-
   only so the pill width never changes and prev/next don't reposition. */
.page-jumper {
  display: inline-flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 4px;
  padding: 6px 14px;
  color: var(--muted);
  font-size: 0.92rem;
  font-weight: 500;
  white-space: nowrap;
  transition: opacity 0.25s ease;
  border-radius: 999px;
  background: rgba(247, 240, 230, 0.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(85, 66, 43, 0.16);
  box-shadow: 0 8px 24px rgba(34, 28, 22, 0.15);
}
/* Immersive mode still shows the page-jumper, just dimmed. Reviewer
   feedback: without it, the reader felt "underpowered or ambiguous" —
   no visible progress, no obvious where-am-I cue. Leaving it at low
   opacity (a) keeps the reading surface uncluttered and (b) gives users
   constant context without forcing them to tap to check progress. */
.reader-app.is-immersive .page-jumper {
  opacity: 0.4;
  pointer-events: auto;
  transition: opacity 0.25s ease;
}
.reader-app.is-immersive .page-jumper:hover,
.reader-app.is-immersive .page-jumper:focus-within { opacity: 1; }
.page-jumper input {
  width: 56px;
  padding: 4px 6px;
  border: 0;
  border-bottom: 1.5px solid transparent;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-weight: 600;
  text-align: center;
  border-radius: 0;
  -moz-appearance: textfield;
  transition: border-color 0.15s ease;
}
.page-jumper input:hover { border-bottom-color: rgba(85, 66, 43, 0.25); }
.page-jumper input:focus {
  outline: none;
  border-bottom-color: var(--accent-dark, #6f4a2a);
}
.page-jumper input::-webkit-inner-spin-button,
.page-jumper input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.page-jumper input[data-pdf-page-input]:focus {
  outline: 2px solid var(--accent-dark);
  outline-offset: 1px;
}
.page-jumper-total { color: var(--muted); white-space: nowrap; flex: 0 0 auto; }

/* Chapter jump dropdown (EPUB) */
.page-jumper-select {
  max-width: 220px;
}

/* Paginated reader stage (EPUB iframe / PDF canvas).
   Mobile-first: viewport minus reader-app top/bottom paddings (~22px) and
   the floating bottom-bar reserve. Earlier versions used 100px which left
   only ~36px between content and the floating prev/next pill — buttons
   visually clipped the bottom of the rendered page. 130px gives a clear
   gap regardless of safe-area inset.
   Desktop: the topbar reserves 80px + toolbar ~50px + bottom bar = 180px. */
.epub-stage, .pdf-stage {
  width: 100%;
  height: calc(100dvh - 130px - env(safe-area-inset-bottom));
  min-height: 360px;
  border-radius: 20px;
  background: transparent;
  border: 0;
  overflow: hidden;
  position: relative;
}
@supports not (height: 100dvh) {
  .epub-stage, .pdf-stage {
    height: calc(100vh - 130px - env(safe-area-inset-bottom));
  }
}
@media (min-width: 768px) {
  /* Desktop: side-arrow nav frees vertical space, so we only reserve the
     topbar + small bottom for the page-jumper chip (~120px total). */
  .epub-stage, .pdf-stage { height: calc(100dvh - 120px); }
  @supports not (height: 100dvh) {
    .epub-stage, .pdf-stage { height: calc(100vh - 120px); }
  }
}

.epub-stage iframe {
  width: 100% !important;
  height: 100% !important;
  min-height: 100% !important;
  border: 0;
  background: transparent;
}
/* epub.js wraps the iframe in `.epub-container > .epub-view`. Force both to
   fill so the rendered EPUB page doesn't appear smaller than the stage,
   especially on short pages or pages with explicit body backgrounds. */
.epub-stage > div,
.epub-stage .epub-container,
.epub-stage .epub-view {
  width: 100% !important;
  height: 100% !important;
}
.epub-stage iframe::-webkit-scrollbar { display: none; }
.epub-stage iframe { scrollbar-width: none; }

.reader-loading {
  display: grid;
  place-items: center;
  margin: 0;
  padding: 60px 20px;
  font-family: "Cormorant Garamond", serif;
  font-size: 1.4rem;
  color: var(--muted);
  text-align: center;
}

/* PDF stage scrolls vertically when the canvas is taller than the
   viewport (typical when fit-to-width on mobile or spread-mode on
   desktop). `safe center` keeps the page centered when it fits but
   falls back to top-aligned when overflowing, so the top of the page
   is always visible instead of being clipped by alignment. */
.pdf-stage {
  display: grid;
  place-items: safe center;
  padding: 12px 12px 28px 12px;
  overflow: auto;
  /* anchor for the loading overlay child */
  position: relative;
}

#pdf-canvas {
  max-width: 100%;
  box-shadow: 0 18px 40px rgba(58, 40, 22, 0.16);
  background: #fff;
  display: block;
}

/* Wrapper that hosts the canvas + transparent text layer(s) on top so
   the user can select text drawn on the canvas. Sized via inline-block
   to the canvas's intrinsic CSS dimensions; text layers are positioned
   absolutely against this container. */
.pdf-page-container {
  position: relative;
  display: inline-block;
  max-width: 100%;
  line-height: 0;
}

/* pdf.js text layer — invisible spans positioned to match each glyph
   on the canvas so the browser's text selection engine can grab them.
   The TextLayer class drives span placement; this stylesheet handles
   visibility (transparent text, visible ::selection highlight) and
   pointer behavior. --scale-factor is set inline by renderTextLayer. */
.pdf-text-layer {
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  opacity: 1;
  line-height: 1;
  text-align: initial;
  -webkit-text-size-adjust: none;
  text-size-adjust: none;
  forced-color-adjust: none;
  transform-origin: 0 0;
  z-index: 2;
  cursor: text;
  /* Containing canvas already has the shadow; keep the text layer
     visually identical to the canvas underneath. */
  pointer-events: auto;
}
.pdf-text-layer[hidden] { display: none; }

.pdf-text-layer span,
.pdf-text-layer br {
  color: transparent;
  position: absolute;
  white-space: pre;
  cursor: text;
  transform-origin: 0% 0%;
}

.pdf-text-layer ::selection {
  background: rgba(186, 125, 74, 0.32);
  color: transparent;
}
.pdf-text-layer ::-moz-selection {
  background: rgba(186, 125, 74, 0.32);
  color: transparent;
}

/* Page-turn animation — three nested elements:
   .pdf-flip-clip   — fixed-position, overflow:hidden, sized to the
                      canvas viewport rect. Holds perspective.
   .pdf-flip-page   — transform-style: preserve-3d, rotates the full
                      180° around its spine edge.
   .pdf-flip-face   — front = <img> snapshot of outgoing page,
                      back = paper-coloured div pre-rotated 180° so it
                      reads right-side-up at the end of the flip.

   Why preserve-3d + two faces: archive.org's BookReader and turn.js
   use this exact structure. A real paper page has thickness — front
   visible 0–90°, the edge at 90°, back visible 90–180°. A one-faced
   rotation hides past 90° via backface-visibility and reads as a card
   being thrown away, not a page turning over. */
.pdf-flip-clip {
  position: fixed;
  pointer-events: none;
  z-index: 1000;
  overflow: hidden;
  perspective: 1800px;
  perspective-origin: 50% 50%;
}
.pdf-flip-page {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
  will-change: transform;
}
.pdf-flip-page.is-turn-next {
  transform-origin: 0% 50%;
  animation: moco-pdf-flip-next 520ms cubic-bezier(0.4, 0.0, 0.2, 1) forwards;
}
.pdf-flip-page.is-turn-prev {
  transform-origin: 100% 50%;
  animation: moco-pdf-flip-prev 520ms cubic-bezier(0.4, 0.0, 0.2, 1) forwards;
}
@keyframes moco-pdf-flip-next {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(-180deg); }
}
@keyframes moco-pdf-flip-prev {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(180deg); }
}
.pdf-flip-face {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.pdf-flip-face-front {
  background: #fff;
  box-shadow: 0 18px 40px rgba(58, 40, 22, 0.16);
}
/* Back of paper — pre-rotated 180° around Y so it appears right-side-up
   once the page wrapper has rotated 180°. A faint diagonal gradient gives
   the back face the look of unprinted paper catching ambient light. */
.pdf-flip-face-back {
  transform: rotateY(180deg);
  background: #f4ede3;
  background-image: linear-gradient(135deg,
    rgba(0, 0, 0, 0.06) 0%,
    rgba(0, 0, 0, 0.00) 40%,
    rgba(0, 0, 0, 0.04) 100%);
  box-shadow: 0 18px 40px rgba(58, 40, 22, 0.16);
}
@media (prefers-reduced-motion: reduce) {
  .pdf-flip-clip { display: none; }
}

/* PDF continuous-scroll fullscreen viewer. Mimics the browser's native
   PDF view — dark backdrop, all pages stacked, lazy rendered, scroll +
   zoom. Lives at body level so it covers Moco's own chrome cleanly.
   Activated only via [data-pdf-fullscreen-toggle]; doesn't affect the
   default paginated reader. */
.pdf-fullscreen {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: #2b2b2b;
  display: flex;
  flex-direction: column;
  /* Pad for iOS notch / Dynamic Island so the toolbar isn't overlapped. */
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
body.pdf-fs-active { overflow: hidden; }
.pdf-fs-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  background: #1f1f1f;
  color: #f0f0f0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  flex-wrap: nowrap;
  min-height: 48px;
  flex-shrink: 0;
}
.pdf-fs-spacer { flex: 1 1 auto; }
.pdf-fs-btn {
  background: transparent;
  border: 0;
  color: #f0f0f0;
  font-size: 1.4rem;
  width: 36px;
  height: 36px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  padding: 0;
  line-height: 1;
}
.pdf-fs-btn:hover { background: rgba(255, 255, 255, 0.1); }
.pdf-fs-btn:active { background: rgba(255, 255, 255, 0.16); }
.pdf-fs-pageind {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.9rem;
  color: #d0d0d0;
}
.pdf-fs-pageind input {
  width: 56px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.12);
  color: #f0f0f0;
  border-radius: 4px;
  padding: 4px 6px;
  text-align: center;
  font: inherit;
  -moz-appearance: textfield;
}
.pdf-fs-pageind input::-webkit-inner-spin-button,
.pdf-fs-pageind input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.pdf-fs-zoom { font-size: 0.9rem; color: #d0d0d0; min-width: 48px; text-align: center; }
.pdf-fs-scroll {
  flex: 1 1 auto;
  overflow: auto;
  overflow-anchor: none; /* don't let lazy-rendered canvases nudge scroll */
  -webkit-overflow-scrolling: touch;
  /* Re-enable mobile pinch-zoom — `.reader-body-paginated` sets
     `touch-action: manipulation` which blocks pinch. We override here so
     fingers can pinch the canvas natively. The +/− buttons re-render
     crisply; pinch gives instant feedback (CSS-level, blurry on extreme
     zoom but matches the browser PDF viewer experience). */
  touch-action: pinch-zoom pan-y pan-x;
}
.pdf-fs-pages {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 16px;
  /* Extra bottom padding so content can scroll clear of the iOS home
     indicator + leaves room for the floating note FAB to not overlap
     the last page's edge. */
  padding-bottom: calc(96px + env(safe-area-inset-bottom));
}
.pdf-fs-page {
  background: #fff;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
}
.pdf-fs-page canvas {
  display: block;
}
/* Floating note FAB inside the fullscreen overlay — same idea as the
   paginated reader's FAB but anchored to the overlay instead. */
.pdf-fs-note-fab {
  position: fixed;
  bottom: calc(24px + env(safe-area-inset-bottom));
  right: calc(24px + env(safe-area-inset-right));
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: #f5f1e8;
  color: #2a2a2a;
  border: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.5);
  z-index: 201;
  transition: transform 0.08s ease, opacity 0.18s ease;
  opacity: 0.92;
}
.pdf-fs-note-fab:hover { opacity: 1; transform: scale(1.04); background: #fff; }
.pdf-fs-note-fab:active { transform: scale(0.96); }
@media (max-width: 600px) {
  .pdf-fs-toolbar { gap: 6px; padding: 6px 8px; }
  .pdf-fs-pageind input { width: 44px; }
  .pdf-fs-zoom { min-width: 40px; font-size: 0.85rem; }
  .pdf-fs-pages { padding: 8px; gap: 8px; }
}

/* Loading spinner overlays the reader stage so the user sees feedback
   while PDF.js fetches + rasterizes the first page (or epub.js hydrates
   the iframe). Removed by JS the moment the first render fires —
   without this the user just stares at blank white (PDF) or empty cream
   (EPUB) for the seconds-long loading window. */
.epub-stage, .pdf-stage { position: relative; }
.epub-stage > [data-reader-loading],
.pdf-stage > [data-reader-loading] {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 60px 20px;
  margin: 0;
  z-index: 5;
  pointer-events: none;
  background: rgba(247, 240, 230, 0.55);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  color: var(--muted);
  font-size: 0.95rem;
  transition: opacity 0.25s ease;
}
.epub-stage > [data-reader-loading].is-fading,
.pdf-stage > [data-reader-loading].is-fading {
  opacity: 0;
}

.markdown-reader h1,
.markdown-reader h2,
.markdown-reader h3,
.markdown-reader h4,
.markdown-reader h5,
.markdown-reader h6 {
  font-family: "Cormorant Garamond", serif;
  color: var(--text-soft);
  scroll-margin-top: 96px;
}

.markdown-reader h1 { font-size: clamp(2rem, 4vw, 2.8rem); margin: 0 0 16px; line-height: 1.1; }
.markdown-reader h2 { font-size: clamp(1.6rem, 3vw, 2.1rem); margin: 28px 0 12px; line-height: 1.15; }
.markdown-reader h3 { font-size: clamp(1.3rem, 2.4vw, 1.6rem); margin: 22px 0 10px; line-height: 1.2; }
.markdown-reader h4,
.markdown-reader h5,
.markdown-reader h6 { font-size: clamp(1.1rem, 2vw, 1.35rem); margin: 18px 0 10px; line-height: 1.2; }

.markdown-reader p {
  font-family: var(--reader-font, "Cormorant Garamond", serif);
  font-size: calc(clamp(1.1rem, 1.6vw, 1.3rem) * var(--reader-font-scale, 1));
  line-height: calc(1.7 * var(--reader-line-scale, 1));
  color: var(--text-soft);
}
.markdown-reader li {
  font-family: var(--reader-font, "Cormorant Garamond", serif);
}
.markdown-reader h1 { font-size: calc(clamp(2rem, 4vw, 2.8rem) * var(--reader-font-scale, 1)); }
.markdown-reader h2 { font-size: calc(clamp(1.6rem, 3vw, 2.1rem) * var(--reader-font-scale, 1)); }
.markdown-reader h3 { font-size: calc(clamp(1.3rem, 2.4vw, 1.6rem) * var(--reader-font-scale, 1)); }

/* Long-form reading elements: lists, links, quotes, tables, code */
.markdown-reader ul,
.markdown-reader ol {
  padding-left: 1.6em;
  margin: 0 0 1.1em;
}
.markdown-reader li {
  font-family: "Cormorant Garamond", serif;
  font-size: calc(clamp(1.05rem, 1.5vw, 1.25rem) * var(--reader-font-scale, 1));
  line-height: calc(1.65 * var(--reader-line-scale, 1));
  color: var(--text-soft);
  margin: 4px 0;
}
.markdown-reader li > p { margin: 0 0 0.4em; }

.markdown-reader a {
  color: var(--accent-dark);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(186, 125, 74, 0.5);
}
.markdown-reader a:hover { text-decoration-color: currentColor; }

.markdown-reader blockquote {
  margin: 1.4em 0;
  padding: 14px 20px;
  border-left: 4px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 12px 12px 0;
  color: var(--text-soft);
  font-style: italic;
}
.markdown-reader blockquote > :last-child { margin-bottom: 0; }

.markdown-reader hr {
  border: 0;
  border-top: 1px solid var(--line-strong);
  margin: 2em auto;
  width: 50%;
}

.markdown-reader table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.4em 0;
  font-family: "Manrope", sans-serif;
  font-size: calc(0.95rem * var(--reader-font-scale, 1));
  display: block;
  overflow-x: auto;
}
.markdown-reader thead { background: var(--accent-soft); }
.markdown-reader th,
.markdown-reader td {
  padding: 10px 14px;
  border: 1px solid var(--line);
  text-align: left;
  color: var(--text);
}
.markdown-reader th { font-weight: 700; color: var(--accent-dark); }
.markdown-reader tbody tr:nth-child(odd) td { background: rgba(85, 66, 43, 0.03); }

/* Inline code */
.markdown-reader code {
  font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.92em;
  padding: 2px 6px;
  border-radius: 6px;
  background: rgba(85, 66, 43, 0.08);
  color: var(--accent-dark);
}

/* Fenced code blocks */
.markdown-reader pre {
  position: relative;
  margin: 1.5em 0;
  padding: 16px 18px;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: rgba(85, 66, 43, 0.06);
  overflow-x: auto;
  font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: calc(0.92rem * var(--reader-font-scale, 1));
  line-height: 1.6;
  -webkit-overflow-scrolling: touch;
}
.markdown-reader pre code {
  display: block;
  padding: 0;
  background: transparent;
  color: var(--text);
  font-size: inherit;
}
.reader-body[data-reader-theme="dark"] .markdown-reader pre { background: rgba(0, 0, 0, 0.45); }
.reader-body[data-reader-theme="sepia"] .markdown-reader pre { background: rgba(85, 66, 43, 0.10); }
.reader-body[data-reader-theme="paper"] .markdown-reader pre { background: rgba(60, 50, 37, 0.10); }

/* Copy button on code blocks */
.code-copy {
  position: absolute;
  top: 8px;
  right: 8px;
  padding: 4px 10px;
  font-size: 0.78rem;
  font-weight: 600;
  background: var(--panel-strong);
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s ease, color 0.15s ease;
  min-height: 0;
}
.markdown-reader pre:hover .code-copy,
.markdown-reader pre:focus-within .code-copy { opacity: 1; }
.code-copy:hover { color: var(--text); }
.code-copy.is-copied { color: var(--success); border-color: rgba(74, 125, 86, 0.4); }
/* On touch devices (no hover), the copy button has no hover trigger to
   reveal it — keep it visible. Detected via interaction-media-features
   instead of a viewport width, which is the semantically correct test. */
@media (hover: none) {
  .code-copy { opacity: 1; }
}

/* Prism syntax tokens — tuned to the Moco palette */
.markdown-reader .token.comment,
.markdown-reader .token.prolog,
.markdown-reader .token.doctype,
.markdown-reader .token.cdata { color: var(--muted); font-style: italic; }
.markdown-reader .token.punctuation { color: var(--text-soft); }
.markdown-reader .token.property,
.markdown-reader .token.tag,
.markdown-reader .token.boolean,
.markdown-reader .token.number,
.markdown-reader .token.constant,
.markdown-reader .token.symbol,
.markdown-reader .token.deleted { color: #b46336; }
.markdown-reader .token.selector,
.markdown-reader .token.attr-name,
.markdown-reader .token.string,
.markdown-reader .token.char,
.markdown-reader .token.builtin,
.markdown-reader .token.inserted { color: #517a4f; }
.markdown-reader .token.operator,
.markdown-reader .token.entity,
.markdown-reader .token.url { color: var(--accent-dark); }
.markdown-reader .token.atrule,
.markdown-reader .token.attr-value,
.markdown-reader .token.keyword { color: var(--accent-dark); font-weight: 600; }
.markdown-reader .token.function,
.markdown-reader .token.class-name { color: #6e4f93; }
.markdown-reader .token.regex,
.markdown-reader .token.important,
.markdown-reader .token.variable { color: #b46336; }
.markdown-reader .token.important,
.markdown-reader .token.bold { font-weight: 700; }
.markdown-reader .token.italic { font-style: italic; }
.markdown-reader h4,
.markdown-reader h5,
.markdown-reader h6 { font-size: calc(clamp(1.1rem, 2vw, 1.35rem) * var(--reader-font-scale, 1)); }

.markdown-reader a {
  color: var(--accent-dark);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.12em;
}

.markdown-reader ul,
.markdown-reader ol {
  margin: 0 0 20px;
  padding-left: 26px;
  display: grid;
  gap: 10px;
}

.markdown-reader li,
.markdown-reader blockquote p {
  font-family: "Cormorant Garamond", serif;
  font-size: calc(clamp(1.1rem, 1.6vw, 1.3rem) * var(--reader-font-scale, 1));
  line-height: calc(1.7 * var(--reader-line-scale, 1));
  color: var(--text-soft);
}

.markdown-reader blockquote {
  margin: 0 0 20px;
  padding: 14px 18px;
  border-left: 4px solid rgba(163, 118, 83, 0.45);
  background: rgba(234, 220, 203, 0.42);
  border-radius: 18px;
}

.markdown-reader pre {
  margin: 0 0 22px;
  padding: 18px;
  overflow: auto;
  border-radius: 18px;
  background: rgba(48, 40, 32, 0.94);
  color: #f3ede5;
}

.markdown-reader code {
  font-family: "SFMono-Regular", "Menlo", monospace;
  font-size: 0.92em;
}

.markdown-reader p code,
.markdown-reader li code,
.markdown-reader blockquote code {
  padding: 0.1em 0.34em;
  border-radius: 8px;
  background: rgba(214, 173, 104, 0.2);
  color: inherit;
}

.markdown-reader hr {
  margin: 28px 0;
  border: 0;
  border-top: 1px solid rgba(85, 66, 43, 0.12);
}

.reader-section {
  display: grid;
  gap: 2px;
}

.saved-highlight {
  padding: 0.05em 0.14em;
  border-radius: 0.28em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

.saved-highlight-amber {
  background: rgba(236, 196, 120, 0.48);
  color: inherit;
}

.saved-highlight-sage {
  background: rgba(180, 199, 160, 0.42);
  color: inherit;
}

.reader-page-full {
  max-width: 720px;
  min-height: calc(100vh - 260px);
  margin: 0 auto;
  padding: 26px 6px 24px;
}

.bullet { margin-left: 8px; }

.reader-notes { grid-template-columns: 1fr; padding: 22px; }

.reader-sidebar {
  display: none;
  position: fixed;
  top: 80px;
  bottom: 12px;
  z-index: 18;
  width: min(380px, calc(100% - 24px));
  max-height: calc(100vh - 100px);
  background: var(--panel-strong);
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  padding: 22px;
  overflow: auto;
}
.reader-sidebar.is-open { display: grid; align-content: start; }
.reader-sidebar[data-reader-panel="toc"] {
  left: 12px;
  right: auto;
}
.reader-sidebar[data-reader-panel="notes"] {
  right: 12px;
  left: auto;
}

.reader-sidebar-close {
  display: inline-flex;
  position: absolute;
  top: 10px;
  right: 10px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: rgba(255, 250, 245, 0.9);
  cursor: pointer;
  align-items: center;
  justify-content: center;
}

.highlight-actions { gap: 10px; margin-top: 22px; }

/* Reading settings panel */
.settings-row { display: grid; gap: 8px; margin-top: 16px; }
.settings-row label { color: var(--muted); font-size: 0.9rem; }
.settings-row input[type="range"] { width: 100%; }
.theme-picker {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
.theme-swatch {
  display: grid;
  place-items: center;
  padding: 14px 8px;
  border-radius: 14px;
  border: 1px solid var(--line);
  cursor: pointer;
  font-weight: 600;
  font-size: 0.85rem;
  background: var(--swatch-bg, transparent);
  color: var(--swatch-fg, var(--text));
  min-height: 56px;
}
.theme-swatch[data-theme="light"]  { --swatch-bg: #f7f1e8; --swatch-fg: #30261d; }
.theme-swatch[data-theme="paper"]  { --swatch-bg: #ebe2cd; --swatch-fg: #2a241c; }
.theme-swatch[data-theme="sepia"]  { --swatch-bg: #f3e8d4; --swatch-fg: #4a3a25; }
.theme-swatch[data-theme="dark"]   { --swatch-bg: #16130f; --swatch-fg: #f1e6d3; }
.theme-swatch[aria-pressed="true"] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Reading-style and line-height pickers reuse .theme-picker grid */
.font-swatch, .line-swatch {
  display: grid;
  place-items: center;
  padding: 12px 8px;
  border-radius: 14px;
  border: 1px solid var(--line);
  cursor: pointer;
  font-weight: 600;
  font-size: 0.85rem;
  background: rgba(255, 251, 246, 0.7);
  color: var(--text);
  min-height: 44px;
}
.font-swatch[data-font-family="serif"]    { font-family: "Cormorant Garamond", Georgia, serif; font-size: 1rem; }
.font-swatch[data-font-family="sans"]     { font-family: "Manrope", system-ui, sans-serif; }
.font-swatch[data-font-family="original"] { font-style: italic; }
.font-swatch[aria-pressed="true"],
.line-swatch[aria-pressed="true"] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  background: var(--accent-soft);
}

/* Hide the page-layout picker on screens too narrow to fit two pages —
   both options collapse to one page below this width (epub.js's responsive
   spread), so showing the toggle would just mislead users: they'd pick
   "Two pages" and still see one, because there's no room for more.
   The saved preference is preserved; it reactivates when viewed wide. */
@media (max-width: 899px) {
  .settings-row--spread { display: none; }
}

/* Page-layout picker — same shape as font/line swatches so the selected
   layout is visibly outlined (was previously unstyled, so both buttons
   looked identical regardless of which was active). */
.spread-swatch {
  display: grid;
  place-items: center;
  padding: 12px 8px;
  border-radius: 14px;
  border: 1px solid var(--line);
  cursor: pointer;
  font-weight: 600;
  font-size: 0.85rem;
  background: rgba(255, 251, 246, 0.7);
  color: var(--text);
  min-height: 44px;
}
.spread-swatch[aria-pressed="true"] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  background: var(--accent-soft);
}

/* Always-visible escape hatch back to the library — works regardless of
   immersive state and doesn't depend on :has(). */
.reader-floating-close {
  position: fixed;
  top: 14px;
  right: 14px;
  width: 44px;
  height: 44px;
  display: none; /* shown only via .is-visible (immersive mode) */
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(34, 28, 22, 0.82);
  color: #fff8f0;
  text-decoration: none;
  z-index: 50;
  border: 1px solid rgba(255, 248, 240, 0.18);
  box-shadow: var(--shadow);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0.92;
  transition: opacity 0.18s ease, transform 0.08s ease, background 0.18s ease;
  cursor: pointer;
}
.reader-floating-close.is-visible { display: inline-flex; }
.reader-floating-close .icon { width: 22px; height: 22px; }

/* While any reader sidebar (TOC / highlights / settings) or modal is
   open, hide the floating page controls and the floating close button.
   They live at higher z-index than the sidebar (z:30 / z:50 vs z:18) and
   without this they overlap panel content — see the side-arrow circle
   bleeding over the Dark theme swatch in the settings panel. */
.reader-panel-open .ebook-stage-full .ebook-controls,
.reader-panel-open .reader-floating-close.is-visible,
.reader-panel-open .reader-fab {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}

/* Floating "Add a note" action button for the PDF reader. The .pdf-stage
   has no text-selection layer, so the only way to attach a note is from
   here. Always visible (when not in immersive mode / no panel open) so
   the entry point isn't buried in the Highlights sidebar.
   Mobile: compact 56px round button bottom-right, sits above the
   page-jumper pill (which is centered, ~bottom 14px).
   Desktop: pill with icon + "Add note" label at the bottom-right corner,
   clear of the centered page-jumper and the right-edge side arrow. */
.reader-fab {
  position: fixed;
  /* Pager pill is ~62px tall at bottom: 14px + safe-area, so its top
     sits at ~76px above the viewport bottom. FAB needs to clear that
     by a comfortable margin — 96px gives ~20px breathing room. */
  bottom: calc(96px + env(safe-area-inset-bottom));
  right: 16px;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--text);
  color: #fff8f0;
  border: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.95rem;
  box-shadow: 0 10px 24px rgba(34, 28, 22, 0.28);
  /* Above the pager (z:30) and the floating close (z:50). The FAB is
     the primary action — it should always win the stacking contest. */
  z-index: 55;
  opacity: 1;
  transition: transform 0.08s ease, opacity 0.22s ease, background 0.18s ease;
}
.reader-fab[hidden] {
  display: inline-flex;
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
}
.reader-fab:hover { background: #1f1813; transform: scale(1.04); }
.reader-fab:active { transform: scale(0.96); }
.reader-fab:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}
.reader-fab-label { display: none; }
/* FAB stays visible in immersive mode — it's the only way to attach a
   note to a PDF page, and immersive is the *primary* reading state. The
   topbar/pager hide as expected; the FAB lingers as a single discreet
   action. Slightly dimmer to read as "background-y". */
.reader-app.is-immersive .reader-fab { opacity: 0.7; }
.reader-app.is-immersive .reader-fab:hover { opacity: 1; }
@media (min-width: 768px) {
  .reader-fab {
    bottom: 24px;
    right: 24px;
    width: auto;
    height: auto;
    min-height: 48px;
    padding: 12px 22px;
    border-radius: 999px;
  }
  .reader-fab-label { display: inline; }
}

/* Floating selection popover for "Highlight this passage" */
.selection-popover {
  position: fixed;
  z-index: 60;
  display: none;
  align-items: center;
  gap: 4px;
  padding: 4px;
  border-radius: 999px;
  background: rgba(34, 28, 22, 0.94);
  box-shadow: var(--shadow);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  font-size: 0.88rem;
  pointer-events: auto;
}
.selection-popover.is-open { display: inline-flex; }
.selection-popover::after {
  content: "";
  position: absolute;
  bottom: -6px;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: rgba(34, 28, 22, 0.94);
  border-bottom: 0;
}
.selection-popover button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  background: transparent;
  color: #fff8f0;
  border: 0;
  cursor: pointer;
  font-weight: 600;
  min-height: 36px;
  white-space: nowrap;
}
.selection-popover button:hover { background: rgba(255, 248, 240, 0.12); }
.selection-popover .swatch {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 999px;
  background: #d6ad68;
  border: 1px solid rgba(255, 248, 240, 0.4);
}
.reader-floating-close:hover { opacity: 1; transform: scale(1.05); background: rgba(34, 28, 22, 0.95); }
.reader-floating-close:active { transform: scale(0.94); }
.reader-floating-close:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

.jump-to-progress {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 30;
  display: none;
  padding: 12px 18px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--text);
  color: #fff8f0;
  box-shadow: var(--shadow);
  cursor: pointer;
  font-weight: 600;
  min-height: var(--tap);
}
.jump-to-progress.is-visible { display: inline-flex; align-items: center; gap: 8px; }

/* ---------- Toasts ---------- */
.toast-stack {
  position: fixed;
  z-index: 100;
  right: 16px;
  bottom: 16px;
  display: grid;
  gap: 8px;
  pointer-events: none;
  max-width: calc(100% - 32px);
}
.toast {
  pointer-events: auto;
  padding: 12px 16px;
  border-radius: 14px;
  background: rgba(34, 28, 22, 0.94);
  color: #fff8f0;
  box-shadow: var(--shadow);
  font-size: 0.92rem;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 360px;
  animation: toast-in 0.18s ease-out;
}
.toast.is-error { background: var(--danger); }
.toast.is-success { background: var(--success); color: #fff8f0; }
.toast button {
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  padding: 4px 6px;
  font-size: 1rem;
  margin-left: auto;
}
.toast.is-update { max-width: 420px; }
.toast .toast-action {
  background: rgba(255, 255, 255, 0.16);
  border: 0;
  border-radius: 999px;
  color: inherit;
  padding: 6px 12px;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  margin-left: 6px;
}
.toast .toast-action.is-primary { background: var(--accent); color: #fff; }
.toast .toast-action:hover { filter: brightness(1.12); }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ---------- Modal ---------- */
.modal {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(34, 28, 22, 0.5);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.modal.is-open { display: flex; }
.modal-card {
  width: min(440px, 100%);
  background: var(--panel-strong);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 24px;
  box-shadow: var(--shadow);
  position: relative;
  max-height: calc(100vh - 32px);
  overflow-y: auto;
}
.modal-card.is-wide { width: min(640px, 100%); padding: 0; }
.modal-card.is-wide .book-detail {
  margin: 0;
  max-width: none;
  border: 0;
  box-shadow: none;
  background: transparent;
}
.modal-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  border: 0;
  background: rgba(34, 28, 22, 0.08);
  color: var(--text);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  z-index: 1;
}
.modal-close:hover { background: rgba(34, 28, 22, 0.16); }
.modal-card h2 {
  margin: 0 0 8px;
  font-family: "Cormorant Garamond", serif;
  font-size: 1.6rem;
}
.modal-card p { margin: 0 0 16px; color: var(--muted); }
.modal-card textarea {
  width: 100%;
  min-height: 120px;
  padding: 12px 14px;
  border-radius: 16px;
  border: 1px solid var(--line);
  background: rgba(255, 251, 246, 0.9);
  color: var(--text);
  resize: vertical;
}
.modal-actions {
  margin-top: 16px;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  flex-wrap: wrap;
}

/* Modal loading state — centered spinner shown while the SPA-style book
   detail card fetches. Without this, the wide modal flashes empty with bare
   "Loading…" text. */
.modal-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  min-height: 320px;
  padding: 40px 24px;
  color: var(--muted);
}
.modal-loading-text {
  font-family: "Cormorant Garamond", serif;
  font-size: 1.15rem;
}

/* Edit-book modal — title/author inputs reuse stack-form; the cover row is
   the only edit-specific layout. Mobile-first: stacked column with preview
   on top; desktop puts preview beside the action buttons. */
.edit-book-card { width: min(520px, 100%); }
.edit-book-cover-row {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: stretch;
}
.edit-book-cover-preview {
  width: 120px;
  height: 180px;
  object-fit: cover;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--accent-soft);
  align-self: center;
}
.edit-book-cover-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}
.edit-book-cover-actions .form-hint {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
}
@media (min-width: 768px) {
  .edit-book-cover-row { flex-direction: row; align-items: flex-start; }
  .edit-book-cover-preview { align-self: flex-start; }
  .edit-book-cover-actions { flex: 1 1 auto; }
}

/* ---------- Reader sidebar overlay ---------- */
.reader-overlay {
  position: fixed;
  inset: 0;
  background: rgba(34, 28, 22, 0.4);
  z-index: 14;
  display: none;
}
.reader-overlay.is-open { display: block; }

/* ===== Responsive (mobile-first additions) =====
   Mobile is the default everywhere above. The blocks below ADD desktop /
   wide-viewport adjustments. New components: don't add `max-width` rules
   here — make the base style mobile-correct, and any desktop variation
   goes inside `@media (min-width: 768px)`. */

/* Tablet landscape — multi-column grids start widening here */
@media (min-width: 1024px) {
  .library-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Tablet portrait — desktop-style readers and grids */
@media (min-width: 768px) {
  .library-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Reader panel sidebars: full-screen drawers on mobile (set elsewhere),
     fixed-width side panels on desktop. */
  .reader-sidebar { width: min(380px, calc(100% - 24px)); }

  .reader-page-full { padding: 26px 6px 24px; }

  .reader-preview, .dashboard-hero { gap: 28px; }
  .reader-preview { grid-template-columns: 1fr 0.8fr; }
  /* .feature-band layout is set at its primary rule above — keeping the
     `2fr 1fr` two-column desktop layout there. Overriding it here was a
     leftover from when the hero was a card; left in place it forced the
     paragraph column too wide and broke the proportions. */

  .hero-copy { order: 0; }
  .hero-panel { order: 0; }
}


/* === 06-pages.css === */
/* ---------- Highlights / Quotes page ---------- */
.quotes-hero {
  display: grid;
  gap: 6px;
  padding: 0;
  max-width: 720px;
}
.quotes-hero h1 { font-size: var(--m-fs-h2); }
.quotes-hero .auth-copy { font-size: var(--m-fs-body); line-height: 1.45; }
@media (min-width: 768px) {
  .quotes-hero { gap: 8px; }
  .quotes-hero h1 { font-size: clamp(2rem, 3.4vw, 2.8rem); }
  .quotes-hero .auth-copy { font-size: 1rem; }
}
.quotes-list {
  display: grid;
  gap: 16px;
}
/* Cross-book quote/highlight cards.
   Mobile-first: tighter padding + smaller serif body. Desktop expands. */
.quote-card {
  position: relative;
  padding: 16px;
  background: rgba(255, 251, 246, 0.86);
  border: 1px solid var(--line);
  border-left: 4px solid var(--accent);
  border-radius: var(--m-radius-card);
  box-shadow: var(--shadow);
}
.quote-text {
  margin: 0 0 14px;
  font-family: "Cormorant Garamond", Georgia, serif;
  font-size: 1.05rem;
  line-height: 1.45;
  color: var(--text-soft);
}
@media (min-width: 768px) {
  .quote-card { padding: 28px 32px 24px; border-radius: var(--radius-md); }
  .quote-text { margin: 0 0 16px; font-size: clamp(1.2rem, 2vw, 1.5rem); line-height: 1.55; }
}
.quote-text::before {
  content: "“";
  margin-right: 4px;
  color: var(--accent);
  font-size: 1.4em;
  vertical-align: -0.1em;
}
.quote-text::after {
  content: "”";
  margin-left: 2px;
  color: var(--accent);
  font-size: 1.4em;
  vertical-align: -0.1em;
}
.quote-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding-top: 14px;
  border-top: 1px dashed var(--line);
  font-size: 0.92rem;
}
.quote-cite {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  color: var(--muted);
  min-width: 0;
}
.quote-cite-title {
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}
.quote-cite-author { font-style: italic; }

/* (.quote-card is mobile-first above — no mobile override needed.) */

/* AJAX loading state on result containers */
[data-library-results].is-loading,
[data-quotes-results].is-loading,
main#main.is-spa-loading {
  opacity: 0.55;
  transition: opacity 0.15s ease;
  pointer-events: none;
}

/* Reader loading spinner — calm circular pulse */
.reader-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 60px 20px;
  color: var(--muted);
  font-size: 0.95rem;
}
.reader-spinner {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 3px solid rgba(85, 66, 43, 0.18);
  border-top-color: var(--accent-dark, #905f36);
  animation: reader-spinner-rotate 0.9s linear infinite;
}
@keyframes reader-spinner-rotate { to { transform: rotate(360deg); } }

/* SPA-loading top bar — slim progress strip across the page */
main#main.is-spa-loading::before {
  content: "";
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: linear-gradient(90deg, transparent, var(--accent-dark, #905f36), transparent);
  background-size: 200% 100%;
  animation: spa-loading-stripe 1s linear infinite;
  z-index: 100;
  pointer-events: none;
}
@keyframes spa-loading-stripe {
  0%   { background-position: 100% 0; }
  100% { background-position: -100% 0; }
}

/* Vertical rhythm between library sections */
[data-library-results] {
  display: grid;
  gap: 36px;
  margin-top: 8px;
}

/* ----- Filter toolbars (highlights search, library filter) -----
   Mobile-first: stack inputs as standalone form fields, no outer pill
   container (drops the "double-pill" visual of a rounded box around
   already-rounded inputs). Desktop wraps them in a hug-content pill row.
*/
.quotes-toolbar, .library-toolbar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  padding: 0;
  margin: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  width: 100%;
  min-width: 0;
}
.quotes-toolbar > *, .library-toolbar > * { min-width: 0; }

@media (min-width: 768px) {
  /* Desktop: lay the search + select pills side by side, but stay flat
     (no outer pill background/border/shadow). The inputs are already
     pill-shaped — wrapping them in another rounded container produced the
     "pill inside a pill" visual that read as broken UI. */
  .quotes-toolbar, .library-toolbar {
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    margin-bottom: 8px;
    width: 100%;
    max-width: 100%;
  }
}

/* ----- Search + select inputs -----
   Mobile-first: full-width form fields with 16px font (no iOS zoom),
   ≥48px tap height. Desktop becomes inline pills for the toolbar layout.
*/
.search-input, .select-input {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  /* min-width: 0 lets the input/select shrink below its intrinsic content
     width when a parent flex column is narrower — without it, a long book
     title in a <select> option (or a long input value) widens this row past
     the viewport on small phones. */
  min-width: 0;
  flex: 0 0 auto;
  padding: 10px 14px;
  background: rgba(255, 251, 245, 0.92);
  border: 1px solid var(--line);
  border-radius: var(--m-radius-input);
  min-height: var(--m-tap);
  box-shadow: var(--shadow-soft);
}
@media (min-width: 768px) {
  .search-input, .select-input {
    display: inline-flex;
    width: auto;
    padding: 6px 12px;
    border-radius: 999px;
    min-height: var(--tap);
    background: rgba(255, 250, 245, 0.8);
    box-shadow: none;
  }
  .search-input { flex: 1 1 240px; }
}
.search-input .icon { color: var(--muted); flex-shrink: 0; }

/* ----- Discover toolbar -----
   Flat layout (no outer pill) so the input doesn't end up nested in a
   second rounded container — that "pill-in-pill" look read as broken UI.
   On desktop the search input takes the lion's share and the sort select
   sits beside it. */
.discover-toolbar {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 10px;
  width: 100%;
  min-width: 0;
}
.discover-search {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-width: 0;
  padding: 8px 16px;
  background: rgba(255, 251, 245, 0.92);
  border: 1px solid var(--line);
  border-radius: 999px;
  min-height: var(--m-tap);
  box-shadow: var(--shadow-soft);
}
.discover-search .icon { color: var(--muted); flex-shrink: 0; }
.discover-search input {
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
  border: 0;
  background: transparent;
  outline: none;
  font-size: 0.95rem;
  color: var(--text);
}
.discover-sort {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  background: rgba(255, 251, 245, 0.92);
  border: 1px solid var(--line);
  border-radius: 999px;
  min-height: var(--m-tap);
  box-shadow: var(--shadow-soft);
}
.discover-sort select {
  border: 0;
  background: transparent;
  outline: none;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--text);
  width: 100%;
  min-width: 0;
}
@media (min-width: 768px) {
  .discover-toolbar { flex-direction: row; align-items: center; }
  /* Slimmer than --tap (44/48) so the search row doesn't dominate the
     viewport above the grid. Still well above hit-target minimums on a
     mouse-driven device. */
  .discover-search { flex: 1 1 auto; min-height: 40px; padding: 6px 16px; }
  .discover-sort { flex: 0 0 auto; min-height: 40px; padding: 4px 14px; }
}
/* Quiet footer at the bottom of /discover — gives a stranger two things:
   somewhere to subscribe (RSS) and a tag on what they're looking at. Kept
   deliberately subtle so it doesn't compete with the grid. */
.discover-footer {
  margin-top: 48px;
  padding: 24px 0 16px;
  border-top: 1px solid var(--line);
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted);
}
.discover-footer p { margin: 0; display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: center; }
.discover-footer a {
  color: var(--text);
  text-decoration: none;
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.discover-footer a:hover { color: var(--accent); }
.discover-footer .icon { color: var(--accent); }
.discover-footer-sep { opacity: 0.55; }
/* /about — narrow editorial column for the personal "what is this site"
   page. Wider line-length than the reader so prose breathes without
   feeling cramped. Heading hierarchy matches the rest of the site. */
.about-page { padding: 24px 0 64px; }
.about-content {
  max-width: 640px;
  margin: 0 auto;
  padding: 0 16px;
}
.about-content h1 {
  font-family: "Cormorant Garamond", serif;
  font-size: clamp(1.8rem, 4vw, 2.6rem);
  line-height: 1.15;
  margin: 4px 0 16px;
  color: var(--text);
}
.about-content h2 {
  font-family: "Cormorant Garamond", serif;
  font-size: clamp(1.2rem, 2vw, 1.5rem);
  margin: 32px 0 8px;
  color: var(--text);
}
.about-content p {
  font-size: 1rem;
  line-height: 1.65;
  color: var(--text);
  margin: 0 0 14px;
}
.about-content .about-lede {
  font-size: 1.08rem;
  line-height: 1.6;
  color: var(--text);
  margin-bottom: 24px;
}
.about-content a {
  color: var(--accent-dark);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.about-content a:hover { color: var(--accent); }
.about-content .about-footer-cta {
  margin-top: 32px;
  display: flex;
  justify-content: flex-start;
}
.about-content .about-footer-cta a { text-decoration: none; }
.search-input input {
  width: 100%;
  min-width: 0;
  border: 0;
  background: transparent;
  outline: none;
  font-size: 0.95rem;
}
.select-input select {
  width: 100%;
  min-width: 0;
  max-width: 100%;
  border: 0;
  background: transparent;
  outline: none;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--text);
  padding-right: 8px;
  /* Truncate long book titles in the closed select so a single long option
     can't widen the parent past the viewport on phones. */
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}

.quote-note {
  margin: 0 0 14px;
  padding: 10px 14px;
  background: rgba(85, 66, 43, 0.05);
  border-radius: 12px;
  font-size: 0.95rem;
  color: var(--muted);
  font-style: italic;
}

/* Quote-card colors per highlight color */
.quote-card[data-color="sage"]  { border-left-color: #8d9b77; }
.quote-card[data-color="rose"]  { border-left-color: #c98072; }

/* ---------- Tags on book cards ---------- */
.book-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 4px 0 10px;
  align-items: center;
}
.tag-chip, .tag-add {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  border: 1px solid var(--line);
  background: var(--accent-soft);
  color: var(--accent-dark);
  cursor: pointer;
  min-height: 28px;
}
.tag-chip { padding-right: 8px; }
.tag-chip:hover { background: rgba(186, 125, 74, 0.28); }

/* Removable variant: a wrapper with two click targets — clicking the label
   filters the library by that tag, clicking × removes it from the book. */
.tag-chip--removable {
  padding: 0;
  cursor: default;
  overflow: hidden;
}
.tag-chip--removable:hover { background: var(--accent-soft); }
.tag-chip-label {
  display: inline-flex;
  align-items: center;
  padding: 4px 6px 4px 10px;
  color: var(--accent-dark);
  text-decoration: none;
  align-self: stretch;
}
.tag-chip-label:hover { background: rgba(186, 125, 74, 0.28); }
.tag-chip-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 9px;
  font-size: 1rem;
  line-height: 1;
  background: transparent;
  border: 0;
  color: var(--accent-dark);
  cursor: pointer;
  opacity: 0.55;
  align-self: stretch;
}
.tag-chip-remove:hover { opacity: 1; background: rgba(186, 125, 74, 0.32); }
.tag-add {
  background: transparent;
  color: var(--muted);
  border-style: dashed;
}
.tag-add:hover { color: var(--text); }
.tag-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  font-size: 0.85rem;
  line-height: 1;
  opacity: 0.6;
}
.tag-cloud {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.tag-cloud .tag-chip {
  background: rgba(255, 251, 246, 0.78);
  color: var(--text);
  text-decoration: none;
}
.tag-count {
  margin-left: 4px;
  color: var(--muted);
  font-weight: 500;
}

/* Reading-time meta line on book cards */
.book-meta {
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0;
}

/* ---------- Stats grid ----------
   Mobile-first: 2-column grid with tighter padding. Desktop: auto-fill
   with 220px min for fluid 3-4 column layouts. */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.stat-card {
  padding: 14px;
  border-radius: var(--m-radius-card);
  border: 1px solid var(--line);
  background: rgba(255, 251, 246, 0.86);
  box-shadow: var(--shadow-soft);
  display: grid;
  gap: 4px;
}
.stat-card-wide { grid-column: 1 / -1; }
.stat-label {
  margin: 0;
  color: var(--accent-dark);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.stat-value {
  margin: 0;
  font-family: "Cormorant Garamond", Georgia, serif;
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--text);
}
.stat-sub { margin: 0; color: var(--muted); font-size: 0.92rem; }
@media (min-width: 768px) {
  .stats-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
  .stat-card { padding: 22px 24px; border-radius: var(--radius-md); }
  .stat-card-wide { grid-column: span 2; }
  .stat-value { font-size: clamp(2rem, 4vw, 2.6rem); }
}

/* ---------- Popover color row ---------- */
.popover-colors {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 6px 0 4px;
  margin-left: 6px;
  border-left: 1px solid rgba(255, 248, 240, 0.18);
}
.popover-colors .swatch {
  width: 22px;
  height: 22px;
  min-height: 0; /* override .selection-popover button min-height */
  border-radius: 50%;
  border: 1.5px solid rgba(255, 248, 240, 0.4);
  cursor: pointer;
  padding: 0;
  background: var(--swatch-bg);
  flex: 0 0 auto;
}
.popover-colors .swatch-amber { --swatch-bg: #d6ad68; }
.popover-colors .swatch-sage  { --swatch-bg: #8d9b77; }
.popover-colors .swatch-rose  { --swatch-bg: #c98072; }
.popover-colors .swatch[aria-pressed="true"] {
  border-color: #fff8f0;
  box-shadow: 0 0 0 2px rgba(255, 248, 240, 0.5);
}

/* ---------- In-book search match highlight (markdown reader) ---------- */
.markdown-reader .search-hit {
  background: rgba(214, 173, 104, 0.5);
  border-radius: 3px;
  padding: 0 2px;
}

/* ---------- Settings page hero ---------- */
.settings-hero { display: grid; gap: 8px; max-width: 720px; margin-bottom: 8px; }

/* Visually hidden helper */
.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;
}

/* =========================================================================
   PHASE-2 STATUS — all components migrated mobile-first
   -------------------------------------------------------------------------
   Every primitive and component now uses mobile-first authoring (default
   styles target the phone viewport, desktop additions live in
   `@media (min-width: 768px)` blocks alongside each component).

   The single remaining viewport-specific media query is for **truly narrow
   phones** (≤420px) where some grids and tap targets need a final tighten.
   This is genuinely viewport-specific — a phone fundamentally has less
   horizontal room — so the max-width query is appropriate here, not a
   patching pattern.
   ========================================================================= */

/* Phone-narrow refinements (≤420px) — smaller iPhones, narrow Androids. */
@media (max-width: 420px) {
  :root { --m-pad-page: 12px; }
  .topbar-actions .button { padding: 6px 10px; font-size: 0.82rem; }
  .dashboard-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
  .stats-grid { grid-template-columns: 1fr; }
  .stat-card-wide { grid-column: auto; }
}
