/* App shell styles. The kit's player.css is loaded first and supplies the player
 * chrome plus the design tokens; everything here either overrides a token or
 * styles app-only furniture (tabs, screens, lists, forms).
 */

:root {
  /* Room for the bottom tab bar so the docked mini-player sits above it.
   * Best-effort static guess for the instant before first paint — app.js
   * measures .tabs' real rendered height and overrides both with an inline
   * property the moment it can, so this only matters for a single frame (and
   * a no-JS fallback). It drifted from the tab bar's actual height once
   * already (was 64px, tabs render at ~53px), which is exactly why the real
   * value now comes from measurement instead of a second hand-maintained
   * number — see syncTabbarHeight() in app.js. */
  /* What a label reads as when it sits ON the accent — a filled amber button,
   * an is-on chip, today's date in the calendar.
   *
   * Theme-independent on purpose, and that is the whole point: these sites
   * used --color-bg, which means "the opposite of the text colour" and gets
   * it right only in dark mode. In light mode --color-bg is cream, and cream
   * on amber is 1.88:1 — unreadable, on the controls every screen makes the
   * most prominent thing on it. The accent is the SAME amber in both themes,
   * so what reads on it is the same dark in both themes. */
  --color-on-primary: #12161f;

  /* The bottom of the spacing scale. Used in nineteen places and never
   * defined, which does not mean "fall back to nothing" — an undefined
   * custom property makes the whole declaration invalid, so every one of
   * those paddings and gaps computed to zero with no rule to blame. The
   * rest of the scale is 2:.5rem 3:.75rem 4:1rem 5:1.25rem 6:1.5rem. */
  --space-1: 0.25rem;

  --pk-dock-bottom: 54px;
  --tabbar-h: 54px;
  --topbar-h: 61px;
  --maxw: 44rem;

  /* Named z-index layers — ascending stack order. Use these instead of
   * raw numbers so the layering is self-documenting and conflict-free. */
  --z-base: 0; /* player panels, feed card backgrounds */
  --z-content: 1; /* card content, expand buttons */
  --z-overlay: 2; /* feed card stamps, drag handles */
  --z-control: 5; /* setting toggles */
  --z-dropdown: 10; /* dropdowns, inline controls */
  --z-player-ctrl: 30; /* player overlay controls */
  --z-pull-refresh: 35; /* under topbar */
  --z-topbar: 40; /* top bar */
  --z-sidebar: 60; /* sidebar / desktop nav */
  --z-tabs: 70; /* bottom tab bar */
  --z-player-bar: 90; /* docked mini-player */
  --z-sheet: 92; /* track detail sheet, guide overlay */
  --z-modal: 100; /* modal backdrop */
  --z-sw-toast: 121; /* service worker update toast */
  --z-skip-link: 200; /* accessibility skip link */
  --z-overlay-sheet: 1000; /* full-screen sheets */
  --z-tts-mini: 1002; /* TTS mini-player dock, above --z-overlay-sheet */
  /* The unified player shell hardcodes 1005 (it lives in the kit, which can't
     reach these tokens — see kit/ui/player-shell.css). A video parked in its
     stage sits just above it: the shell's chrome is arranged around the stage,
     never over it. */
  --z-player-shell-video: 1006;
  --z-player-sheet: 1100; /* player action sheets */
  --z-player-sheet-high: 1150; /* player sheets (higher) */
  --z-toast: 1200; /* toast notifications (top of stack) */
  --btn-radius: 10px;

  /* Two families: Space Grotesk for titles/screen names/brand-adjacent
   * numerals (overrides the kit's --font-display, which every existing
   * title/numeral rule in kit/ui/player.css already keys off — one token
   * swap re-themes all of them without touching that file); Geist for
   * everything else. Self-hosted so the service worker gets offline parity;
   * a Google Fonts <link> would leave first paint dependent on the network. */
  --font-display: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-body: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  font-family: var(--font-body);
  color-scheme: light dark;
}

@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('fonts/geist.woff2') format('woff2');
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('fonts/space-grotesk.woff2') format('woff2');
}

/* ── Theme ────────────────────────────────────────────────────────────────
 *
 * Dark is the default, not merely an option: this is an app for listening and
 * reading, often at night, and a bright screen is the wrong default for that.
 *
 * The palette lives under [data-theme='dark'] rather than only inside a
 * prefers-color-scheme query, because the user's choice has to be able to
 * override the system. index.html stamps data-theme on <html> BEFORE first paint
 * — without that there is a white flash on every load, which is exactly what a
 * dark-by-default app must not do.
 *
 * The media query is kept as the fallback for "follow system", which is stored as
 * no data-theme at all.
 */

:root[data-theme='dark'] {
  /* Without this, an explicit choice against the OS preference (dark theme
   * on a light-mode OS) still leaves native controls — scrollbars, form
   * fields, the date picker — rendered in the OS's scheme, since the base
   * :root's color-scheme: light dark defers to it. */
  color-scheme: dark;
  --color-bg: #08090d;
  --color-surface: #0e1016;
  --color-surface-2: #14171e;
  --color-surface-offset: #22262f;
  --color-border: #21242e;
  --color-border-strong: #262a34;
  --color-divider: #14161c;
  --color-text: #edeff4;
  --color-text-2: #c8cdd8;
  --color-text-3: #a7aebc;
  --color-text-faint: #8a91a0;
  --color-text-dim: #7f8797;
  --color-text-dimmer: #79818f;
  --color-primary: #f2a113;
  --color-primary-text: #f2a113;
  --color-primary-highlight: #f2a11314;
  --color-navy: #1f274c;
  --color-secondary: #9fb6ee;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    color-scheme: dark;
    --color-bg: #08090d;
    --color-surface: #0e1016;
    --color-surface-2: #14171e;
    --color-surface-offset: #22262f;
    --color-border: #21242e;
    --color-border-strong: #262a34;
    --color-divider: #14161c;
    --color-text: #edeff4;
    --color-text-2: #c8cdd8;
    --color-text-3: #a7aebc;
    --color-text-faint: #8a91a0;
    --color-text-dim: #7f8797;
    --color-text-dimmer: #79818f;
    --color-primary: #f2a113;
    --color-primary-text: #f2a113;
    --color-primary-highlight: #f2a11314;
    --color-navy: #1f274c;
    --color-secondary: #9fb6ee;
  }
}

/* Real Day theme (README "Colour — Day"): navy is the primary control fill
 * here and amber is the accent/progress colour — not a filter inversion of
 * dark. Previously light mode fell through to the kit's own cream/gold
 * defaults (kit/ui/player.css :root); this is the first explicit override.
 * --color-primary-text exists because a filled amber button on paper fails
 * contrast (README) — text/icon uses of amber should read this token, fills
 * and progress bars keep reading --color-primary. */
:root[data-theme='light'] {
  color-scheme: light;
  --color-bg: #f4f1ea;
  --color-surface: #fbf9f3;
  --color-surface-2: #ffffff;
  --color-surface-offset: #ede9e2;
  --color-border: #e4decf;
  --color-border-strong: #d9d1be;
  --color-divider: #e9e3d6;
  --color-text: #1f274c;
  --color-text-2: #3a4161;
  --color-text-3: #63697e;
  --color-text-faint: #5f6167;
  --color-text-dim: #67634f;
  --color-text-dimmer: #6f6b5b;
  --color-primary: #f2a113;
  --color-primary-text: #7f6100;
  --color-primary-highlight: #f2a11314;
  --color-navy: #1f274c;
  --color-secondary: #2c3e6b;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) {
    color-scheme: light;
    --color-bg: #f4f1ea;
    --color-surface: #fbf9f3;
    --color-surface-2: #ffffff;
    --color-surface-offset: #ede9e2;
    --color-border: #e4decf;
    --color-border-strong: #d9d1be;
    --color-divider: #e9e3d6;
    --color-text: #1f274c;
    --color-text-2: #3a4161;
    --color-text-3: #63697e;
    --color-text-faint: #5f6167;
    --color-text-dim: #67634f;
    --color-text-dimmer: #6f6b5b;
    --color-primary: #f2a113;
    --color-primary-text: #7f6100;
    --color-primary-highlight: #f2a11314;
    --color-navy: #1f274c;
    --color-secondary: #2c3e6b;
  }
}

* {
  box-sizing: border-box;
}

/* Browser tabs: `manipulation` allows panning and pinch-zoom while disabling
 * double-tap-zoom — the browser-chrome-y side effect this rule exists to
 * suppress. `pan-y` was wrong: it blocks pinch-zoom per the CSS Touch-Action
 * spec, defeating #443's goal of keeping zoom accessible in browser tabs
 * (WCAG SC 1.4.4). overscroll-behavior still kills the rubber-band. */
html,
body {
  overscroll-behavior-y: none;
  touch-action: manipulation;
}
/* Installed as a standalone PWA: lock touch-action to pan-x pan-y to
 * suppress pinch-zoom gestures at the CSS level. The viewport meta
 * swap in app.html handles iOS Safari (which ignores touch-action for
 * zoom); this reinforces it on Android Chrome. */
@media (display-mode: standalone) {
  html,
  body {
    touch-action: pan-x pan-y;
  }
}

/* When the kit docks a player above the tab bar, reserve room for it so the last
 * lines of a chapter are not hidden underneath. :has() keeps this out of JS; a
 * browser without it just overlaps slightly. */
body:has(.tts-mini:not([hidden])),
body:has(.hp-mini:not([hidden])),
body:has(.snd-mini:not([hidden])) {
  padding-bottom: calc(var(--tabbar-h) + 58px + env(safe-area-inset-bottom, 0px));
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  -webkit-font-smoothing: antialiased;
  padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
}

.sr-only,
.skip-link:not(:focus) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.skip-link:focus {
  position: fixed;
  top: var(--space-2);
  left: var(--space-2);
  z-index: var(--z-skip-link);
  background: var(--color-surface);
  color: var(--color-text);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
}

/* ── Top bar ─────────────────────────────────────────────────────────────── */

.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-topbar);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: calc(env(safe-area-inset-top, 0px) + var(--space-3)) var(--space-4) var(--space-3);
  background: color-mix(in srgb, var(--color-bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--color-divider);
}

/* ── Pull-to-refresh ─────────────────────────────────────────────────────── */
/* overscroll-behavior-y: none above kills the native bounce this would
 * otherwise ride on, so the indicator and its motion are driven entirely by
 * --pull, a unitless px-ish value app/js/pull-refresh.js sets on drag. */

.pull-refresh {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 8px);
  left: 50%;
  z-index: var(--z-pull-refresh); /* under the topbar (40) — reads as sliding out from beneath it */
  width: 34px;
  height: 34px;
  margin-left: -17px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
  opacity: 0;
  transform: translateY(-36px) scale(0.7);
  pointer-events: none;
  transition:
    transform 0.15s ease-out,
    opacity 0.15s ease-out;
}
.pull-refresh.is-visible {
  opacity: calc(var(--pull, 0) / 64);
  transform: translateY(calc(var(--pull, 0) * 1px - 8px))
    scale(calc(0.7 + 0.3 * min(1, var(--pull, 0) / 64)));
}
.pull-refresh.is-loading {
  opacity: 1;
  transform: translateY(28px) scale(1);
}
.pull-refresh-spin {
  width: 16px;
  height: 16px;
  border: 2px solid var(--color-border);
  border-top-color: var(--color-text-faint);
  border-radius: 50%;
}
.pull-refresh.is-ready .pull-refresh-spin {
  border-top-color: var(--color-primary);
}
.pull-refresh.is-loading .pull-refresh-spin {
  animation: pull-refresh-spin 0.7s linear infinite;
}
@keyframes pull-refresh-spin {
  to {
    transform: rotate(360deg);
  }
}
.brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
/* The full lockup, not a mark beside a text label: it carries the name itself,
 * so the wordmark IS the brand block here.
 *
 * Two cuts, not one file for both themes: the Night cut is solid white
 * letterforms (the amber outline that separates the Day navy cut from a
 * light ground would only muddy the edge on near-black), and the two are
 * re-stroked per render size so the junction gap between the headband and
 * the earcups survives — the design package's "UI" cut, tuned for 22-34px,
 * is what loads here. Below 22px the gap closes regardless of stroke
 * tuning, so the lockup stops being legible. If a smaller placement ever
 * appears, use the mark-alone art from docs/logos/ rather than shrinking
 * this one further. */
.brand-logo {
  width: auto;
  height: 26px;
  aspect-ratio: 1326 / 571;
  flex: none;
  background: url('logo-lockup-ui-dark.svg') left center / contain no-repeat;
}
:root[data-theme='light'] .brand-logo {
  background-image: url('logo-lockup-ui.svg');
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) .brand-logo {
    background-image: url('logo-lockup-ui.svg');
  }
}
/* Persona chip — margin-left: auto so it (and .topbar-actions after it) sit
 * pinned to the right regardless of how wide .brand renders. */
.persona-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: auto;
  padding: 6px 10px 6px 8px;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-full);
  color: var(--color-text);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 500;
  cursor: pointer;
}
.persona-chip:hover {
  border-color: color-mix(in srgb, var(--color-border-strong) 60%, var(--color-text-faint));
}
.persona-chip-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  background: var(--color-text-dimmer, var(--color-text-faint));
}
.persona-chip-name {
  max-width: 9rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.persona-chip svg {
  color: var(--color-text-faint);
  flex: none;
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.topbar-btn[hidden] {
  display: none;
}
.topbar-btn {
  background: none;
  border: 0;
  color: var(--color-text-faint);
  padding: var(--space-2);
  border-radius: var(--radius-md);
  cursor: pointer;
  min-height: 44px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.topbar-btn:hover {
  color: var(--color-text);
  background: var(--color-surface-2);
}

/* ── Screens ─────────────────────────────────────────────────────────────── */

main {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--space-4);
}
.screen[hidden] {
  display: none;
}
.screen-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0 0 var(--space-4);
  /* Belt-and-suspenders: .screen-head is meant to stay put while only
   * .screen-scroll (its sibling) scrolls — the .screen/.screen-head/
   * .screen-scroll convention documented above <main> in app.html, backed by
   * body:has(.screen...) { overflow: hidden } further down this file. If
   * that containment is ever defeated for any reason (a screen with extra
   * fixed-position children, a WebView quirk, anything), plain in-flow
   * content has nothing stopping it from scrolling up and disappearing
   * behind the sticky topbar — reported on the Reader screen's own head
   * (back button, cover, title/author), which sits well below it normally.
   * Sticky here means it always stays visible right under the topbar
   * regardless of which element ends up actually scrolling.
   *
   * top is 0, not --topbar-h: .screen-head's sticky containing block is
   * .screen (the nearest ancestor with overflow set, which is what
   * establishes its scrollport), and .screen already renders below the
   * topbar — it's <main>'s sibling-after-topbar, not stacked underneath it.
   * Offsetting by --topbar-h here double-counts the topbar height, pushing
   * .screen-head down out of sync with .screen-scroll (whose flex position
   * is computed from .screen-head's un-offset flow position), which tore
   * the two apart — visible as a gap above .screen-head and .screen-scroll's
   * content overlapping it (seen on Settings, whose jump-nav chips sit at
   * the very top of .screen-scroll). */
  position: sticky;
  top: 0;
  z-index: var(--z-control);
  background: var(--color-bg);
}
.screen-head h1 {
  font-family: var(--font-display);
  font-size: 1.25rem; /* 20px */
  line-height: 1.2;
  font-weight: 500;
  letter-spacing: -0.01em;
  margin: 0;
  flex: 1;
  min-width: 0;
}
/* Keep the back control on the same row as the title — a long title plus a
 * separate back sibling used to wrap, leaving the button alone on a clipped
 * line above the heading (notably on source/reader detail). */
.screen-head-title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  flex: 1;
}
.screen-head-title .back {
  flex: none;
}
.screen-head-title h1 {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.screen-head-text {
  min-width: 0;
  flex: 1;
}
.screen-head-text h1 {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#reader-cover {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  overflow: hidden;
}
#reader-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
#reader-chapter-line {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Forces the title block onto its own full-width row within .screen-head
 * (flex-wrap: wrap), so .reader-mode-toggle — a sibling, not a child, of
 * .screen-head-title — wraps to a second row sized to its own content
 * instead of squeezed into the title's row. Scoped to #s-reader alone:
 * .screen-head-title is shared by every screen's header, and widening its
 * base rule would touch headers that carry no second row at all. */
#s-reader .screen-head-title {
  flex-basis: 100%;
}
/* §17: Read is where you already are — there's no separate "read mode" to
 * switch into, so it renders pressed-and-inert while Listen hands off to
 * the same listenFromChapter() the in-body Listen button calls. */
.reader-mode-toggle {
  flex: none;
  display: inline-flex;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  overflow: hidden;
}
.reader-mode-btn {
  padding: 6px 14px;
  font-size: 0.78rem;
  font-weight: 600;
  background: none;
  border: 0;
  color: var(--color-text-faint);
  cursor: pointer;
}
.reader-mode-btn.is-active {
  background: var(--color-primary);
  color: var(--color-on-primary);
  cursor: default;
  /* The global `button:disabled { opacity: 0.45 }` would otherwise wash out
   * the pressed segment — Read is disabled to make it inert (there's no
   * separate mode to switch into), not to show as unavailable, and those
   * read as the same thing at 45% opacity. Two classes here already
   * outrank button:disabled's specificity, so this wins regardless of
   * source order. */
  opacity: 1;
}
.reader-mode-btn:not(.is-active):hover {
  color: var(--color-text);
}
/* A route change moves focus to the new screen's heading so screen readers
 * announce it. That heading carries tabindex="-1", so it is never reachable by
 * Tab — suppressing its focus ring costs keyboard users nothing, and avoids a
 * box that reads as an error appearing on every navigation. */
[tabindex='-1']:focus {
  outline: none;
}
.section-head {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--color-text-faint);
  margin: var(--space-5) 0 var(--space-2);
}
.section-head:empty {
  display: none;
}
.section-head-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin: var(--space-5) 0 var(--space-2);
}
.section-head-row .section-head {
  margin: 0;
}
.source-open-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  flex: 1;
}
.source-open-head .back {
  flex: none;
}
.source-open-head .section-head {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#source-open-art {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  object-fit: cover;
}
/* Needs an ID selector to win: .source-art's shared `display: grid` (for the
 * icon-fallback rows) otherwise beats the `hidden` attribute's default
 * `display: none` — a single class has the same specificity as `[hidden]`,
 * so the later-declared rule (.source-art) would apply regardless of the
 * attribute. */
#source-open-art[hidden] {
  display: none;
}
#source-close {
  flex: none;
  background: none;
  border: 0;
  color: var(--color-text-faint);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
}
#source-close:hover {
  color: var(--color-primary-text);
  background: var(--color-surface-2);
}

/* ── Podcasting 2.0 feature tags (show detail screen) ───────────────────── */

.source-features {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.source-features[hidden] {
  display: none;
}
.source-features-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-faint);
  font-weight: 600;
  width: 100%;
  margin-bottom: -0.2rem;
}
.feature-tag {
  display: inline-flex;
  align-items: center;
  padding: 0.22em 0.65em;
  border-radius: var(--radius-full);
  background: var(--color-primary-highlight);
  border: 1px solid var(--color-primary);
  color: var(--color-primary-text);
  font-size: 0.74rem;
  font-weight: 500;
  white-space: nowrap;
}
.source-skip {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  font-size: 0.85rem;
}
.source-skip[hidden] {
  display: none;
}
.source-skip .skip-label {
  color: var(--color-text-faint);
  font-weight: 600;
}
.source-skip label {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
}
.source-skip input {
  width: 3em;
  padding: 0.2em 0.4em;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.85rem;
}
.source-skip .skip-toggle {
  margin-left: 0.25em;
}
.source-skip .skip-toggle input {
  width: auto;
}
/* Live podcast badge + notification toggle */
.source-live {
  margin-bottom: var(--space-2);
}
.source-live[hidden] {
  display: none;
}
.source-live-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.5em 0.75em;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  margin-bottom: 0.4em;
}
.live-badge {
  background: #e33;
  color: #fff;
  font-size: 0.7rem;
  font-weight: 700;
  padding: 0.15em 0.5em;
  border-radius: var(--btn-radius);
  letter-spacing: 0.02em;
  flex-shrink: 0;
}
/* A scheduled-but-not-started liveItem. Deliberately not red — red is the
 * "playing right now" signal everywhere else in the app. */
.live-badge-pending {
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  border: 1px solid var(--color-border);
}
.live-title {
  flex: 1;
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.live-notify-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  font-size: 0.85rem;
  cursor: pointer;
  margin-left: 0.5em;
}
.live-notify-toggle input {
  width: auto;
}
.source-lang-filter {
  margin-bottom: var(--space-2);
}
.source-lang-filter select {
  padding: 0.3em 0.5em;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.85rem;
}
.reader-chapter-translate {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0.4em 0.8em;
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  background: transparent;
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: pointer;
}
.source-remind {
  margin-bottom: var(--space-2);
}
.source-remind[hidden] {
  display: none;
}
.source-remind button {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0.4em 0.8em;
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  background: transparent;
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: pointer;
}
.source-detail {
  margin-bottom: var(--space-2);
}
.source-detail[hidden] {
  display: none;
}
.source-detail-meta {
  font-size: 0.85rem;
  color: var(--color-text-faint);
  margin-bottom: var(--space-2);
}
.source-detail-meta a {
  color: var(--color-primary-text);
}
/* A credited author/publisher name, tappable through to the Author overview
 * screen (all other shows/books/feeds by them) — styled the same as the
 * Feed/Website <a> links right next to it in this same meta line, since it's
 * functionally the same thing: a link out of this row, just to another
 * screen in the app instead of off it. */
/* A person's name is a link into their creator page, and it has to LOOK like
 * one at rest rather than only on hover (§8). A creator name must never be
 * visually indistinguishable from a publish date sitting beside it in the
 * same meta line — which is exactly what an undecorated coloured span is on
 * a screen where several other things are also amber. Same treatment as the
 * player's source line. */
.author-link,
.creator-link {
  font: inherit;
  color: var(--color-primary-text, var(--color-primary));
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: #f2a11359;
  text-underline-offset: 3px;
}
.author-link:hover,
.author-link:focus-visible,
.creator-link:hover,
.creator-link:focus-visible {
  text-decoration-color: currentColor;
}
.source-detail-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.source-detail-actions button {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0.4em 0.8em;
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  background: transparent;
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: pointer;
}
.source-detail-actions button:hover {
  background: var(--color-surface-2);
}
.source-detail-unfollow {
  color: var(--color-danger, #c53030) !important;
  border-color: var(--color-danger, #c53030) !important;
}
.source-detail-feed-toggle {
  display: flex;
  align-items: center;
  gap: 0.4em;
  margin-top: var(--space-3);
  font-size: 0.85rem;
  cursor: pointer;
}
.source-detail-feed-toggle input {
  width: 16px;
  height: 16px;
  accent-color: var(--color-primary);
}
.source-detail-tags {
  margin-bottom: var(--space-2);
}
.batch-tag-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
.batch-tag-bar input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.9rem;
  padding: 0.5em 0.7em;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text);
}
.batch-count {
  font-size: 0.85rem;
  color: var(--color-text-faint);
  white-space: nowrap;
}
.batch-cb {
  flex: none;
  width: 20px;
  height: 20px;
  margin-right: var(--space-2);
  cursor: pointer;
  accent-color: var(--color-primary);
}
.source-row.is-selected,
.browse-card.is-selected {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}
.browse-card .batch-check {
  position: absolute;
  top: var(--space-1);
  right: var(--space-1);
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--color-primary);
  color: var(--color-on-primary);
  display: grid;
  place-items: center;
}
.reminder-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--color-border);
}
.reminder-info {
  font-size: 0.85rem;
}
.reminder-row button {
  padding: 0.2em;
  border: none;
  background: transparent;
  color: var(--color-text-faint);
  cursor: pointer;
}
.lock-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  z-index: var(--z-modal);
}
.lock-bar[hidden] {
  display: none;
}
.lock-bar-label {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: 0.85rem;
  color: var(--color-text-faint);
}
.lock-bar button {
  padding: 0.5em 1em;
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  background: transparent;
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: pointer;
  user-select: none;
  touch-action: none;
}
.lede {
  color: var(--color-text-faint);
  font-size: 0.92rem;
  line-height: 1.55;
  margin: 0 0 var(--space-4);
}
.muted {
  color: var(--color-text-faint);
}
.error {
  color: var(--color-error);
}
.back {
  background: var(--color-surface-2);
  border: 0;
  color: var(--color-text);
  border-radius: var(--btn-radius);
  /* The one control on a detail screen you press without looking. */
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  cursor: pointer;
  transform: rotate(90deg);
}

/* ── Buttons ─────────────────────────────────────────────────────────────── */

/* A floor for the button families that live outside the shared metrics above.
 * Each of these sets its own padding in ems, which on a 0.8–0.9rem label
 * lands between 26 and 32px tall — wide enough to see, thin enough to miss.
 * 40px rather than WCAG's AAA 44 because these sit in dense rows where 44
 * would break the rhythm; the AA floor is 24, so this is well clear of it and
 * e2e_contrast's sibling check holds the line. */
.notes-chip,
.notes-new,
.notes-actions button,
.dsc-intent-btn,
.dsc-example,
.pl-new button,
.pl-head button,
.notes-export button,
.empty button,
.screen-head button {
  min-height: 40px;
}

button {
  font: inherit;
  cursor: pointer;
}
.primary,
.row-actions button,
.row-actions a,
.inline-form button,
.stack button,
.file-btn,
#settings-body button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  background: var(--color-surface-2);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 0.55em 0.9em;
  /* An em-based padding on a 0.9rem label lands these at 31px tall, which is
   * a thin thing to hit on a phone even when it is 120px wide — WCAG's AAA
   * target is 44 and its AA floor is 24. A floor in px rather than more
   * padding, so the label keeps its proportions and only the short ones
   * grow. */
  min-height: 40px;
  transition:
    background var(--transition),
    border-color var(--transition);
}
.primary {
  background: var(--color-primary);
  border-color: var(--color-primary);
  /* Was #fff, which is 2.12:1 on this amber — the worst contrast anywhere in
   * the app, in BOTH themes, on the one control every screen makes the most
   * prominent thing on it. Amber is a light colour; the label on it has to be
   * dark. Navy is the app's own dark, and gives 6.8:1. */
  color: var(--color-on-primary);
  font-weight: 600;
}
.primary:hover {
  filter: brightness(1.06);
}
.row-actions button:hover,
.row-actions a:hover,
.inline-form button:hover,
.file-btn:hover {
  border-color: var(--color-primary);
}
.row-actions a {
  text-decoration: none;
}
button:disabled {
  opacity: 0.45;
  cursor: default;
}
.row-actions {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}
.row-actions button.is-on {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-primary); /* same amber, same problem — see .primary */
}

/* Kind colours AS TEXT.
 *
 * KIND_COLOR in app/js/row.js is a tint palette — the right thing for a dot,
 * a backdrop or a progress rail, and much too light to read as small text on
 * the light theme's cream: amber lands at 1.88:1. Same split as
 * --color-primary / --color-primary-text, for the same reason.
 *
 * Dark theme keeps the tints, which read fine on near-black. Light theme gets
 * darkened versions of the same hues, all at or above 4.5:1 on both the page
 * and the surface behind a card, so the colour still says "podcast" without
 * the label becoming decoration. */
[data-kind='podcast'],
[data-kind='audio'] {
  color: #f2a113;
}
[data-kind='text'] {
  color: #7fb2e5;
}
[data-kind='book'] {
  color: #8f7be8;
}
[data-kind='youtube'],
[data-kind='rumble'] {
  color: #e5533c;
}
[data-kind='music'],
[data-kind='radio'] {
  color: #4fbfa0;
}
[data-kind='ambient'] {
  color: #9aa3b2;
}
:root[data-theme='light'] [data-kind='podcast'],
:root[data-theme='light'] [data-kind='audio'] {
  color: #876700;
}
:root[data-theme='light'] [data-kind='text'] {
  color: #2b5580;
}
:root[data-theme='light'] [data-kind='book'] {
  color: #54409f;
}
:root[data-theme='light'] [data-kind='youtube'],
:root[data-theme='light'] [data-kind='rumble'] {
  color: #a82d1a;
}
:root[data-theme='light'] [data-kind='music'],
:root[data-theme='light'] [data-kind='radio'] {
  color: #1c6e59;
}
:root[data-theme='light'] [data-kind='ambient'] {
  color: #565d6a;
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) [data-kind='podcast'],
  :root:not([data-theme='dark']) [data-kind='audio'] {
    color: #876700;
  }
  :root:not([data-theme='dark']) [data-kind='text'] {
    color: #2b5580;
  }
  :root:not([data-theme='dark']) [data-kind='book'] {
    color: #54409f;
  }
  :root:not([data-theme='dark']) [data-kind='youtube'],
  :root:not([data-theme='dark']) [data-kind='rumble'] {
    color: #a82d1a;
  }
  :root:not([data-theme='dark']) [data-kind='music'],
  :root:not([data-theme='dark']) [data-kind='radio'] {
    color: #1c6e59;
  }
  :root:not([data-theme='dark']) [data-kind='ambient'] {
    color: #565d6a;
  }
}

/* ── Shared modes: links, and the pairing QR ─────────────────────────────── */

.shared-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-divider);
}
.shared-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.shared-row-name {
  font-weight: 600;
}
.shared-row-sub {
  font-size: 0.85rem;
  color: var(--color-text-2);
}
.shared-row-remove {
  flex: none;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--color-text-faint);
}
.shared-row-remove:hover {
  color: var(--color-primary-text);
  background: var(--color-surface-2);
}

.pair-invite {
  margin-top: var(--space-3);
}
/* The QR's own white ground is drawn inside the SVG (a transparent code on a
 * dark theme is inverted, and a camera will not read an inverted symbol), so
 * this only has to size it. Capped, because past a certain size a phone
 * camera has to be moved further away rather than closer. */
.pair-qr {
  width: min(260px, 70vw);
  margin: var(--space-3) auto;
  padding: 10px;
  background: #fff;
  border-radius: var(--radius-md);
}
.pair-qr svg {
  display: block;
  width: 100%;
  height: auto;
}
/* The typed fallback. Monospace and spaced out because this is read off one
 * screen and typed into another, which is the whole reason it is grouped in
 * fours. */
.pair-code {
  text-align: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  color: var(--color-text);
  word-break: break-all;
  margin: 0 0 var(--space-2);
}
.pair-arrival {
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
}
.pair-arrival h2 {
  margin-top: 0;
}

/* ── Forms ───────────────────────────────────────────────────────────────── */

.inline-form {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.inline-form input {
  flex: 1;
}
#source-q-clear {
  background: none;
  border: 0;
  color: var(--color-text-faint);
  padding: 0.55em;
  border-radius: var(--radius-md);
}
#source-q-clear:hover {
  color: var(--color-primary-text);
  background: var(--color-surface-2);
}
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.stack label {
  font-weight: 600;
  font-size: 0.9rem;
  margin-top: var(--space-2);
}
.stack button {
  align-self: flex-start;
  margin-top: var(--space-3);
}
/* Stays a bare tag selector on purpose. Excluding the checkbox types here
 * with :not() works — but it lifts this rule's specificity from 0,0,1 to
 * 0,2,1, which then beats every wrapper-level override in the file
 * (`.lib-search input` and friends, all 0,1,1). The Library search field
 * lost its own sizing that way and clipped its row by 2px, caught by the QA
 * sweep and not by any e2e suite. The checkbox/radio reset immediately below
 * does the same job at 0,1,1, which is under the wrappers rather than over
 * them. */
input,
select,
textarea {
  font: inherit;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 0.6em 0.75em;
  width: 100%;
}
/* The rule above is written for things you type into, and a checkbox is not
 * one of them: inherited whole, it stretched every checkbox to the width of
 * its container, which inside a flex row makes the box a full-width flex item
 * and pushes the label clean off the right edge while the glyph still draws
 * small at the left. The persona source picker rendered as a column of
 * anonymous squares with every source name off-screen — "the persona screen
 * is blank".
 *
 * The type-attribute selector (0,1,1) beats the bare tag selector (0,0,1)
 * above, and still loses to wrapper overrides like `.lib-search input`, which
 * is the property that matters and the reason this is a separate rule rather
 * than a :not() on the one above. */
input[type='checkbox'],
input[type='radio'] {
  width: 16px;
  height: 16px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  accent-color: var(--color-primary);
  cursor: pointer;
}
/* Below 16px, iOS Safari (and the installed PWA — it's the WebKit engine's
 * own behavior, not a browser-chrome thing a standalone display-mode
 * escapes) zooms the whole page in on focus, because it assumes text that
 * small must be too hard to read to type into otherwise. A dozen-plus
 * selectors across this file size inputs down for visual density in tight
 * contexts (search bars, tag chips, the omnibar) — every one of them below
 * that threshold, which is what made this "any text box, anywhere." An
 * !important floor here is deliberate, for the same reason
 * prefers-reduced-motion's is above: it has to win over every existing and
 * future component-specific size, not just the ones already known about.
 * Desktop has no such behavior to guard against and keeps its tighter,
 * intentionally-sized fields. */
:root:not([data-layout='desktop']) input,
:root:not([data-layout='desktop']) select,
:root:not([data-layout='desktop']) textarea {
  font-size: 16px !important;
}
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 1px;
}
button:focus-visible,
a:focus-visible,
.tab:focus-visible,
.topbar-btn:focus-visible,
.back:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
textarea {
  resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85rem;
}
.hint {
  color: var(--color-text-faint);
  font-size: 0.82rem;
  margin: 0.15rem 0 0.6rem;
  line-height: 1.5;
}

/* Links were never styled, so every one of them rendered as default browser
   blue-and-underlined on a dark surface — legible, but obviously unstyled. */
main a {
  color: var(--color-primary-text);
  text-decoration-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
  text-underline-offset: 0.15em;
}
main a:hover {
  text-decoration-color: currentColor;
}
.file-btn {
  align-self: flex-start;
}
.alt-import {
  margin-top: var(--space-5);
  border-top: 1px solid var(--color-divider);
  padding-top: var(--space-3);
}
.alt-import summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.92rem;
}
.alt-import[open] summary {
  margin-bottom: var(--space-3);
}

/* §16: one column of cards. This wraps the Import screen's EXISTING
 * sections — search, browse, and the three alt-method disclosures — rather
 * than restyling what's inside them: .browse-chip/.browse-card/.found-*
 * etc are shared with Discover's podcast-directory shelf, and reskinning
 * them here would silently reskin Discover too. `.import-card` is additive
 * on the three <details>, layered alongside `.alt-import` (kept for its
 * existing collapse styling and the scroll-into-view-on-open behavior
 * wired to that class in app.js) rather than replacing it. */
.import-card {
  margin-bottom: var(--space-4);
  padding: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}
.import-card-title {
  display: block;
  margin: 0 0 0.3rem;
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text);
}
.import-card-hint {
  margin: 0 0 var(--space-3);
  color: var(--color-text-faint);
  font-size: 0.85rem;
  line-height: 1.5;
}
/* .import-card-details layers onto .alt-import's own <details> styling
 * (border-top + padding, above) rather than replacing it — this just adds
 * the card surface and moves the disclosure's title/hint into the summary
 * row so the one-line explanation is visible before the card is opened. */
.import-card-details {
  margin-top: 0;
  border-top: 0;
  padding-top: 0;
  cursor: default;
}
.import-card-details summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.import-card-details summary::-webkit-details-marker {
  display: none;
}
.import-card-details .import-card-title::before {
  content: '+';
  display: inline-block;
  width: 1em;
  color: var(--color-primary-text);
}
.import-card-details[open] .import-card-title::before {
  content: '\2013';
}
.import-card-details .import-card-hint {
  margin: 0 0 0 1em;
}
.import-card-details[open] summary {
  margin-bottom: var(--space-3);
}
.import-card-details form,
.import-card-details > .hint,
.import-card-details .file-btn {
  margin-left: 1em;
}

.import-status:empty {
  display: none;
}
.import-status {
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  border-left: 3px solid var(--color-border);
  font-size: 0.92rem;
  line-height: 1.55;
}
.import-status.is-ok {
  border-left-color: var(--color-primary);
}
.import-status.is-error {
  border-left-color: var(--color-error);
}
.import-warn {
  margin-top: var(--space-2);
  color: var(--color-text-faint);
  font-size: 0.85rem;
}
.import-next {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  flex-wrap: wrap;
}

/* ── Lists ───────────────────────────────────────────────────────────────── */

.list {
  display: flex;
  flex-direction: column;
}
.pl-list-actions {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.pl-list-actions button {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-faint);
  border-radius: var(--radius-md);
  padding: 0.4rem 0.8rem;
  font-size: 0.9rem;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
}
.book-row,
.source-row,
.pl-row,
.pl-entry,
.item-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-divider);
}
.book-row,
.source-row {
  cursor: pointer;
}
.book-row:hover,
.source-row:hover {
  background: var(--color-surface-2);
}
/* .item-detail's flex-basis:100% (below) only breaks onto its own line when
 * the row can wrap — without this the description sits squeezed to the right
 * of the art/actions instead of stacking underneath them. Scoped to .item-row
 * alone: the other rows above share the base rule but have no such child. */
.item-row {
  flex-wrap: wrap;
}
.book-art,
.source-art,
.item-art,
.pl-kinds,
.pl-art,
.pl-entry-icon {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  color: var(--color-primary-text);
  display: grid;
  place-items: center;
  object-fit: cover;
}
.pl-kinds {
  gap: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  padding: 3px;
}
.pl-entry-icon {
  width: 32px;
  height: 32px;
}
.book-meta,
.source-meta,
.pl-meta,
.pl-entry-meta,
.item-meta {
  flex: 1;
  min-width: 0;
}
.book-meta h3,
.source-meta h3,
.pl-meta h3 {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The row itself opens on tap for mouse/touch, but an <article> isn't
 * natively focusable or keyboard-operable — this real <button> around the
 * title is the row's actual keyboard/screen-reader entry point to the same
 * detail view. `all: unset` strips button chrome so it reads as plain text,
 * inheriting the h3's size/weight rather than fighting it. */
.row-title-btn {
  all: unset;
  cursor: pointer;
}
.row-title-btn:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.book-sub,
.source-sub,
.pl-sub,
.book-facts,
.pl-entry-sub,
.item-sub {
  color: var(--color-text-faint);
  font-size: 0.82rem;
  margin-top: 0.15rem;
}
.pl-tag {
  display: inline-block;
  vertical-align: 0.08em;
  margin-left: 0.4em;
  padding: 0.05em 0.45em;
  border-radius: 999px;
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* ── Source tags (Library "Following" list) ─────────────────────────────── */

.source-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin-top: var(--space-2);
}
.source-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0.15em 0.15em 0.15em 0.6em;
  border-radius: var(--radius-full);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  font-size: 0.76rem;
  color: var(--color-text);
}
.source-tag button {
  display: inline-flex;
  padding: 0.25em;
  color: var(--color-text-faint);
}
.source-tag-add {
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
  padding: 0.2em 0.55em;
  border-radius: var(--btn-radius);
  border: 1px dashed var(--color-border);
  background: none;
  color: var(--color-text-faint);
  font-size: 0.76rem;
}
.source-tag-form {
  flex-basis: 100%;
}
.source-tag-form[hidden] {
  display: none;
}
.source-tag-form input {
  width: 100%;
  font: inherit;
  font-size: 0.85rem;
  padding: 0.4em 0.6em;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-text);
}

/* The filter chip row above the Following list — same visual language as the
 * theme/layout option pills in Settings. A single scrolling row (most-used
 * tag first, from Tags.all()'s sort) rather than wrapping, so it stays a
 * fixed-height strip regardless of how many tags exist. */
.source-tag-filter {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-bottom: 2px; /* keeps the focus ring from being clipped by overflow */
}
.source-tag-filter::-webkit-scrollbar {
  display: none;
}
.source-tag-chip {
  flex: 0 0 auto;
  white-space: nowrap;
  padding: 0.4em 0.9em;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-text);
  font-size: 0.82rem;
}
.source-tag-chip.is-on {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
  font-weight: 600;
}

/* How many items a standing entry contributes. A number field rather than a
 * slider: the useful values are 1–5 and typing one is faster than aiming. */
.pl-depth {
  display: flex;
  align-items: center;
  gap: 0.35em;
  margin-top: 0.3rem;
  color: var(--color-text-faint);
  font-size: 0.78rem;
}
.pl-depth input {
  width: 4.2em;
  padding: 0.15rem 0.35rem;
  font-size: 0.78rem;
}

/* The Daily's own panel, above its entries. */
.daily-panel {
  margin: 0 0 0.75rem;
  padding: 0.75rem 0.85rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}
.daily-panel .hint {
  margin: 0 0 0.6rem;
}
.daily-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}
.daily-chapters {
  display: flex;
  align-items: center;
  gap: 0.45em;
  font-size: 0.85rem;
}
.daily-chapters input {
  width: 4.5em;
  padding: 0.25rem 0.4rem;
}

.pl-warn {
  color: var(--color-primary-text);
  font-size: 0.78rem;
  margin-top: 0.2rem;
  display: flex;
  align-items: center;
  gap: 0.3em;
}
.pl-built {
  color: var(--color-text-faint);
}

/* The stacked-art tile: up to four covers in a 2x2 grid inside the same
 * 44px box `.pl-kinds`' icon-strip fallback already uses (`.book-art` etc,
 * above) — kept at that size rather than the handoff's 46px so every list
 * in this row family still shares one geometry; a playlist row sitting next
 * to a Library row with a visibly different art size would read as a bug,
 * not a detail. One image fills the box; fewer than four leaves the unused
 * cells as the surface colour, which reads as intentional at this size. */
.pl-art {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(2, 1fr);
  gap: 1px;
  overflow: hidden;
}
.pl-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.pl-art-1 img {
  grid-column: 1 / 3;
  grid-row: 1 / 3;
}

/* One playlist's hero-less header (§15): name is the screen's own <h1>, this
 * is the meta line under it. */
.pl-open-meta {
  color: var(--color-text-faint);
  font-size: 0.86rem;
  /* No negative top margin: the h1 it visually follows lives in the
   * screen's own .screen-head, a sibling container this element is not
   * adjacent to — .screen-scroll (this element's actual parent) is a
   * separately scrolling box, so pulling this up clipped it against that
   * box's own top edge instead of tightening anything under the h1. */
  margin: 0 0 var(--space-3);
}

/* Reorder mode swaps the ordinary swipeable rows for a plain label plus a
 * drag handle — deliberately a different, simpler row than the canonical
 * one, rather than a handle bolted onto it. The canonical row's swipe
 * wiring reads any horizontal pointer travel as a swipe with no regard for
 * vertical intent, so a drag handle sharing that row would have every
 * reorder attempt reinterpreted as a delete past 90px of horizontal
 * wobble — the two gestures need to never be live on the same element at
 * the same time, and this is the simplest way to guarantee it. */
.pl-reorder-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-divider);
}
.pl-drag-handle {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  color: var(--color-text-faint);
  background: var(--color-surface-2);
  border-radius: var(--radius-sm);
  cursor: grab;
  touch-action: none;
}
.pl-drag-handle:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
.pl-reorder-row.is-dragging {
  opacity: 0.5;
}
.pl-reorder-label {
  flex: 1;
  min-width: 0;
}
.pl-reorder-title {
  font-size: 0.92rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pl-reorder-sub {
  color: var(--color-text-faint);
  font-size: 0.78rem;
  margin-top: 0.1rem;
}
.pl-entry-title,
.item-title {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A playlist entry opens on tap same as any other row — see .row-title-btn's
 * own comment for why this is a real <button> and not just an onclick div.
 * Re-applies the flex sizing .row-title-btn's `all: unset` strips out. */
.pl-entry-meta.row-title-btn {
  flex: 1;
  min-width: 0;
  text-align: left;
  display: block;
}
.book-actions,
.source-actions,
.pl-actions,
.pl-entry-actions,
.item-actions {
  display: flex;
  gap: 2px;
  flex: none;
}
/* Episode metadata (date, duration, etc.) shares a row with action icons —
 * date left, actions right-justified. */
.item-sub-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: 0.15rem;
  min-width: 0;
}
.item-sub-row .item-sub {
  flex: 1;
  min-width: 0;
  margin-top: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.item-sub-row .item-actions {
  flex: none;
  gap: var(--space-1);
}
:root[data-layout='phone'] .item-sub-row .item-actions button {
  padding: 4px;
  min-width: 32px;
  min-height: 32px;
}
.book-actions button,
.source-actions button,
.pl-actions button,
.pl-entry-actions button,
.item-actions button {
  background: none;
  border: 0;
  color: var(--color-text-faint);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  /* inline-flex, not grid. Most of these are icon-only and grid centred them
   * fine, but Discover's rows are `${icon('play')} Play` — and CSS grid wraps
   * a bare text run in an anonymous grid item, so the word landed on a second
   * row under the glyph, wider than it and with no button chrome. flex puts
   * the two on one line and the gap only shows when there is a second item. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
}
.book-actions button:hover,
.source-actions button:hover,
.pl-actions button:hover,
.item-actions button:hover {
  color: var(--color-primary-text);
  background: var(--color-surface-2);
}
.is-played .item-title,
.is-played .pl-entry-title {
  opacity: 0.55;
}
.pl-entry-smart {
  background: var(--color-surface-2);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  border-bottom: 0;
  margin-bottom: 2px;
}

.empty {
  text-align: center;
  padding: var(--space-6) var(--space-4);
  color: var(--color-text-faint);
  line-height: 1.6;
}
.empty p {
  margin: 0 0 var(--space-3);
}
.empty strong {
  color: var(--color-text);
}
.empty [data-icon],
.empty svg {
  color: var(--color-primary-text);
}

/* ── Reader ──────────────────────────────────────────────────────────────── */

.reader {
  /* Deliberately not var(--font-display): that token now points at Space
   * Grotesk for titles/numerals (README Typography), but long-form reading
   * is a different job — an extended-reading serif is what it was set to
   * before the redesign, and the redesign doesn't specify a replacement. */
  font-family: 'EB Garamond', Georgia, serif;
  font-size: 1.14rem;
  line-height: 1.72;
  max-width: 34rem;
  margin: 0 auto;
}
.reader-eyebrow {
  font-family: -apple-system, sans-serif;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--color-text-faint);
}
.reader-chapter-head {
  margin-bottom: var(--space-5);
}
.reader-chapter-head h2 {
  margin: 0.2rem 0 0;
  font-size: 1.4rem;
}
.reader-chapter-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.reader-chapter-listen {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.5em 1em;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--color-primary-text);
  background: var(--color-primary-highlight);
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.reader-chapter-listen:hover {
  filter: brightness(1.1);
}
.reader-chapters-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.5em 0.8em;
  font-size: 0.85rem;
  color: var(--color-text-faint);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.reader-chapters-toggle:hover {
  color: var(--color-text);
  border-color: var(--color-primary);
}
.reader-para {
  margin: 0 0 1.1em;
}
.reader-empty {
  margin: 0 0 1.1em;
  color: var(--color-text-2);
  font-style: italic;
}
.reader-para.hl-saved {
  background: linear-gradient(transparent 60%, rgba(217, 180, 95, 0.25) 60%);
  cursor: pointer;
}
/* Jumping to a clip/note's source (Notes/Journal "Open" link) lands you mid-
 * chapter with no other cue which paragraph it was — a brief highlight flash
 * on arrival is the difference between "here" and "somewhere on this page". */
.reader-para.clip-jump-flash {
  animation: clip-jump-flash 2s ease-out;
}
@keyframes clip-jump-flash {
  0%,
  40% {
    background-color: rgba(217, 180, 95, 0.35);
  }
  100% {
    background-color: transparent;
  }
}
@media (prefers-reduced-motion: reduce) {
  .reader-para.clip-jump-flash {
    animation: none;
    background-color: rgba(217, 180, 95, 0.35);
  }
}
.reader-para {
  cursor: pointer;
}
.reader-illustration {
  margin: var(--space-5) 0;
  text-align: center;
}
.reader-illustration img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-md);
}
.reader-illustration figcaption {
  margin-top: var(--space-2);
  font-family: -apple-system, sans-serif;
  font-size: 0.8rem;
  color: var(--color-text-faint);
}
.reader-chapter-nav {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  margin: var(--space-6) 0;
  font-family: -apple-system, sans-serif;
}
.reader-chapter-nav button {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  border-radius: var(--radius-md);
  padding: 0.5em 0.85em;
  font-size: 0.9rem;
}
.reader-chapters-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.reader-chapters-group {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-2);
  font-family: -apple-system, sans-serif;
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-faint);
}
.reader-chapters-group:first-child {
  margin-top: 0;
}
.reader-chapters-item {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
  border-bottom: 1px solid var(--color-divider);
}
.reader-chapters-item.is-current .reader-chapters-title {
  color: var(--color-primary-text);
}
.reader-chapters-jump {
  flex: 1;
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  text-align: left;
  background: none;
  border: 0;
  color: var(--color-text);
  padding: var(--space-3) 0;
  font-family: -apple-system, sans-serif;
  font-size: 0.95rem;
}
.reader-chapters-num {
  color: var(--color-text-faint);
  font-size: 0.8rem;
  min-width: 1.4em;
}
.reader-chapters-listen {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.4rem;
  background: none;
  border: 0;
  color: var(--color-text-faint);
}
.reader-chapters-listen:hover,
.reader-chapters-listen:active {
  color: var(--color-primary-text);
}

/* ── Settings ────────────────────────────────────────────────────────────── */

/* #69: Sticky jump-link chips so any setting is one tap + one scroll away. */
/* Settings navigation: a search field and a row of groups.
 *
 * This replaces .settings-jumps, eight anchors pinned above one continuous
 * scroll. Those were a table of contents — you still landed in the middle of a
 * single long document and hunted for the rest of it. Groups show one section
 * set at a time, and the field searches across all of them. */
.settings-find {
  margin-bottom: var(--space-3);
}
.settings-find input {
  width: 100%;
}
.settings-groups {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
/* Also raised above `#settings-body button`, or the base pill shape loses too:
 * the radius, the padding and the dim colour were all being overridden, so
 * these rendered as ordinary square secondary buttons. */
#settings-body .settings-group,
.settings-group {
  padding: 0.45em 0.85em;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
}
.settings-group:hover {
  border-color: var(--color-primary);
}
/* Beats `#settings-body button` (1-0-1), which was flattening every one of
 * these to the same grey pill — you could not tell which group you were in,
 * and the intended 999px radius was gone too. An id selector in a shared base
 * rule outranks any class that comes later, so the state has to answer in
 * kind. Same story one rule down for the report's range chips. */
#settings-body .settings-group.is-on,
.settings-group.is-on {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
}
.settings-no-hits {
  margin-top: var(--space-4);
}

.setting {
  border-top: 1px solid var(--color-divider);
  padding: var(--space-4) 0;
}
.setting:first-of-type {
  border-top: 0;
  padding-top: 0;
}
.setting h2 {
  font-size: 1.05rem;
  margin: 0 0 var(--space-2);
}
.radio {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  padding: var(--space-3);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
  margin-top: var(--space-2);
  cursor: pointer;
  font-size: 0.92rem;
  line-height: 1.5;
}
.radio:has(input:checked) {
  border-color: var(--color-primary);
  background: var(--color-surface-2);
}
.radio input {
  width: auto;
  margin-top: 0.2em;
}

/* ── Backup and restore ──────────────────────────────────────────────────── */

.check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.92rem;
  cursor: pointer;
}
.check input {
  width: auto;
  margin: 0;
}
.bk-row {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: var(--space-3) 0 0;
}
#settings-body .bk-row button.ghost {
  background: transparent;
}
/* Replace is the only control here that can lose data. It reads as dangerous
   before it is pressed, not only in the dialog afterwards. */
#settings-body button.danger {
  border-color: color-mix(in srgb, var(--color-error) 55%, var(--color-border));
  color: var(--color-error);
}
#settings-body button.danger:hover {
  border-color: var(--color-error);
  background: color-mix(in srgb, var(--color-error) 12%, transparent);
}
.bk-sep {
  border: 0;
  border-top: 1px solid var(--color-divider);
  margin: var(--space-4) 0 var(--space-3);
}
.bk-h {
  font-size: 0.95rem;
  margin: 0 0 var(--space-1);
}
#backup-file {
  display: block;
  margin-top: var(--space-2);
  font-size: 0.88rem;
  max-width: 100%;
}
.bk-found {
  margin: var(--space-3) 0 0;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  border-left: 3px solid var(--color-primary);
  font-size: 0.92rem;
  line-height: 1.5;
}
/* ── Episode rows ────────────────────────────────────────────────────────── */

.item-filter {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.item-filter .hint {
  margin: 0;
}
.item-open {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  padding: 0;
  cursor: pointer;
}
.item-open:hover .item-title {
  color: var(--color-primary-text);
}
/* Played rows stay legible rather than disappearing — the point is "you have
   heard this", not "this is gone". */
.item-row.is-played .item-title {
  color: var(--color-text-faint);
}
.item-played[aria-pressed='true'] {
  color: var(--color-primary-text);
}
.item-remind.is-set {
  color: var(--color-primary-text);
}
.item-star.is-starred {
  color: var(--color-primary-text);
}
.item-detail {
  flex-basis: 100%;
  margin-top: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  font-size: 0.9rem;
  line-height: 1.6;
}
.item-detail[hidden] {
  display: none;
}
.item-detail-actions {
  margin-bottom: var(--space-3);
}
.item-archive-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.82rem;
}
.item-archive-btn.is-archived {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
.item-notes {
  /* Show notes can run to thousands of words; this keeps a row from becoming
     the whole screen while still being scrollable in place. */
  max-height: 22rem;
  overflow-y: auto;
  white-space: normal;
}
.item-notes a {
  word-break: break-word;
}
.item-ext-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.item-ext,
.item-ext-lite {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--color-primary-text);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--btn-radius);
  padding: 8px 14px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  text-decoration: none;
  transition:
    color 0.15s,
    border-color 0.15s;
}
.item-ext:hover,
.item-ext:focus-visible,
.item-ext-lite:hover,
.item-ext-lite:focus-visible {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
.item-links {
  display: flex;
  flex-direction: column;
  gap: 0.3em;
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-divider);
  font-size: 0.86rem;
  word-break: break-word;
}
.item-people {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  font-size: 0.85rem;
}
.item-person {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
}
.item-person img {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  object-fit: cover;
}
#opml-file {
  display: block;
  margin-top: var(--space-3);
  font-size: 0.88rem;
  max-width: 100%;
}
#opml-status.is-ok {
  color: var(--color-primary-text);
}
#opml-status.is-bad {
  color: var(--color-error);
}

#backup-status.is-ok,
#sync-status.is-ok {
  color: var(--color-primary-text);
}
#backup-status.is-bad,
#sync-status.is-bad {
  color: var(--color-error);
}

/* The sync code has to be read off one screen and typed into another phone, so
   it gets a monospace face with unambiguous digits and enough size to read at
   arm's length. It wraps rather than scrolling — a code with its tail off the
   edge is a code you will type wrong. */
.sync-code {
  margin: var(--space-3) 0 0;
}
.sync-code code {
  display: inline-block;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.02rem;
  letter-spacing: 0.06em;
  line-height: 1.7;
  word-break: break-all;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  user-select: all;
}
.sync-on {
  margin: 0;
  font-size: 0.92rem;
  color: var(--color-text-faint);
}
#sync-code-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  text-transform: uppercase;
}

/* ── Video surface ───────────────────────────────────────────────────────── */
/* A genuine full-screen view by default — its own "page", not a block
 * competing with whatever screen is underneath it (see the doc comment on
 * VideoSurface in app/js/sources.js). Minimized state below turns the SAME
 * element into a small fixed corner dock instead; the node itself never
 * moves, only these classes toggle. */

#video-surface {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay-sheet);
  background: #000;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}
#video-surface[hidden] {
  display: none;
}
.vs-inner {
  width: min(100%, 52rem);
  margin: 0 auto;
  padding: calc(var(--space-3) + env(safe-area-inset-top, 0px)) var(--space-4) var(--space-5);
}
.vs-head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  color: #f5efe0;
  margin-bottom: var(--space-2);
  padding-top: 10px;
  touch-action: none;
  user-select: none;
}
/* Purely visual — signals the header is draggable/swipeable without
 * widening the actual swipe target (see _bindSwipeDownToMinimize's doc
 * comment on why that stays scoped to the header strip alone). Absolutely
 * positioned so it never competes with the title/actions flex layout. */
.vs-drag-handle {
  position: absolute;
  left: 50%;
  top: -2px;
  transform: translateX(-50%);
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.3);
}
.vs-title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vs-head-actions {
  display: flex;
  gap: 2px;
  flex: none;
}
.vs-min-btn,
.vs-close {
  background: none;
  border: 0;
  color: #dcd6c8;
  padding: var(--space-2);
  border-radius: var(--radius-sm);
}
.vs-min-btn:hover,
.vs-close:hover {
  background: rgba(255, 255, 255, 0.08);
}
.vs-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.vs-yt-target,
.vs-frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
.vs-embed-err {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  gap: var(--space-2);
  padding: var(--space-4);
  text-align: center;
  color: #dcd6c8;
}
/* Info panel below the video — title, channel, date/duration/views,
 * description. Only what fromVideo/fromRumbleVideo actually carry gets
 * shown: Rumble tracks have no duration, views or description today (its
 * RSS source doesn't provide them), so those spans just stay hidden rather
 * than showing a blank line. */
.vs-body {
  margin-top: var(--space-4);
  color: #f5efe0;
}
.vs-full-title {
  margin: 0 0 var(--space-2);
  font-size: 1.2rem;
  line-height: 1.35;
}
.vs-full-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4em 0.8em;
  font-size: 0.85rem;
  color: #b9b3a6;
  margin-bottom: var(--space-3);
}
.vs-full-channel {
  font-weight: 600;
  color: #dcd6c8;
}
.vs-full-desc {
  font-size: 0.92rem;
  line-height: 1.6;
  color: #dcd6c8;
  white-space: pre-wrap;
  margin: 0;
}
/* Action buttons — icon-only rounded squares matching the podcast
 * full-screen player's look, adapted to .vs-body's always-dark palette. */
.vs-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-bottom: var(--space-2);
}
.vs-actions button {
  font: inherit;
  color: #dcd6c8;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--btn-radius);
  width: 44px;
  height: 44px;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition:
    color 0.15s,
    border-color 0.15s;
}
.vs-actions button svg {
  display: block;
}
.vs-actions button:hover,
.vs-actions button:focus-visible {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
.vs-actions button.on {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
/* Resume links — styled as button-like pills, both on one row. */
.vs-ext-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.vs-ext,
.vs-ext-lite {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--color-primary-text);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--btn-radius);
  padding: 8px 14px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  text-decoration: none;
  transition:
    color 0.15s,
    border-color 0.15s;
}
.vs-ext:hover,
.vs-ext:focus-visible,
.vs-ext-lite:hover,
.vs-ext-lite:focus-visible {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}

/* Floating chrome — small overlay buttons on the frame itself, shown only
 * while minimized (see below). Absent from normal DOM flow either way; only
 * their `display` toggles. */
.vs-float-chrome {
  display: none;
  position: absolute;
  top: 4px;
  left: 4px;
  right: 4px;
  z-index: var(--z-overlay);
  justify-content: space-between;
}
.vs-float-btn {
  width: 44px;
  height: 44px;
  border-radius: var(--btn-radius);
  background: rgba(5, 8, 18, 0.65);
  color: #fff;
  border: none;
  display: grid;
  place-items: center;
  cursor: pointer;
  backdrop-filter: blur(4px);
}
.vs-float-btn:hover {
  background: rgba(5, 8, 18, 0.85);
}

/* Drag-to-reposition hit area for the floating dock — sits above the video
 * iframe so a drag gesture never gets swallowed by it, but below the
 * expand/close buttons (z-index 2) so those stay independently tappable.
 * Inert outside vs-floating: display:none there, so this never intercepts a
 * click on the full, docked player. */
.vs-drag-layer {
  display: none;
  position: absolute;
  inset: 0;
  z-index: var(--z-content);
  touch-action: none;
  cursor: grab;
}
.vs-drag-layer:active {
  cursor: grabbing;
}

#video-surface.vs-floating {
  position: fixed;
  inset: auto;
  top: auto;
  left: auto;
  right: 10px;
  bottom: calc(var(--tabbar-h) + 10px + env(safe-area-inset-bottom, 0px));
  /* The dock's default corner sits directly above the tab bar — the same
   * spot the docked mini-player bar (.pk-bar, --z-player-bar: 90) occupies
   * when a track is active, which is the normal case while a video is
   * floating. Sharing --z-tabs (70) with the actual tab bar put the dock
   * BELOW the mini bar in that overlap, so the bar's controls silently
   * swallowed every tap and drag meant for the dock underneath it. The dock
   * is something the user is actively watching and repositioning; it wins. */
  z-index: calc(var(--z-player-bar) + 1);
  width: min(46vw, 220px);
  height: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
  overflow: hidden;
}
#video-surface.vs-floating .vs-inner {
  width: 100%;
  padding: 0;
}
/* A paused dock has to read as paused.
 *
 * Another surface taking over pauses the video rather than closing it, on
 * purpose — you may be coming back to it. But nothing said so, so starting a
 * podcast left a silent, frozen video floating in the corner that looked
 * exactly like a playing one, which is what "the video is stuck" was
 * describing. The frame is dimmed and a play glyph sits over it, so the dock
 * states its own condition and its close button reads as a real choice
 * rather than the only escape from something broken. */
/* The frame only. .vs-float-chrome (the ✕ and expand buttons) lives inside
 * it, and group opacity would dim the two controls this state exists to make
 * worth reaching for. */
#video-surface.vs-floating.vs-paused .vs-frame > :not(.vs-float-chrome) {
  opacity: 0.45;
  filter: grayscale(0.35);
}
#video-surface.vs-floating.vs-paused .vs-inner::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 34px;
  height: 34px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  /* A play triangle drawn in CSS rather than an icon element, so this needs
   * no markup change and cannot desynchronise from the class that shows it. */
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
  pointer-events: none;
}
#video-surface.vs-floating.vs-paused .vs-inner::before {
  content: '';
  position: absolute;
  left: calc(50% + 2px);
  top: 50%;
  transform: translate(-50%, -50%);
  border-style: solid;
  border-width: 7px 0 7px 12px;
  border-color: transparent transparent transparent #fff;
  z-index: 1;
  pointer-events: none;
}
#video-surface.vs-floating .vs-inner {
  position: relative;
}
@media (prefers-reduced-motion: reduce) {
  #video-surface.vs-floating.vs-paused .vs-frame > :not(.vs-float-chrome) {
    filter: none;
  }
}
#video-surface.vs-floating .vs-head,
#video-surface.vs-floating .vs-body,
#video-surface.vs-floating .vs-note {
  display: none;
}
#video-surface.vs-floating .vs-frame {
  border-radius: 0;
}
#video-surface.vs-floating .vs-float-chrome {
  display: flex;
}
#video-surface.vs-floating .vs-drag-layer {
  display: block;
}
/* Parked in the unified player shell's stage.
 *
 * The shell owns the layout; this element supplies the pixels. It is NOT moved
 * into the shell's DOM to get there — re-parenting an iframe reloads it on iOS
 * WKWebView, the same reason this node is never moved after insertion (see the
 * VideoSurface doc comment in app/js/sources.js). Instead the shell measures
 * its stage and this parks itself over that rect, above the shell's panel, so
 * the shell's chrome sits around the video rather than over it.
 *
 * Same recipe as .vs-floating above — one element, chrome hidden, geometry
 * from outside — differing only in where the geometry comes from. */
#video-surface.vs-parked {
  position: fixed;
  inset: auto;
  top: var(--vs-park-top, 0px);
  left: var(--vs-park-left, 0px);
  width: var(--vs-park-width, 100%);
  height: var(--vs-park-height, auto);
  z-index: var(--z-player-shell-video);
  background: #000;
  border-radius: 0;
  overflow: hidden;
}
#video-surface.vs-parked .vs-inner {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
}
#video-surface.vs-parked .vs-head,
#video-surface.vs-parked .vs-body,
#video-surface.vs-parked .vs-note,
#video-surface.vs-parked .vs-float-chrome {
  display: none;
}
/* Parked, the drag layer covers the video except its own control bar.
 *
 * It was a 56px strip along the top edge, on the reasoning that the embed's
 * controls had to stay clickable. They do — but at 390px a parked video is
 * 219px tall, so that strip was a quarter of it, along an edge nobody swipes.
 * The other 163px was bare cross-origin iframe, which keeps its own pointer
 * events and hands the app nothing: swiping across the middle of a video,
 * which is what everybody actually does, did nothing at all. Reported twice.
 *
 * 48px stays free at the bottom, where YouTube draws its own bar (scrubber,
 * quality, captions, fullscreen). Everything above it belongs to the app and
 * carries both axes through the same _wireDragSurface every other surface
 * uses: sideways for Cover Flow, down to close.
 *
 * The cost is tap-to-pause on the middle of the embed, and it is small — the
 * shell's own transport sits directly underneath with play/pause, a scrubber
 * and skip, all of which drive a parked video. */
#video-surface.vs-parked .vs-drag-layer {
  display: block;
  inset: 0 0 48px 0;
  height: auto;
}
#video-surface.vs-parked .vs-frame {
  height: 100%;
  aspect-ratio: auto;
  border-radius: 0;
}

/* Double-tap size — a middle ground between the small corner dock and full
 * screen, still floating (draggable, rest of the app reachable underneath).
 * Same corner-anchored positioning as the small dock, just bigger; a saved
 * drag position and the viewport clamp both already read the element's
 * actual rendered size, so they need no separate handling for this. */
#video-surface.vs-floating.vs-floating-lg {
  width: min(84vw, 440px);
}

/* ── Playlist picker ─────────────────────────────────────────────────────── */

#pl-picker {
  position: fixed;
  inset: 0;
  /* --z-player-sheet (1100), not --z-sheet (92): the player's List button
   * (data-act="list" → App.addTrackToPlaylist → this picker) opens it while
   * the full player shell (.ps, z-index 1005) is still showing. At --z-sheet
   * the picker opened underneath the still-open shell — technically visible
   * in the DOM, invisibly stacked behind an opaque player, which read as
   * "the List button doesn't do anything". This token existed, unused, for
   * exactly this — every other place the picker opens from (Library, Feed)
   * has nothing near this z-range to collide with either way. */
  z-index: var(--z-player-sheet);
  background: rgba(8, 9, 16, 0.55);
  display: flex;
  align-items: flex-end;
}
#pl-picker[hidden] {
  display: none;
}
.picker-inner {
  width: 100%;
  max-height: 76vh;
  overflow-y: auto;
  background: var(--color-surface);
  border-top-left-radius: var(--radius-xl);
  border-top-right-radius: var(--radius-xl);
  padding: var(--space-4);
  padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
}
.picker-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-weight: 600;
  margin-bottom: var(--space-3);
}
.picker-close {
  background: none;
  border: 0;
  color: var(--color-text-faint);
  padding: var(--space-2);
}
.picker-body button {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--color-divider);
  color: var(--color-text);
  padding: var(--space-3) 0;
  text-align: left;
}
.picker-body button:disabled {
  color: var(--color-text-faint);
}
.picker-state {
  color: var(--color-primary-text);
  display: flex;
}
.picker-empty {
  color: var(--color-text-faint);
  font-size: 0.9rem;
}
.picker-new {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
}
.picker-new input {
  flex: 1;
}
.picker-new button {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  background: var(--color-primary);
  border: 0;
  color: #1c1810;
  border-radius: var(--radius-md);
  padding: 0.55em 0.9em;
  font-weight: 600;
  white-space: nowrap;
}

/* ── Tabs ────────────────────────────────────────────────────────────────── */

.tabs {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-tabs);
  display: grid;
  /* Count-agnostic: hard-coding repeat(5, 1fr) meant adding the Notes tab
     silently wrapped the bar onto two rows on a 390px phone. */
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  /* Extend into the home-indicator safe area on iPhone PWA so the mini player
     docks flush above the tab content, not above the safe-area gap. syncTabbarHeight
     subtracts this padding when setting --pk-dock-bottom. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
  /* Opaque, and with an edge.
   *
   * The phone bar had no background at all — the surface colour was only ever
   * declared under [data-layout='desktop'], where this becomes a sidebar. So
   * every screen scrolled visibly THROUGH the tab labels: 16px of bleed in
   * portrait, the full 54px in landscape with body copy plainly legible behind
   * "Home" and "Feed". It also made the bar look like part of whatever was
   * underneath it, which is why the buttons behind it read as tappable when
   * they are not (see the padding below). */
  background: var(--color-surface);
  border-top: 1px solid var(--color-divider);
}
/* Nothing scrolls to a stop underneath the tab bar.
 *
 * Notes' "Export everything" and "Share" sat at y 775-806 with the bar
 * starting at 790, so elementFromPoint on the middle of Export returned the
 * tab: the control was visible, looked pressable, and could not be pressed.
 * A scroll region that ends exactly where a fixed bar begins will always do
 * that to its last row; the fix belongs on every screen, not on Notes. */
.screen-scroll,
.screen > .screen-scroll {
  padding-bottom: calc(var(--tabbar-h, 54px) + var(--space-3));
}
.tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  min-height: 44px;
  background: none;
  border: 0;
  color: var(--color-text-faint);
  padding: calc(0.6rem + 3px) 0.1rem calc(0.6rem - 3px);
  font-size: 0.62rem;
  letter-spacing: 0.01em;
}
/* Six labels on a narrow phone: ellipsise rather than wrap, so every tab keeps
   the same height and the bar keeps the same one row. */
.tab > span:last-child {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Below ~360px the labels stop earning their space; the icons still identify
   the tabs and the accessible name comes from aria-current plus the text, which
   stays in the DOM. */
@media (max-width: 22.5rem) {
  .tab > span:last-child {
    /* 0.59375rem is 9.5px at the default root — the type-scale floor (README
     * Typography); 0.56rem (8.96px) was under it. Written in rem rather than
     * as a flat 9.5px so the floor tracks the user's base size instead of
     * pinning this label at 9.5px however far they have scaled their type up. */
    font-size: 0.59375rem;
  }
}
.tab.is-on {
  color: var(--color-primary-text);
}
.tab svg {
  display: block;
}

/* ── Home ────────────────────────────────────────────────────────────────── */

.home-card {
  margin-bottom: var(--space-5);
}
.home-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.home-card-head h2 {
  /* README Typography "Section label": Geist 10.5/600, .14em, uppercase. */
  font-size: 0.65625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-text-dim, var(--color-text-faint));
  margin: 0;
}
.home-card-head-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Continue is the one thing on this screen that is a single, obvious target,
   so it is a full-width button rather than a row with a play icon on the end. */
.home-resume {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 12px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: linear-gradient(180deg, var(--color-surface-2), var(--color-surface));
  color: var(--color-text);
  transition: border-color var(--transition);
}
.home-resume:hover {
  /* #3A3F4C is the README's literal Night-theme value, but the Continue
   * card is one theme-aware component — a fixed dark-gray hex would read
   * as an almost invisible near-black hairline on Day's cream surface.
   * Deriving it from theme-aware tokens (same technique as .persona-chip's
   * hover in Phase 3) keeps the "border lightens on hover" effect correct
   * in both themes instead of just the one the spec was written against. */
  border-color: color-mix(
    in srgb,
    var(--color-border-strong, var(--color-border)) 60%,
    var(--color-text-faint)
  );
}
.home-resume-art-wrap {
  position: relative;
  flex: none;
  width: 78px;
  height: 78px;
}
.home-resume-art {
  display: block;
  width: 78px;
  height: 78px;
  border-radius: 4px;
  object-fit: cover;
}
div.home-resume-art {
  background: var(--color-surface-2);
  color: var(--color-primary-text);
  display: grid;
  place-items: center;
}
.home-resume-rail {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  border-radius: 0 0 4px 4px;
  /* This sits over the episode/book art (or its icon-fallback square), not
   * the page background, so it needs to read against whatever colour that
   * art actually is — including the fallback square, which is
   * --color-surface-2 and turns solid white on Day, where the spec's
   * translucent-white track (#ffffff26) would vanish. A dark scrim reads
   * as a visible groove against art of any colour in either theme, which a
   * theme token alone (a flat colour with no art underneath it in mind)
   * wouldn't. */
  background: rgba(0, 0, 0, 0.35);
  overflow: hidden;
}
.home-resume-rail-fill {
  display: block;
  height: 100%;
  background: var(--color-primary);
}
.home-resume-meta {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  flex: 1;
}
.home-resume-kindrow {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.home-resume-kind {
  font-size: 0.59375rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.home-resume-pos {
  font-size: 0.65625rem;
  color: var(--color-text-dim, var(--color-text-faint));
}
.home-resume-title {
  font-family: var(--font-display);
  font-size: 1.03125rem;
  line-height: 1.25;
  font-weight: 500;
  /* Two lines, then ellipsis. An episode title can be a paragraph, and letting
     it push the card to six lines tall defeats the point of a summary. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.home-resume-sub {
  font-size: 0.75rem;
  color: var(--color-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-resume-play {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 5px;
  background: var(--color-primary);
  color: var(--color-on-primary);
  display: grid;
  place-items: center;
}
.home-resume-where svg {
  display: block;
}

/* History reuses .home-resume's card look (Home's single "Continue" row) for
 * a whole scrollable list of past plays — this is the only place several of
 * them stack, so the gap between cards is scoped to this screen rather than
 * changing .list generally, which other screens use for border-separated
 * rows with no card styling of their own. */
#history-body.list {
  gap: var(--space-2);
}

/* #35/#40: Swipe acts directly, Gmail-style — the labels underneath double
 * as a live "this is what letting go does now" preview. Whichever one is
 * armed for the current drag distance brightens and grows; the other fades. */
.swipe-row {
  position: relative;
  overflow: hidden;
  touch-action: pan-y;
}
.swipe-actions {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: stretch;
  justify-content: flex-end;
  gap: 2px;
}
.swipe-action {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0.5em 0.8em;
  min-height: 44px;
  border: 0;
  border-radius: var(--radius-sm);
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  /* Invisible until a drag actually starts. `.swipe-content` covers this
   * layer at rest, but only to within a subpixel — on a two-column desktop
   * grid the row lands on a half-pixel and the amber `Played` behind it
   * bled a hairline across the top of every row. Nothing is lost: these are
   * a touch affordance and there is nothing to see before the drag. */
  opacity: 0;
  transform: scale(1);
  transition:
    transform 0.15s ease,
    opacity 0.15s ease;
}
.swipe-row.is-swiping .swipe-action {
  opacity: 0.55;
}
.swipe-played {
  background: var(--color-primary-highlight);
  color: var(--color-primary-text);
}
.swipe-remove {
  background: var(--color-surface-2);
  color: var(--color-text-faint);
}
.swipe-row.swipe-armed-remove .swipe-remove,
.swipe-row.swipe-armed-played .swipe-played {
  opacity: 1;
  transform: scale(1.08);
}
.swipe-row.swipe-armed-remove .swipe-played,
.swipe-row.swipe-armed-played .swipe-remove {
  opacity: 0.3;
}

/* Home "New" rows. .home-new-row is the outer element in both shapes it's
 * used in — a plain <article> for Live-now items, or the swipe-wired
 * <article class="swipe-row"> for New-in-<persona> rows — but only the
 * plain shape lays out its own children directly; the swipe shape's real
 * visible row is .swipe-content; giving both the same padding would double
 * it, so the swipe shape's own padding is zeroed and delegates to it. */
.home-new-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 14px;
  border-top: 1px solid var(--color-divider);
}
.home-new-row:first-child {
  border-top: 0;
}
.home-new-row.swipe-row {
  padding: 0;
}
.home-new-row-art-wrap {
  position: relative;
  flex: none;
  display: block;
}
.home-new-row .source-art {
  width: 38px;
  height: 38px;
  border-radius: 4px;
}
.home-new-row .source-art svg {
  width: 18px;
  height: 18px;
}
.home-new-row-dot {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--color-bg);
}
.home-new-row .source-meta h3 {
  font-size: 0.84375rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-new-row .source-meta.row-title-btn {
  flex: 1;
  min-width: 0;
  text-align: left;
}
.home-new-row .source-sub {
  font-size: 0.71875rem;
  color: var(--color-text-faint);
  margin-top: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* button.home-row-action, not .home-row-action: the shared .source-actions
 * button rule (app.css, Library rows too) sets its own padding/hover
 * background at equal-or-higher specificity — matching its type+class shape
 * here, at a later point in the cascade, is what makes these actually win
 * rather than silently inheriting Library's padding and hover fill. */
button.home-row-action {
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: 4px;
  border: 1px solid var(--color-border-strong, var(--color-border));
  background: none;
  color: var(--color-text);
  display: grid;
  place-items: center;
  transition:
    border-color 0.15s,
    color 0.15s;
}
.home-row-action svg {
  display: block;
}
button.home-row-action:hover,
button.home-row-action:focus-visible {
  background: none;
  border-color: var(--accent, var(--color-primary));
  color: var(--accent, var(--color-primary));
}
/* Kind colours, hover/press only — resting rows stay monochrome so a long
 * list doesn't turn into confetti (README "New in <persona>"). */
.home-row-action-watch {
  --accent: #e5533c;
}
.home-row-action-read {
  --accent: #7fb2e5;
}
.home-row-action-play {
  --accent: var(--color-primary);
}
button.home-row-action-queue {
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text-dim, var(--color-text-faint));
  display: grid;
  place-items: center;
}
button.home-row-action-queue:hover {
  background: none;
  color: var(--color-text);
}
.home-new-count {
  font-size: 0.65625rem;
  /* #4E5563 is the README's literal Night value; --color-text-dimmer is
   * the theme-aware token nearest it in the same tier (least-prominent
   * meta text), so this stays legible against Day's cream surface too. */
  color: var(--color-text-dimmer, var(--color-text-faint));
}
.swipe-content {
  position: relative;
  z-index: var(--z-content);
  background: var(--color-bg);
  transition: transform 0.2s ease-out;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 14px;
  flex: 1;
  min-width: 0;
  touch-action: pan-y;
}

/* ── The canonical episode row (app/js/row.js, handoff §21) ───────────────
 *
 * One geometry for all eight episode lists. The kind dot leading the meta
 * line is the only colour on a resting row — the action glyphs stay
 * monochrome until hover or press, so a long mixed list doesn't turn into
 * confetti. Two buttons maximum, audio play always rightmost.
 */
.ep-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 14px;
  border-top: 1px solid var(--color-divider);
}
.ep-row:first-child {
  border-top: 0;
}
/* The swipe shape's real visible row is .swipe-content, which carries the
 * padding itself — leaving it on the outer element as well would double it. */
.ep-row.swipe-row {
  padding: 0;
  gap: 0;
}
.ep-row-art {
  flex: none;
  width: 46px;
  height: 46px;
  border-radius: 5px;
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  display: grid;
  place-items: center;
  object-fit: cover;
}
.ep-row-list-dense .ep-row-art {
  width: 40px;
  height: 40px;
}
/* `all: unset` strips the button chrome so the title reads as plain text.
 * It is a real <button> because the row opens on tap and an <article> is
 * neither focusable nor keyboard-operable. */
.ep-row-main {
  all: unset;
  flex: 1;
  min-width: 0;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ep-row-main[disabled] {
  cursor: default;
}
.ep-row-main:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.ep-row-title {
  display: block;
  font-size: 0.84375rem; /* 13.5px */
  font-weight: 500;
  line-height: 1.3;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ep-row-played {
  color: var(--color-primary-text);
  vertical-align: -0.1em;
}
.ep-row-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.ep-row-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
}
.ep-row-meta-text {
  font-size: 0.6875rem; /* 11px */
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ep-row-actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
}
/* button.ep-row-action, not .ep-row-action: the shared `.source-actions
 * button` rule sets its own padding and hover fill at equal specificity, and
 * some of these rows still live inside that container. */
button.ep-row-action,
.ep-row-action {
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: 4px;
  border: 1px solid var(--color-border-strong, var(--color-border));
  background: none;
  color: var(--color-text);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition:
    border-color 0.15s,
    color 0.15s;
}
.ep-row-action svg {
  display: block;
}
button.ep-row-action:hover,
button.ep-row-action:focus-visible,
.ep-row-action:hover,
.ep-row-action:focus-visible {
  background: none;
  border-color: var(--accent, var(--color-primary));
  color: var(--accent, var(--color-primary));
}
.ep-row-action-watch {
  --accent: #e5533c;
}
.ep-row-action-read {
  --accent: #7fb2e5;
}
.ep-row-action-play {
  --accent: var(--color-primary);
}
/* The overflow that a row reveals on hover, so that with a pointer every
 * action touch puts behind a long-press is one click away (§20). It is
 * genuinely absent — not merely transparent — until then, which is what
 * keeps the resting row as quiet as it is on the phone. On a touch device
 * there is no hover, so it never takes space there at all. */
/* §20: every action that lives on a swipe on touch lives on hover with a
 * pointer. Hidden at rest so the resting row is as quiet as it is on the
 * phone — the point is that a pointer has somewhere to put these, not that
 * the row should be busier. */
.ep-row-more,
.ep-row-quick {
  display: none;
  /* The same 30px square geometry as the play button beside them, because
   * they sit in the same row and a cluster of three different-sized targets
   * reads as three unrelated controls. `background: none` is load-bearing:
   * the app's generic button fill rendered these as grey blocks with the
   * glyph invisible behind them — which is how the ··· has looked since it
   * was added, unnoticed because nothing hovered it in a capture. */
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  place-items: center;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--color-text-dim);
}
@media (hover: hover) and (pointer: fine) {
  .ep-row:hover .ep-row-more,
  .ep-row:focus-within .ep-row-more,
  .ep-row:hover .ep-row-quick,
  .ep-row:focus-within .ep-row-quick {
    display: grid;
  }
}
/* A star that is on stays on whether or not the pointer is over the row —
 * hiding it would mean the only way to see what you had starred was to hover
 * every row in the list. */
@media (hover: hover) and (pointer: fine) {
  .ep-row-quick.is-on {
    display: grid;
    color: var(--color-primary-text);
  }
}
button.ep-row-quick:hover,
button.ep-row-quick:focus-visible {
  border: 0;
  color: var(--color-text);
}
button.ep-row-quick.is-on:hover {
  color: var(--color-primary-text);
}
button.ep-row-more:hover,
button.ep-row-more:focus-visible {
  border: 0;
  color: var(--color-text);
}
/* A played row steps back rather than disappearing — it is still in the
 * list, it just isn't the thing to open next. */
.ep-row.is-played .ep-row-title,
.ep-row.is-played .ep-row-art {
  opacity: 0.55;
}

/* Starred's unstar sits outside the two-button budget on purpose: it is the
 * one action that list exists for, and it is already amber at rest because
 * it reports state rather than offering a way in. */
.ep-row-action-star {
  --accent: var(--color-primary);
  color: var(--color-primary-text);
}

/* Section labels for the grouped cross-cutting lists — Starred by kind,
 * All episodes and History by day (§14). */
.ep-group + .ep-group {
  margin-top: 14px;
}
.ep-group-label {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 10px 14px 6px;
  font-size: 0.65625rem; /* 10.5px */
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}
.ep-group-count {
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--color-text-dimmer);
  font-variant-numeric: tabular-nums;
}
/* The first row of every group draws its own top border — without this only
 * the first group's first row would be borderless. */
.ep-group .ep-row:first-child {
  border-top: 1px solid var(--color-divider);
}

/* The right-swipe queue ladder (§22). Distance picks the destination and the
 * label under the thumb changes as the row travels, so overshooting once
 * teaches the third rung rather than hiding it behind a long-press. */
.swipe-actions-left {
  justify-content: flex-start;
  align-items: center;
  gap: 0;
  padding-left: 14px;
}
.swipe-rung {
  position: absolute;
  left: 14px;
  font-size: 0.78125rem;
  font-weight: 600;
  opacity: 0;
  transition: opacity 0.12s;
  white-space: nowrap;
}
.swipe-rung-later {
  color: #4fbfa0;
}
.swipe-rung-next {
  color: var(--color-primary-text);
}
.swipe-rung-last {
  color: #7fb2e5;
}
.swipe-row.swipe-armed-later .swipe-rung-later,
.swipe-row.swipe-armed-next .swipe-rung-next,
.swipe-row.swipe-armed-last .swipe-rung-last {
  opacity: 1;
}
.source-layout-toggle {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.unified-follow-all {
  margin-top: var(--space-3);
  text-align: center;
}
#search-results .section-head {
  margin-top: var(--space-3);
}
/* ── Day activity (journal tab) ────────────────────────────────────────── */
.day-activity {
  margin-top: var(--space-4);
}
.day-activity .section-head {
  margin-top: var(--space-3);
  display: flex;
  align-items: center;
  gap: 0.3em;
}
/* Only clip-row. A .day-list .note-row carries onclick="…openNote(…)"
 * (notes-ui.js), so overriding its pointer cursor made a row that opens a note
 * look like it does nothing — the opposite of the affordance this block is
 * for. Clips genuinely aren't clickable here, so they keep the override. */
.day-list .clip-row {
  cursor: default;
}
/* Scoped to .day-list, which is the only place hist-row exists. Unlike
 * note-row/clip-row (which get their own flex layout from a rule elsewhere,
 * and only lose their cursor styling in this context), hist-row has no other
 * rule at all — without display:flex here, the chevron icon after .hist-meta
 * (a block-level div) wraps onto its own row instead of sitting at the end of
 * the row it belongs to. Global selectors for a one-context layout are how a
 * later .hist-row somewhere else inherits a fix it never asked for. */
.day-list .hist-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
}
.day-list .hist-meta {
  flex: 1;
  min-width: 0;
}
.day-list .hist-row h3 {
  font-size: 0.92rem;
  font-weight: 600;
  margin: 0;
}

/* ── Library: one filtered list (handoff §4) ────────────────────────────
 *
 * Four controls above the first source instead of eleven: a layout switch, a
 * search field, a sort that cycles in place, and the kind chips. The six
 * accordions this replaced are gone — a kind is a filter, never a container —
 * and everything that was a setting rather than content moved into the ···
 * sheet.
 */
.lib-head {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 14px 14px 10px;
}
.lib-head-text {
  flex: 1;
  min-width: 0;
}
.lib-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.375rem; /* 22px */
  font-weight: 500;
  letter-spacing: -0.015em;
  line-height: 1.2;
}
.lib-count {
  margin-top: 2px;
  font-size: 0.71875rem; /* 11.5px */
  color: var(--color-text-faint);
}
.lib-layout {
  flex: none;
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--color-border);
  border-radius: 7px;
}
.lib-layout-btn {
  width: 28px;
  height: 26px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--color-text-dim);
  cursor: pointer;
}
.lib-layout-btn.is-on {
  background: var(--color-surface-offset, var(--color-surface-2));
  color: var(--color-text);
}

.lib-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 14px;
}
.lib-search {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 38px;
  padding: 0 11px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  color: var(--color-text-dim);
}
.lib-search input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.8125rem; /* 13px */
  border: 0;
  background: none;
  color: var(--color-text);
  padding: 0;
}
.lib-search input:focus {
  outline: none;
}
.lib-search:focus-within {
  border-color: var(--color-primary);
}
.lib-search-clear {
  flex: none;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--color-border);
  color: var(--color-text-2);
  cursor: pointer;
}
/* Cycles Recent → A–Z → Unplayed in place. Three values are not worth a
 * menu, and every one of them actually reorders the list. */
.lib-sort,
.lib-more {
  flex: none;
  height: 38px;
  padding: 0 12px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  color: var(--color-text-2);
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
}
.lib-more {
  width: 38px;
  padding: 0;
  display: grid;
  place-items: center;
  color: var(--color-text-dim);
}
.lib-sort:hover,
.lib-more:hover {
  border-color: var(--color-border-strong);
}

/* The chip row scrolls horizontally rather than wrapping: a wrapping row
 * changes the height of everything below it as the query narrows, and the
 * list jumping under the thumb while typing is worse than a scroll. */
.lib-chips,
.lib-collections {
  display: flex;
  gap: 6px;
  padding: 12px 14px 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.lib-chips::-webkit-scrollbar,
.lib-collections::-webkit-scrollbar {
  display: none;
}
.lib-chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-2);
  font-size: 0.75rem; /* 12px */
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}
.lib-chip-count {
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--color-surface-offset, var(--color-surface-2));
  color: var(--color-text-faint);
  font-size: 0.625rem; /* 10px */
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.lib-chip.is-on {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-primary);
}
.lib-chip.is-on .lib-chip-count {
  background: #08090d26;
  color: var(--color-on-primary);
}

.lib-collections {
  gap: 8px;
  padding-bottom: 4px;
  /* `.screen-scroll` here is also `.list`, i.e. a column flexbox, and a flex
   * item defaults to shrinking before its container scrolls. With the list
   * long enough to overflow, that squeezed these cards to two-thirds height
   * and clipped the sub-line off every one of them — `built` / `0` / `recent
   * plays`, the only thing on the card that carries information. */
  flex: none;
}
.lib-chips {
  flex: none;
}
.lib-collection {
  flex: none;
  width: 104px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 11px;
  text-align: left;
  background: linear-gradient(180deg, var(--color-surface-2), var(--color-surface));
  border: 1px solid var(--color-border);
  border-radius: 8px;
  cursor: pointer;
}
.lib-collection:hover {
  border-color: var(--color-border-strong);
}
.lib-collection-label {
  font-size: 0.78125rem; /* 12.5px */
  font-weight: 500;
  color: var(--color-text);
}
.lib-collection-sub {
  font-size: 0.65625rem; /* 10.5px */
  color: var(--color-text-dim);
}

.lib-list {
  margin-top: 12px;
}
.lib-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 14px;
  border-top: 1px solid var(--color-divider);
  cursor: pointer;
}
.lib-row:first-child {
  border-top: 0;
}
.lib-row:hover,
.lib-row.is-selected {
  background: var(--color-surface);
}
.lib-row-art {
  flex: none;
  width: 46px;
  height: 46px;
  border-radius: 5px;
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  display: grid;
  place-items: center;
  object-fit: cover;
}
.lib-row-main {
  all: unset;
  flex: 1;
  min-width: 0;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.lib-row-main:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.lib-row-title {
  display: block;
  font-size: 0.84375rem; /* 13.5px */
  font-weight: 500;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lib-row-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.lib-row-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
}
.lib-row-sub {
  font-size: 0.6875rem; /* 11px */
  color: var(--color-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lib-pill {
  flex: none;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--color-surface-2);
  color: var(--color-text-faint);
  font-size: 0.65625rem; /* 10.5px */
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.lib-pill.is-new {
  background: var(--color-primary-highlight);
  color: var(--color-primary-text, var(--color-primary));
}
.lib-row-chevron {
  flex: none;
  display: grid;
  place-items: center;
  color: var(--color-text-dimmer);
}

.lib-grid {
  display: grid;
  /* minmax(0, 1fr), not 1fr.
   *
   * `1fr` is `minmax(auto, 1fr)`, so a column whose content has a wide
   * minimum — a long unbroken title like "Interfaces at Work" — grows past
   * its share. Measured at 390px: 97.4 / 115.1 / 97.5, which made the middle
   * column's artwork taller and pushed its title 18px below its neighbours'.
   * The grid read as ragged for a reason that looked like a spacing bug. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px 10px;
  padding: 14px;
}
.lib-tile {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
}
.lib-tile-art {
  position: relative;
  aspect-ratio: 1;
  border-radius: 5px;
  overflow: hidden;
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  display: grid;
  place-items: center;
}
.lib-tile-art img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.lib-tile-pill {
  position: absolute;
  right: 4px;
  bottom: 4px;
  padding: 2px 6px;
  border-radius: 999px;
  background: #08090dcc;
  color: var(--color-text);
  font-size: 0.59375rem; /* 9.5px — the floor, and it is paired with the art */
  font-weight: 600;
}
.lib-tile-title {
  font-size: 0.71875rem; /* 11.5px */
  font-weight: 500;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lib-tile-kind {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.65625rem; /* 10.5px */
  color: var(--color-text-dim);
}
.lib-tile.is-selected .lib-tile-art {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

.lib-empty {
  padding: 34px 24px;
  text-align: center;
}
.lib-empty-lead {
  margin: 0;
  font-size: 0.84375rem;
  font-weight: 500;
  color: var(--color-text-2);
}
.lib-empty-sub {
  margin: 6px 0 0;
  font-size: 0.75rem;
  color: var(--color-text-dim);
}

/* The ··· sheet — everything one layer down. */
.lib-sheet-group + .lib-sheet-group {
  margin-top: 18px;
}
.lib-sheet-label {
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
  margin-bottom: 8px;
}
.lib-sheet-hint {
  margin: 0 0 8px;
  font-size: 0.75rem;
  color: var(--color-text-dim);
}
.lib-sheet-action {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin-top: 6px;
  padding: 10px 12px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.8125rem;
  cursor: pointer;
  text-align: left;
}
.lib-sheet-action:hover {
  border-color: var(--color-border-strong);
}
.lib-sheet-group select {
  width: 100%;
  font: inherit;
  font-size: 0.8125rem;
  padding: 9px 11px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  color: var(--color-text);
}
.lib-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: 0.85rem;
  cursor: pointer;
}
.played-badge {
  color: var(--color-primary-text);
  vertical-align: middle;
}
/* Flex only when visible — #s-feed { display:flex } beats .screen[hidden] on
 * specificity, which left the feed covering other screens after tab navigation. */
/* main's own bottom padding (--space-4) is dropped while Feed is open — the
 * card stack and the Up Next list below it already fill #s-feed exactly (see
 * #feed-body), so that padding just left a strip of bare background between
 * the list and the docked mini-player, before the list actually ran out.
 * feed-pane-h below is adjusted to match: only the top --space-4 remains. */
body:has(#s-feed:not([hidden])) main {
  padding-bottom: 0;
}
#s-feed:not([hidden]) {
  touch-action: none;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  --feed-pane-h: calc(
    100dvh - var(--topbar-h) - env(safe-area-inset-top, 0px) - var(--tabbar-h) -
      env(safe-area-inset-bottom, 0px) - var(--space-4)
  );
  height: var(--feed-pane-h);
  max-height: var(--feed-pane-h);
}
#feed-body {
  touch-action: none;
  overscroll-behavior: contain;
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
body:has(.snd-mini:not([hidden])) #s-feed,
body:has(.tts-mini:not([hidden])) #s-feed,
body:has(.hp-mini:not([hidden])) #s-feed {
  --feed-pane-h: calc(
    100dvh - var(--topbar-h) - env(safe-area-inset-top, 0px) - var(--tabbar-h) -
      env(safe-area-inset-bottom, 0px) - var(--space-4) - 58px
  );
}
/* The stack is the flex item and owns the sizing; both the active card and
 * the one peeking out behind it are absolutely positioned to fill it, so
 * they can overlap instead of stacking in normal flow. */
.feed-stack {
  position: relative;
  flex: 1;
  min-height: 0;
  max-height: 100%;
  width: 100%;
  max-width: 32rem;
  margin: 0 auto 20px;
}
/* The same swipe-to-decide card stack, reused for a single playlist's own
 * "Feed" view — but #playlist-feed sits in ordinary scrolling flow inside
 * .screen-scroll, not in #s-feed's flex column with an explicit height. Without
 * a height of its own here, flex:1 above has no flex parent to fill against,
 * .feed-stack collapses to 0px, and the absolutely-positioned .feed-card
 * (inset:0 relative to it) collapses right along with it — cards were
 * rendering, just at zero height, indistinguishable from tapping Feed doing
 * nothing at all. */
#playlist-feed .feed-stack {
  flex: none;
  height: min(70vh, 34rem);
  max-height: none;
}
.feed-card {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
/* The rest of the review queue, below the card stack — what's coming up as
 * you swipe, so the deck reads as a real list you're working through rather
 * than a bottomless stack. #s-feed only: the stack's flex:1 above shrinks to
 * share space with this instead of filling #feed-body outright. Fixed max-
 * height rather than flex-basis so it always reads as "a few rows, scroll
 * for the rest" regardless of how long the queue actually is. */
.feed-upcoming {
  flex: 0 0 auto;
  width: 100%;
  max-width: 32rem;
  max-height: 9rem;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
  margin: 0 auto;
  border-top: 1px solid var(--color-border);
}
.feed-upcoming-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-faint);
  padding: var(--space-2) var(--space-1) var(--space-1);
}
.feed-upcoming-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-1);
  border-bottom: 1px solid var(--color-divider);
}
.feed-upcoming-row:last-child {
  border-bottom: 0;
}
.feed-upcoming-art {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
  background: var(--color-surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-faint);
}
.feed-upcoming-meta {
  flex: 1;
  min-width: 0;
}
.feed-upcoming-meta h4 {
  margin: 0;
  font-size: 0.82rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.feed-upcoming-sub {
  font-size: 0.72rem;
  color: var(--color-text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.feed-upcoming-more {
  padding: var(--space-2) var(--space-1);
  font-size: 0.78rem;
  color: var(--color-text-faint);
  text-align: center;
}
.feed-card-top {
  z-index: var(--z-overlay);
}
.feed-card-peek {
  z-index: var(--z-content);
  pointer-events: none;
  transform: scale(0.92) translateY(10px);
  opacity: 0.6;
  transition:
    transform 0.25s ease,
    opacity 0.25s ease;
}
/* The swipe ladder's live label (§22, answer 2). One element per side that
 * changes as the card travels, rather than four fixed hints — the rung under
 * the thumb IS the verdict, so the label has to be able to say three
 * different things on one side. */
.feed-card-rung {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: var(--z-overlay);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-shadow: 0 1px 10px currentColor;
  white-space: nowrap;
}
.feed-card-rung-right {
  right: var(--space-4);
}
.feed-card-rung-left {
  left: var(--space-4);
}
/* The vertical pair centre horizontally instead, and override the base rule's
 * vertical centring — a label sitting in the middle of the card is ambiguous
 * about which way you are going, so each sits at the edge it commits to. */
.feed-card-rung-up,
.feed-card-rung-down {
  left: 50%;
  transform: translateX(-50%);
}
.feed-card-rung-up {
  top: var(--space-4);
}
.feed-card-rung-down {
  top: auto;
  bottom: var(--space-4);
}
@media (prefers-reduced-motion: reduce) {
  .feed-card-rung {
    transition: none;
  }
}
.feed-card-content {
  position: relative;
  z-index: var(--z-content);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg, 12px);
  height: 100%;
  display: flex;
  flex-direction: column;
  cursor: grab;
  user-select: none;
  overflow: hidden;
  touch-action: none;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}
.feed-card-content:active {
  cursor: grabbing;
}
.feed-card-art {
  width: 100%;
  height: auto;
  max-height: min(26vh, 200px);
  max-height: min(26dvh, 200px);
  display: block;
  flex-shrink: 0;
  background: var(--color-surface-2);
  /* Initial aspect-ratio prevents ~0 height layout shift before the image
   * loads; _fitFeedCardArt overrides with the real ratio once available.
   * When max-height clamps the box to something narrower than the image's
   * own ratio, cover crops the overflow instead of stretching pixels. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.feed-card-art-fallback {
  width: 100%;
  max-height: min(26vh, 200px);
  max-height: min(26dvh, 200px);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-surface-2);
  color: var(--color-text-faint);
  flex-shrink: 0;
}
.feed-card-body {
  flex: 1;
  overflow: hidden;
  padding: var(--space-3);
  min-height: 0;
  touch-action: none;
}
.feed-card-kind-row {
  display: flex;
  align-items: center;
  gap: 0.5em;
  margin-bottom: var(--space-1);
}
.feed-card-kind {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-primary-text);
}
.feed-card-likely-skip {
  font-size: 0.7rem;
  color: var(--color-text-faint);
  background: var(--color-surface-2);
  border-radius: var(--radius-sm);
  padding: 0.1em 0.5em;
}
.feed-card-body h3 {
  margin: 0 0 var(--space-1);
  font-size: 1.15rem;
  line-height: 1.3;
}
.feed-card-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3em;
  font-size: 0.82rem;
  color: var(--color-text-faint);
  margin-bottom: var(--space-2);
}
.feed-card-dot {
  opacity: 0.5;
}
.feed-card-desc {
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--color-text);
  margin: 0;
}
.feed-card-desc a {
  word-break: break-word;
}
.feed-card-guide {
  position: absolute;
  bottom: var(--space-3);
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-overlay);
  text-align: center;
  font-size: 0.8rem;
  color: var(--color-text-faint);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md, 8px);
  padding: var(--space-2) var(--space-3);
  pointer-events: none;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.feed-card-guide p {
  margin: 0;
}
.feed-card-guide p + p {
  margin-top: 0.3em;
}
.feed-card-guide-arrow {
  font-weight: 700;
  color: var(--color-primary-text);
}
/* Also used by the track-detail sheet's Play / Add to playlist buttons. */
/* Named for the feed card, used only by the track-detail sheet — the feed's
 * own toolbar is .feed-card-tool-btn. Kept as-is rather than renamed because
 * the name is load-bearing in nothing and the churn would touch every call
 * site for no behavioural gain.
 *
 * Icon over label, on a fixed track, because this row is a grid of up to nine
 * peers and not a sentence. Side-by-side icon and text gave every button a
 * different width, so the row read as ragged pills, wrapped mid-phrase
 * ("Play / next"), and clipped the last one off the right edge. Stacking puts
 * every label on the same baseline and makes the row scannable in one pass. */
.feed-card-action-btn {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 0.35em;
  /* A floor rather than a fixed width: the label decides, and a long one
   * ("Add to playlist") widens its own cell instead of wrapping to two lines
   * while its neighbours sit half-empty. */
  min-width: 4.75rem;
  padding: 0.6em 0.5em;
  border-radius: var(--btn-radius);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-text);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.25;
  text-align: center;
}
.feed-card-action-btn:hover {
  border-color: var(--color-primary);
}
/* The primary action for what you are looking at — Read on an article. It
 * leads the row, so it should look like the answer rather than the first of
 * nine equals. */
.feed-card-action-btn.is-primary {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
}
/* A real, always-present alternative to the swipe gestures above — those are
 * pointer/touch-only, so without this a keyboard or switch-access user has no
 * way to act on a card at all. Laid over the art itself (not a row below the
 * card) so the card's height goes to the description instead. */
.feed-card-art-wrap {
  position: relative;
  flex-shrink: 0;
}
.feed-card-watch-btn {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 60px;
  height: 60px;
  border-radius: var(--btn-radius);
  border: 2px solid rgba(255, 255, 255, 0.85);
  background: rgba(4, 5, 10, 0.55);
  color: #fff;
  display: grid;
  place-items: center;
  backdrop-filter: blur(2px);
}
.feed-card-watch-btn svg {
  /* Optically centering a play triangle means nudging it right of true
   * center — same reason play buttons everywhere do this. */
  transform: translateX(2px);
}
.feed-card-watch-btn:hover {
  border-color: #fff;
  background: rgba(4, 5, 10, 0.75);
}
.feed-card-inline-video {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  background: #000;
}
.feed-card-inline-close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  width: 30px;
  height: 30px;
  border-radius: var(--btn-radius);
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(4, 5, 10, 0.6);
  color: #fff;
  display: grid;
  place-items: center;
  backdrop-filter: blur(4px);
}
.feed-card-toolbar {
  flex-shrink: 0;
  display: flex;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-bottom: 1px solid var(--color-border);
}
/* Icon-only circular buttons had no visible hint of what they did — title
 * tooltips don't exist on touch. A tiny label under the icon costs little
 * height (still far more compact than the old below-the-card pill row) and
 * means you don't have to learn the icon vocabulary by trial and error. */
/* Three equal buttons, committing the same three verdicts the gestures do
 * (§3). Play next is the primary because it is the one that acts on the
 * decision rather than deferring it. */
.feed-card-tool-btn {
  flex: 1;
  min-width: 0;
  padding: 13px 0;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  background: none;
  color: var(--color-text-2);
  font-size: 0.8125rem;
  font-weight: 500;
}
.feed-card-tool-btn:hover {
  border-color: var(--color-primary);
}
.feed-card-tool-label {
  font-size: 0.62rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.01em;
}
.feed-card-tool-primary {
  color: var(--color-primary-text, var(--color-primary));
  border-color: var(--color-primary);
  background: var(--color-primary-highlight);
}
.feed-card-tool-keep:hover {
  border-color: #4fbfa0;
  color: #4fbfa0;
}
.feed-card-tool-skip:hover {
  border-color: #e5533c;
  color: #e5533c;
}
/* The fourth button, not a fourth verdict — Discard/Play next/Keep are the
 * three the gestures also commit (§3), and adding a playlist to that set
 * would suggest it is a peer decision rather than a separate action. Fixed
 * width and icon-only keeps it visually secondary without hiding it behind
 * a tap into the detail sheet, which is where this lived before and the
 * one thing about it nobody could find twice. */
.feed-card-tool-playlist {
  flex: 0 0 auto;
  width: 42px;
  display: grid;
  place-items: center;
}

/* The fifth rung of the description ladder: nothing to say, so the card ends
 * after the meta line and the art takes the room the body would have had. */
.feed-card-short .feed-card-art,
.feed-card-short .feed-card-art-fallback {
  max-height: none;
}
.feed-card-short .feed-card-body {
  padding-bottom: var(--space-3);
}

/* Rungs two and three: chapters, or one transcript line marked as a quote. */
.feed-card-chapters {
  margin-top: var(--space-2);
}
.feed-card-chapter {
  display: flex;
  gap: 10px;
  padding: 3px 0;
  font-size: 0.6875rem;
}
.feed-card-chapter-at {
  flex: none;
  min-width: 42px;
  color: var(--color-text-dimmer);
  font-variant-numeric: tabular-nums;
}
.feed-card-chapter-title {
  color: var(--color-text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.feed-card-quote {
  margin: var(--space-2) 0 0;
  padding-left: 10px;
  border-left: 2px solid var(--color-primary);
  font-size: 0.78125rem;
  line-height: 1.55;
  color: var(--color-text-3);
}
.feed-card-more {
  margin: var(--space-2) 0 0;
  font-size: 0.78125rem;
  color: var(--color-text-faint);
}
.feed-empty {
  text-align: center;
  padding: var(--space-6) var(--space-3);
  color: var(--color-text-faint);
}
/* ── Track detail sheet ──────────────────────────────────────────────────
 * Same bottom-sheet pattern as #pl-picker, and deliberately body-level for
 * the same reason: nesting it inside #s-feed would inherit that screen's
 * touch-action:none, and the show notes inside need to scroll normally. */
.track-detail-sheet {
  position: fixed;
  inset: 0;
  /* --z-player-sheet (1100), not --z-sheet (92) — same reasoning as
   * #pl-picker above: the player's More button (data-act="more" →
   * App.openPlayerMore → this sheet) opens it while the shell (z-index
   * 1005) is still up, and at --z-sheet it rendered invisibly behind it. */
  z-index: var(--z-player-sheet);
  background: rgba(8, 9, 16, 0.55);
  display: flex;
  align-items: flex-end;
}
.track-detail-sheet[hidden] {
  display: none;
}
.track-detail-backdrop {
  position: absolute;
  inset: 0;
}
.track-detail-panel {
  position: relative;
  width: 100%;
  max-height: 76vh;
  background: var(--color-surface);
  border-top-left-radius: var(--radius-xl);
  border-top-right-radius: var(--radius-xl);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.track-detail-close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: var(--z-content);
  background: var(--color-surface-2);
  border: 0;
  border-radius: var(--btn-radius);
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  color: var(--color-text-faint);
}
.track-detail-scroll {
  overflow-y: auto;
  padding: var(--space-4);
  padding-top: calc(var(--space-4) + 28px);
  padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
}
.track-detail-scroll h2 {
  margin: 0 0 var(--space-1);
  font-size: 1.15rem;
}
.track-detail-meta {
  font-size: 0.82rem;
  color: var(--color-text-faint);
  margin-bottom: var(--space-3);
}
.track-detail-actions {
  display: flex;
  /* Wrap, because this row is not a fixed set: it carries up to eight
   * text-labelled buttons ("Play last", "Add to playlist", …) and grows again
   * whenever a contextual one applies. Without wrapping they compress into
   * each other on a phone until the labels are unreadable — the row was laid
   * out for the three or four it had when it was written. */
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

/* Persona sheet — same bottom-sheet pattern as .track-detail-sheet above,
 * just under its own class names since the two are unrelated overlays that
 * can in principle both exist in the DOM at once. */
.persona-sheet {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  background: rgba(8, 9, 16, 0.55);
  display: flex;
  align-items: flex-end;
}
.persona-sheet[hidden] {
  display: none;
}
.persona-sheet-backdrop {
  position: absolute;
  inset: 0;
}
.persona-sheet-panel {
  position: relative;
  width: 100%;
  max-height: 76vh;
  background: var(--color-surface);
  border-top-left-radius: var(--radius-xl);
  border-top-right-radius: var(--radius-xl);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.persona-sheet-close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: var(--z-content);
  background: var(--color-surface-2);
  border: 0;
  border-radius: var(--btn-radius);
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  color: var(--color-text-faint);
}
.persona-sheet-scroll {
  overflow-y: auto;
  padding: var(--space-4);
  padding-top: calc(var(--space-4) + 28px);
  padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
}
.persona-sheet-scroll h2 {
  margin: 0 0 var(--space-3);
  font-size: 1.15rem;
}
/* .persona-row is a plain div, not a button: it holds .persona-row-main (the
 * select action) and .persona-row-delete as SIBLINGS, deliberately, because
 * a <button> can't validly contain another <button> — nesting them left the
 * delete button breaking out of the row's flex layout entirely, hoisted out
 * by the browser's own HTML parsing rather than styled wrong. */
.persona-row {
  display: flex;
  align-items: center;
  width: 100%;
  border-top: 1px solid var(--color-divider);
}
.persona-row:first-child {
  border-top: 0;
}
.persona-row-main {
  display: flex;
  align-items: center;
  flex: 1;
  min-width: 0;
  gap: var(--space-3);
  padding: 0.7em 0.2em;
  border: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: 0.92rem;
  text-align: left;
  cursor: pointer;
}
.persona-row-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
  background: var(--color-text-dimmer, var(--color-text-faint));
}
.persona-row-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.persona-row-main.is-active {
  color: var(--color-primary-text);
}
.persona-row-delete {
  flex: none;
  background: none;
  border: 0;
  color: var(--color-text-faint);
  padding: 0.3em;
  margin-right: 0.2em;
  border-radius: var(--radius-sm);
}
.persona-row-delete:hover {
  color: var(--color-error);
}
.persona-row-new {
  color: var(--color-primary-text);
  font-weight: 500;
}

.persona-row-manage {
  flex: none;
  background: none;
  border: 0;
  color: var(--color-text-faint);
  padding: var(--space-2);
  cursor: pointer;
  border-radius: 4px;
}
.persona-row-manage:hover {
  color: var(--color-primary-text);
}
.persona-row-count {
  font-size: 0.75rem;
  color: var(--color-text-faint);
  margin-left: var(--space-1);
}
.persona-sources-back {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  background: none;
  border: 0;
  color: var(--color-text-faint);
  font-size: 0.9rem;
  padding: var(--space-2) 0;
  margin-bottom: var(--space-2);
  cursor: pointer;
}
.persona-sources-back:hover {
  color: var(--color-primary-text);
}
.persona-source-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--color-divider);
  cursor: pointer;
}
.persona-source-row:first-child {
  border-top: 0;
}
.persona-source-meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}
.persona-source-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.persona-source-sub {
  font-size: 0.75rem;
  color: var(--color-text-faint);
}

/* Feed is a fixed card stack, not a scrolling list — its own dedicated rules
 * above are a deliberate exception to the generic pattern below. */
body:has(#s-feed:not([hidden])) {
  overflow: hidden;
}

/* ── Scroll containment for every screen (systemic, not per-screen) ────────
 * A body-level scroll drags position:fixed elements (the tab bar, the docked
 * mini-player) out of place mid-gesture — a mobile WebKit bug. Feed and
 * Sounds were the first two screens fixed for it, each by hand, with their
 * own one-off CSS. That doesn't scale: every screen added since needed the
 * same fix re-derived, and any screen someone forgot to update kept the bug.
 *
 * This is keyed off the .screen / .screen-head / .screen-scroll convention
 * every screen in app.html follows (see the note above <main>): a fixed
 * .screen-head (back button, title) plus a .screen-scroll wrapping
 * everything else that should scroll together as one region. Any screen
 * shaped that way gets correct scroll containment for free — there is
 * nothing to add here when a new one shows up, and nothing to remember.
 * Feed is the one deliberate exception (:not(#s-feed) below); it keeps its
 * own rules above instead of this generic pattern. */
body:has(.screen:not([hidden]):not(#s-feed)) {
  overflow: hidden;
}
.screen:not([hidden]):not(#s-feed) {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  --screen-h: calc(
    100dvh - var(--topbar-h) - env(safe-area-inset-top, 0px) - var(--tabbar-h) -
      env(safe-area-inset-bottom, 0px)
  );
  height: var(--screen-h);
}
/* A docked mini-player sits on top of this same space — without this its
 * last rows would scroll in underneath it instead of clearing it. Matches
 * the 58px #s-feed already reserves for the same bar. */
body:has(.tts-mini:not([hidden])) .screen:not([hidden]):not(#s-feed),
body:has(.hp-mini:not([hidden])) .screen:not([hidden]):not(#s-feed),
body:has(.snd-mini:not([hidden])) .screen:not([hidden]):not(#s-feed) {
  --screen-h: calc(
    100dvh - var(--topbar-h) - env(safe-area-inset-top, 0px) - var(--tabbar-h) -
      env(safe-area-inset-bottom, 0px) - 58px
  );
}
.screen:not([hidden]) > .screen-head {
  flex-shrink: 0;
}
.screen:not([hidden]) > .screen-scroll {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  flex: 1;
  min-height: 0;
}

.feed-view-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  font-size: 0.8rem;
  padding: 0.3em 0.6em;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  color: var(--color-text);
  cursor: pointer;
}
.feed-view-toggle:hover {
  border-color: var(--color-primary);
}
.feed-view-toggle.active {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
}

/* Still reading: a horizontal scroller of spines, not the row list every
 * other section uses — a shelf reads as a shelf. */
.home-shelf {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding-bottom: 2px;
}
.home-spine {
  flex: none;
  width: 106px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: none;
  border: 0;
  color: inherit;
  text-align: left;
  font: inherit;
  cursor: pointer;
}
.home-spine-cover {
  width: 106px;
  height: 142px;
  border-radius: 3px;
  background: var(--color-surface-2);
  color: var(--color-primary-text);
  display: grid;
  place-items: center;
  overflow: hidden;
  /* The gutter-and-page illusion: a shadow along the spine edge, a hairline
   * highlight along the page edge. */
  box-shadow:
    inset -12px 0 18px -14px #000,
    inset 3px 0 0 #ffffff1f;
}
.home-spine-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.home-spine-title {
  font-size: 0.75rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-spine-pos {
  font-size: 0.65625rem;
  color: var(--color-text-dim, var(--color-text-faint));
}

.home-today {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}
.home-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 12px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-2, var(--color-text));
  font-size: 0.78125rem;
}
.home-chip:hover {
  border-color: var(--color-primary);
}
.home-chip-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--color-primary);
  flex: none;
}
.home-top-source {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 9px 0;
  border-top: 1px solid var(--color-divider);
  cursor: pointer;
}
.home-top-source:first-child {
  border-top: 0;
}
.home-top-count {
  font-family: var(--font-display);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--color-primary-text);
  min-width: 22px;
}
.home-top-title {
  flex: 1;
  min-width: 0;
  color: var(--color-text);
  font-size: 0.8125rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-top-kind {
  flex: none;
  font-size: 0.65625rem;
  color: var(--color-text-dim, var(--color-text-faint));
  text-align: right;
}
.home-empty-actions {
  display: flex;
  gap: var(--space-2);
  justify-content: center;
  flex-wrap: wrap;
  margin-top: var(--space-4);
}
#home-body .empty button {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  background: var(--color-surface-2);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 0.55em 0.9em;
}
#home-body .empty button.primary {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #1c1810;
  font-weight: 600;
}

/* ── Desktop layout ──────────────────────────────────────────────────────── */
/*
 * The whole app is phone-first, and on a 27" monitor that reads as a phone
 * screenshot pinned to the middle of the desk: a bottom tab bar the mouse has
 * to travel to, and two thirds of the window unused.
 *
 * There is exactly ONE selector for the wide layout — :root[data-layout='desktop']
 * — and JavaScript decides when it applies. The alternative, duplicating every
 * rule under both a media query and a manual override, is how these blocks drift
 * apart: someone fixes the sidebar under the media query and the person who
 * forced the layout by hand still sees the bug. app.js resolves the stored
 * preference ('auto' | 'phone' | 'desktop') against a matchMedia listener and
 * writes the ANSWER onto <html>, the same way the theme works.
 */

:root[data-layout='desktop'] {
  /* A starting point, not a fixed width — the rail is draggable (.rail-resizer)
     and a saved width overrides this, applied before first paint in app.html.
     Narrower than it was: the labels fit comfortably by ~200px and the rest was
     coming out of the content beside it. */
  --sidebar-w: 12.5rem;
  --tabbar-h: 0px;
  --maxw: 62rem;
}

/* §20's two wide tiers. The layout *mode* stays JS-resolved — see the note
 * above; these only size what that mode already decided, which is why they
 * can be media queries without reintroducing the drift that note warns
 * about.
 *
 * The caps are the point: at 62rem a single-column screen ran to 992px on a
 * 1440 window, which is a title measured in ninety characters and a line
 * nobody's eye tracks back from. §20 caps content at 720 and 760 while
 * letting the sidebar take the extra room. */
@media (min-width: 1024px) and (max-width: 1279px) {
  :root[data-layout='desktop'] {
    --sidebar-w: 14.25rem; /* 228px */
    --maxw: 45rem; /* 720px */
  }
}
@media (min-width: 1280px) {
  :root[data-layout='desktop'] {
    --sidebar-w: 15.75rem; /* 252px */
    --maxw: 47.5rem; /* 760px */
  }
  /* The kit docks its player bar to the viewport. Left, not just bottom: the
     bar would otherwise run under the sidebar and swallow its first control. */
  --pk-dock-bottom: 0px;
  --pk-dock-left: var(--sidebar-w);
}

:root[data-layout='desktop'] body {
  padding-bottom: 0;
  padding-left: var(--sidebar-w);
}
/* The mini player is fixed over the content, so the last rows of a long list
   still need room underneath it — there is no tab bar left to supply it. */
:root[data-layout='desktop'] body:has(.tts-mini:not([hidden])),
:root[data-layout='desktop'] body:has(.hp-mini:not([hidden])),
:root[data-layout='desktop'] body:has(.snd-mini:not([hidden])) {
  padding-bottom: 64px;
}

/* The tab bar becomes a rail. Same buttons, same handlers, same accessible
   names — only the direction changes. */
:root[data-layout='desktop'] .tabs {
  top: 0;
  right: auto;
  width: var(--sidebar-w);
  grid-auto-flow: row;
  grid-auto-columns: none;
  align-content: start;
  gap: 2px;
  padding: var(--space-3) var(--space-2);
  border-top: 0;
  border-right: 1px solid var(--color-divider);
  background: var(--color-surface);
  backdrop-filter: none;
}
/* The drag handle for that width. Sits on the rail's right edge, straddling
   the border so the whole seam is grabbable. .tabs is already position:fixed,
   which is the containing block this anchors to. Phone layout has no rail, so
   it stays display:none there and in the narrow fallback below. */
.rail-resizer {
  display: none;
}
:root[data-layout='desktop'] .rail-resizer {
  display: block;
  position: absolute;
  top: 0;
  right: -4px;
  width: 8px;
  height: 100%;
  cursor: col-resize;
  z-index: var(--z-dropdown);
  /* Without this the browser claims the gesture for scrolling on a touch
     screen and the pointermove stream stops partway through a drag. */
  touch-action: none;
}
:root[data-layout='desktop'] .rail-resizer:hover,
:root[data-layout='desktop'] .rail-resizer.is-dragging {
  background: var(--color-primary);
  opacity: 0.5;
}
:root[data-layout='desktop'] .tab {
  flex-direction: row;
  justify-content: flex-start;
  gap: var(--space-3);
  padding: 0.6rem 0.75rem;
  border-radius: var(--radius-md);
  font-size: 0.92rem;
  letter-spacing: 0;
}
:root[data-layout='desktop'] .tab > span:last-child {
  font-size: 0.92rem;
}
:root[data-layout='desktop'] .tab:hover {
  background: var(--color-surface-2);
  color: var(--color-text);
}
:root[data-layout='desktop'] .tab.is-on {
  background: var(--color-primary-highlight);
}

/* The brand moves into the rail, where a desktop app puts it. */
.rail-brand {
  display: none;
}
:root[data-layout='desktop'] .rail-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3) var(--space-4);
  color: var(--color-primary-text);
  font-weight: 600;
}
:root[data-layout='desktop'] .topbar .brand {
  display: none;
}
:root[data-layout='desktop'] .topbar {
  justify-content: flex-end;
}

/* What the extra width is actually for. Rows that are a fixed-height strip —
   a cover, a title, a few buttons — read better two-up than stretched to 62rem
   with an acre of blank between the title and its controls. */
:root[data-layout='desktop'] #home-new,
:root[data-layout='desktop'] #library-list,
:root[data-layout='desktop'] #sources-list,
:root[data-layout='desktop'] #playlists-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-5);
  align-content: start;
}
/* A section heading introduces the rows under it, so it must not sit in a
   column beside them. */
:root[data-layout='desktop'] #sources-list .section-head,
:root[data-layout='desktop'] #library-list .section-head,
:root[data-layout='desktop'] #playlists-list .section-head,
:root[data-layout='desktop'] #library-list .empty,
:root[data-layout='desktop'] #sources-list .empty,
:root[data-layout='desktop'] #playlists-list .empty,
:root[data-layout='desktop'] #sources-list .lib-head,
:root[data-layout='desktop'] #sources-list .lib-controls,
:root[data-layout='desktop'] #sources-list .lib-chips,
:root[data-layout='desktop'] #sources-list .lib-collections,
:root[data-layout='desktop'] #sources-list .lib-list,
:root[data-layout='desktop'] #sources-list .lib-grid,
:root[data-layout='desktop'] #sources-list .lib-empty,
:root[data-layout='desktop'] #sources-list .batch-tag-bar {
  grid-column: 1 / -1;
}

:root[data-layout='desktop'] #toast {
  left: calc(50% + var(--sidebar-w) / 2);
  bottom: calc(var(--space-4) + 64px);
}

/* The rail is 15rem of a window that might be 60rem wide. Below that the two
   columns and the sidebar stop being an improvement, so a forced desktop
   layout still folds back rather than becoming unusable — the preference says
   "prefer wide", not "wedge me into wide at 500px". */
@media (max-width: 52rem) {
  :root[data-layout='desktop'] {
    --sidebar-w: 0px !important;
    /* Static fallback only — syncTabbarHeight() overrides both the moment
       .tabs folds back to a bottom bar here. See the :root block's note. */
    --tabbar-h: 54px;
    --pk-dock-bottom: 54px;
    --pk-dock-left: 0px;
  }
  :root[data-layout='desktop'] body {
    padding-left: 0;
    padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
  }
  :root[data-layout='desktop'] .tabs {
    top: auto;
    right: 0;
    width: auto;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    padding: 0 0 env(safe-area-inset-bottom, 0px);
    border-top: 1px solid var(--color-divider);
    border-right: 0;
  }
  :root[data-layout='desktop'] .tab {
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    padding: 0.6rem 0.1rem;
    font-size: 0.62rem;
  }
  :root[data-layout='desktop'] .tab > span:last-child {
    font-size: 0.62rem;
  }
  :root[data-layout='desktop'] .tab.is-on {
    background: none;
  }
  :root[data-layout='desktop'] .rail-brand {
    display: none;
  }
  /* The rail has folded back to a bottom bar here — there is no vertical edge
     left to drag, and --sidebar-w is forced to 0 above. */
  :root[data-layout='desktop'] .rail-resizer {
    display: none;
  }
  :root[data-layout='desktop'] .topbar .brand {
    display: flex;
  }
  :root[data-layout='desktop'] .topbar {
    justify-content: space-between;
  }
  :root[data-layout='desktop'] #home-new,
  :root[data-layout='desktop'] #library-list,
  :root[data-layout='desktop'] #sources-list,
  :root[data-layout='desktop'] #playlists-list {
    display: block;
  }
  :root[data-layout='desktop'] #toast {
    left: 50%;
    bottom: calc(var(--tabbar-h) + var(--space-4) + env(safe-area-inset-bottom, 0px));
  }
}

/* ── Toast ───────────────────────────────────────────────────────────────── */

#toast {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--tabbar-h) + var(--space-4) + env(safe-area-inset-bottom, 0px));
  /* A toast fires from anywhere — including actions taken from inside the
   * unified player shell (.ps, kit/ui/player-shell.css, z-index 1005) and the
   * sheets that open over it (up to 1150, the queue/chapters picker) — so it
   * has to sit above every one of them or it renders invisibly behind
   * whatever's currently open, silently dropping the confirmation (or
   * error) it exists to show. */
  z-index: var(--z-toast);
  max-width: min(92vw, 30rem);
  display: flex;
  align-items: center;
  gap: 0.5em;
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  padding: 0.5em 0.8em;
  border-radius: var(--radius-sm, 6px);
  font-size: 0.88rem;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  white-space: nowrap;
}
#toast[hidden] {
  display: none;
}
#reachability-banner {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  top: calc(env(safe-area-inset-top, 0px) + var(--space-2));
  z-index: var(--z-toast);
  max-width: min(92vw, 30rem);
  background: rgba(180, 40, 40, 0.95);
  color: #fff;
  border-radius: var(--radius-sm, 6px);
  padding: 0.5em 0.8em;
  font-size: 0.86rem;
  font-weight: 600;
  text-align: center;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
#reachability-banner[hidden] {
  display: none;
}
.toast-action {
  flex: none;
  padding: 0.25em 0.6em;
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-sm, 6px);
  background: transparent;
  color: var(--color-primary-text);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.toast-action:hover {
  background: var(--color-primary);
  color: var(--color-on-primary);
}

/* Persistent update prompt — unlike #toast above, this doesn't auto-dismiss;
 * refreshing is a real choice, not an FYI. */
.sw-update-toast {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--tabbar-h) + var(--space-4) + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-sw-toast);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: min(92vw, 22rem);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.88rem;
  font-weight: 500;
  padding: 0.8em 0.7em 0.8em 1em;
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-primary);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.28);
}
.sw-update-toast button {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--color-on-primary);
  background: var(--color-primary);
  border: 0;
  border-radius: var(--radius-md);
  padding: 0.5em 1em;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── Background sound ─────────────────────────────────────────────────────── */

.reader-ambience {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin: 0 0 var(--space-5);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-divider);
}
.reader-ambience:empty {
  display: none;
}
.amb-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  border-radius: var(--btn-radius);
  padding: 0.4em 0.9em;
  font-size: 0.85rem;
}
.amb-btn.is-on {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
}
.amb-btn:hover {
  border-color: var(--color-primary);
}
.amb-note {
  font-size: 0.78rem;
  color: var(--color-text-faint);
}
.amb-clear {
  background: none;
  border: 0;
  color: var(--color-text-faint);
  font-size: 0.78rem;
  text-decoration: underline;
}

.switch {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  font-size: 0.92rem;
  line-height: 1.5;
  cursor: pointer;
  /* The label IS the target — clicking it toggles the input — and it was the
   * height of one line of text. The padding is the tap area; .radio already
   * had it, .switch never did. */
  padding: 6px 0;
  min-height: 44px;
  align-content: center;
}
.switch input,
.radio input[type='radio'] {
  /* Rendered at the UA's 13×16 default, which is a small thing to aim at even
   * with the label around it — and the only visual confirmation of the state
   * these controls have. */
  width: 20px;
  height: 20px;
  flex: none;
  accent-color: var(--color-primary);
}
.switch input {
  margin-top: 0.15em;
}
.slider-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
  font-size: 0.88rem;
}
.slider-row.is-off {
  opacity: 0.5;
}
.slider-row input[type='range'] {
  flex: 1;
}
.slider-row output {
  min-width: 3.2em;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-faint);
}

/* The theme picker in Settings. */
.theme-row {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
  flex-wrap: wrap;
}
.theme-opt {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.5em 0.9em;
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  background: var(--color-surface-2);
  color: var(--color-text);
  font-size: 0.88rem;
}
.theme-opt.is-on {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
  font-weight: 600;
}

/* The narration voice picker in Settings (mounted by kit's renderVoicePicker). */
.voice-picker-row {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
  flex-wrap: wrap;
}
.voice-picker-select {
  flex: 1 1 14rem;
  min-width: 0;
}
.voice-picker-preview {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  white-space: nowrap;
}

/* ── Notes and journal ──────────────────────────────────────────────────── */

/* ── Notes groups ─────────────────────────────────────────────────────────
 *
 * Clips, Highlights and Journal are three kinds of thing you kept, not three
 * places to go — so they stack as collapsible groups rather than hiding two
 * behind a tab strip while you guess which one holds what you half-remember
 * saving. The header is the control; the chevron only reports. */
/* ── Notes: one list, filtered by chips (handoff §5, reference `7a`) ─────
 *
 * Clips, highlights and journal entries all live here, because they are all
 * the same thing: something you wrote about something you played. Three
 * collapsed group headers made you guess which one held the thing you
 * half-remembered saving, and hid the other two while you guessed.
 */
.notes-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 14px 0 10px;
}
.notes-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.375rem;
  font-weight: 500;
  letter-spacing: -0.015em;
}
.notes-count {
  flex: 1;
  font-size: 0.71875rem;
  color: var(--color-text-faint);
}
/* Square and in the header, never a floating action button — it must not sit
 * on top of the list. The same action registers in the omnibar as New note. */
.notes-new {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 11px 7px 9px;
  border: 0;
  border-radius: 6px;
  background: var(--color-primary);
  color: var(--color-on-primary);
  font: inherit;
  font-size: 0.78125rem;
  font-weight: 500;
  cursor: pointer;
}
/* .help-search (§19) shares this rather than duplicating it — but as its own
 * class, not a second use of .notes-search: both screens stay mounted with
 * `hidden` rather than removed, so an `.notes-search input` selector reaching
 * for one field started matching both and broke on the ambiguity — a real
 * failure this shared-selector form exists to prevent happening again. */
.notes-search,
.help-search {
  display: flex;
  align-items: center;
  gap: 9px;
  height: 40px;
  padding: 0 11px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  color: var(--color-text-dim);
}
.notes-search:focus-within,
.help-search:focus-within {
  border-color: var(--color-primary);
}
.notes-search input,
.help-search input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.8125rem;
  border: 0;
  background: none;
  color: var(--color-text);
  padding: 0;
}
.notes-search input:focus,
.help-search input:focus {
  outline: none;
}
.notes-chips {
  display: flex;
  gap: 6px;
  margin-top: 10px;
  overflow-x: auto;
  scrollbar-width: none;
}
.notes-chips::-webkit-scrollbar {
  display: none;
}
.notes-chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-2);
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}
.notes-chip.is-on {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-primary);
}
.notes-chip-count {
  font-size: 0.625rem;
  font-weight: 600;
  opacity: 0.65;
  font-variant-numeric: tabular-nums;
}

.notes-cards {
  margin-top: 12px;
}
/* The one place kind colour touches a note: a 3px stripe down its left edge,
 * the same hue the player's stage badge and a list row's dot use. It never
 * colours the text. */
.note-card {
  padding: 11px 12px;
  margin-top: 8px;
  background: var(--color-surface);
  border: 1px solid var(--color-surface-offset, var(--color-border));
  border-left: 3px solid var(--note-kind, transparent);
  border-radius: 4px;
  cursor: pointer;
}
.note-card:hover {
  border-color: var(--color-border-strong);
  border-left-color: var(--note-kind, transparent);
}
.note-card-top {
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.note-card-title {
  flex: 1;
  margin: 0;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text);
}
.note-card-kind {
  flex: none;
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* The kind colour is the card's LEFT BORDER, which is where the glance-level
   * signal lives (see KIND_COLOUR in notes-ui.js). Reusing that tint on a
   * 10.5px uppercase label put it at 3.2:1 — and for a note with no source it
   * fell through to a hardcoded grey inlined from JS, which no stylesheet can
   * reach. The label is text and is coloured as text; the border still says
   * what kind it is. */
  color: var(--color-text-2);
}
.note-card-body {
  margin: 4px 0 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--color-text-3);
}
.note-card-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 0.65625rem;
  color: var(--color-text-dim);
}
.note-card-lead {
  font-variant-numeric: tabular-nums;
}
.note-card-source {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.note-card-checks {
  flex: none;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--color-surface-2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.notes-empty {
  padding: 28px 14px;
  text-align: center;
  font-size: 0.8125rem;
  color: var(--color-text-dim);
}

/* ── The Journal calendar (§11) ─────────────────────────────────────────── */

.cal {
  margin-top: 18px;
  background: var(--color-surface);
  border: 1px solid var(--color-surface-offset, var(--color-border));
  border-radius: 6px;
  overflow: hidden;
}
.cal-head {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 11px 12px;
  border: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.cal-glyph {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 4px;
  border: 1px solid var(--color-border);
  color: var(--color-text-faint);
}
.cal-head-text {
  flex: 1;
  min-width: 0;
}
.cal-title {
  display: block;
  font-family: var(--font-display);
  font-size: 0.90625rem;
  font-weight: 500;
}
.cal-count {
  display: block;
  font-size: 0.65625rem;
  color: var(--color-text-dim);
}
.cal-chev {
  flex: none;
  display: inline-flex;
  color: var(--color-text-faint);
  transition: transform 0.18s ease;
}
.cal.is-open .cal-chev {
  transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
  .cal-chev {
    transition: none;
  }
}
.cal-body {
  padding: 0 12px 12px;
}
.cal-months {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding-bottom: 8px;
}
.cal-month-label {
  font-family: var(--font-display);
  font-size: 0.875rem;
  font-weight: 500;
}
.cal-nav {
  /* 26px was a 26px thumb target. The icon stays small; the button around it
   * is what a finger has to hit. */
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--color-text-faint);
  cursor: pointer;
}
.cal-nav:hover {
  background: var(--color-surface-2);
  color: var(--color-text);
}
.cal-week,
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.cal-week span {
  font-size: 0.59375rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-align: center;
  color: var(--color-text-dimmer);
}
/* Leading blanks are transparent placeholders with no border, background or
 * hover. Rendered as empty buttons they pick up default button chrome and
 * read as broken. */
.cal-blank {
  aspect-ratio: 1;
}
.cal-day {
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--color-text-dimmer);
  cursor: pointer;
}
.cal-num {
  font-size: 0.71875rem;
  font-variant-numeric: tabular-nums;
}
.cal-dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: transparent;
}
.cal-day.has-entry {
  color: var(--color-text);
  font-weight: 500;
}
.cal-day.has-entry .cal-dot {
  background: var(--color-primary);
}
.cal-day.is-today {
  box-shadow: inset 0 0 0 1px var(--color-border-strong);
}
.cal-day.is-on {
  background: var(--color-primary);
  color: var(--color-on-primary);
}
.cal-day.is-on .cal-dot {
  background: transparent;
}
.cal-entry {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--color-surface-offset, var(--color-border));
}
.cal-entry-label {
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}
.cal-entry-body {
  margin: 6px 0 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--color-text-2);
}
.cal-entry-open,
.cal-entry-empty {
  margin-top: 8px;
  /* A whole row of prose that is also a button, and it was 17px tall — the
   * text height, because there was no padding to press. */
  padding: 11px 0;
  min-height: 40px;
  text-align: left;
  border: 0;
  background: none;
  color: var(--color-text-dimmer);
  font: inherit;
  font-size: 0.78125rem;
  cursor: pointer;
  text-align: left;
}
.cal-entry-open {
  color: var(--color-primary-text);
}

/* ── The note editor ─────────────────────────────────────────────────────
 *
 * A textarea over plain markdown with a Write / Read switch. The toolbar
 * never takes focus (see note-editor.js), and the surface reserves room for
 * the on-screen keyboard through --keyboard-inset.
 */
.ned {
  --keyboard-inset: 0px;
  --keyboard-buffer: 0px;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.ned-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 0;
}
.ned-back {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  background: var(--color-surface-2);
  color: var(--color-text-2);
  cursor: pointer;
}
.ned-title {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 500;
  border: 0;
  background: none;
  color: var(--color-text);
  padding: 0;
}
.ned-title:focus {
  outline: none;
}
.ned-mode {
  flex: none;
  display: flex;
  gap: 3px;
  padding: 3px;
  border-radius: 6px;
  background: var(--color-surface-2);
}
.ned-mode-btn {
  padding: 5px 10px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--color-text-3);
  font: inherit;
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
}
.ned-mode-btn.is-on {
  background: var(--color-primary);
  color: var(--color-on-primary);
}

.ned-anchor {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  margin-bottom: 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-surface-offset, var(--color-border));
  border-radius: 7px;
}
.ned-anchor-art {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 4px;
  background: var(--color-surface-2);
  color: var(--color-text-faint);
}
.ned-anchor-text {
  flex: 1;
  min-width: 0;
  font-size: 0.75rem;
  color: var(--color-text-2);
}
.ned-anchor-time {
  color: var(--color-primary-text);
  font-variant-numeric: tabular-nums;
}
.ned-anchor-jump {
  flex: none;
  padding: 5px 9px;
  border: 1px solid var(--color-border-strong);
  border-radius: 5px;
  background: none;
  color: var(--color-text-2);
  font: inherit;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  cursor: pointer;
}
.ned-anchor-jump:hover {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
}

/* Scrolls rather than wrapping: a toolbar that changes height as the window
 * narrows moves the text under the caret. */
.ned-toolbar {
  display: flex;
  align-items: center;
  gap: 2px;
  padding-bottom: 8px;
  overflow-x: auto;
  scrollbar-width: none;
}
.ned-toolbar::-webkit-scrollbar {
  display: none;
}
.ned-tool {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--color-text-faint);
  cursor: pointer;
}
.ned-tool:hover,
.ned-tool:active {
  background: var(--color-surface-2);
  color: var(--color-text);
}
.ned-tool-div {
  flex: none;
  width: 1px;
  height: 18px;
  margin: 0 4px;
  background: var(--color-border);
}

.ned-surface {
  position: relative;
  flex: 1;
  min-height: 0;
  padding-bottom: calc(var(--keyboard-inset) + var(--keyboard-buffer));
}
.ned-body {
  width: 100%;
  min-height: 240px;
  font: inherit;
  font-size: 0.96875rem;
  line-height: 1.62;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text);
  resize: none;
  overflow: hidden;
}
.ned-body:focus {
  outline: none;
}
.ned-read {
  font-size: 0.96875rem;
  line-height: 1.62;
  color: var(--color-text-2);
}

/* The four-state checkbox. The tap target is the box alone — a tap on the
 * label is not a toggle, because toggling from a stray tap on a long line is
 * the single most annoying thing a checklist can do. */
.ned-task {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 4px 0;
  margin-left: calc(var(--depth, 0) * 18px);
}
.ned-check {
  flex: none;
  width: 17px;
  height: 17px;
  margin-top: 2px;
  display: grid;
  place-items: center;
  border: 1.5px solid var(--color-text-dimmer);
  border-radius: 3px;
  background: none;
  color: var(--color-bg);
  cursor: pointer;
  padding: 0;
}
.ned-task-doing .ned-check {
  background: var(--color-primary-highlight);
  border-color: var(--color-primary);
  color: var(--color-primary-text);
}
.ned-task-done .ned-check {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-primary);
}
.ned-task-dropped .ned-check {
  border-color: var(--color-border-strong);
}
.ned-task-done .ned-task-label {
  text-decoration: line-through;
  color: var(--color-text-dim);
}
.ned-task-dropped .ned-task-label {
  text-decoration: line-through;
  color: var(--color-text-dimmer);
}

/* One machine, three triggers: `/` commands, `[[` entities, `@` dates. */
.ned-menu {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--keyboard-inset) + 8px);
  z-index: var(--z-dropdown);
  max-height: 240px;
  overflow-y: auto;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border-strong);
  border-radius: 8px;
  box-shadow: 0 12px 30px -12px #000;
}
.ned-menu[hidden] {
  display: none;
}
.ned-menu-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  padding: 9px 12px;
  border: 0;
  background: none;
  color: var(--color-text-2);
  font: inherit;
  font-size: 0.8125rem;
  text-align: left;
  cursor: pointer;
}
.ned-menu-item.is-on {
  background: var(--color-primary-highlight);
  color: var(--color-text);
}
.ned-menu-label {
  flex: none;
}
.ned-menu-sub {
  flex: 1;
  font-size: 0.6875rem;
  color: var(--color-text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ned-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 14px 0;
}
.ned-delete {
  margin-left: auto;
  color: var(--color-error, #e2777a);
}

/* The journal is a writing surface first, so it gets the room and the line
   height of one rather than the density of a list. */
.journal-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.j-date {
  flex: 1;
  text-align: center;
  font-weight: 600;
}
.j-today {
  color: var(--color-primary-text);
  font-weight: 400;
  font-size: 0.82rem;
}
.j-nav {
  min-width: 44px;
  min-height: 44px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--btn-radius);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
}
.j-nav:disabled {
  opacity: 0.35;
}
.j-back-today {
  background: none;
  border: 0;
  color: var(--color-primary-text);
  padding: 0 0 var(--space-2);
  text-decoration: underline;
}
.journal-text,
.note-body {
  width: 100%;
  min-height: 240px;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.7;
  resize: vertical;
}
.journal-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-2);
  flex-wrap: wrap;
}
.journal-out {
  display: flex;
  gap: var(--space-2);
}
.journal-out button,
.notes-export button {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  border-radius: var(--radius-md);
  padding: 0.35rem 0.7rem;
  font-size: 0.82rem;
}
.j-past {
  display: flex;
  flex-direction: column;
}
.j-past button {
  text-align: left;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--color-divider);
  color: var(--color-text);
  padding: var(--space-3) 0;
  display: block;
  width: 100%;
}
.j-past-date {
  display: block;
  font-size: 0.82rem;
  color: var(--color-primary-text);
  margin-bottom: 2px;
}
.j-past-preview {
  display: block;
  font-size: 0.85rem;
  color: var(--color-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.notes-tools {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.notes-tools input {
  flex: 1;
}
.notes-source-filter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  background: var(--color-surface-2);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  font-size: 0.85rem;
  color: var(--color-text-faint);
}
.notes-folders {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.notes-folders button {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-faint);
  border-radius: var(--btn-radius);
  padding: 0.25rem 0.75rem;
  font-size: 0.82rem;
}
.notes-folders button.is-on {
  color: var(--color-text);
  border-color: var(--color-primary);
}
.note-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-divider);
  cursor: pointer;
}
/* Without min-width:0 a flex child refuses to shrink below its content, so the
   preview's ellipsis never engages and the line runs off the right edge. */
.note-meta {
  flex: 1;
  min-width: 0;
}
.note-row h3 {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
}
.note-sub {
  color: var(--color-text-faint);
  font-size: 0.8rem;
  margin-top: 0.1rem;
}
.note-preview {
  color: var(--color-text-faint);
  font-size: 0.85rem;
  margin-top: 0.25rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.clip-row {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
}
.clip-row:last-child {
  border-bottom: 0;
}
.clip-quote {
  font-family: Georgia, serif;
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--color-text);
  margin-bottom: var(--space-2);
}
.clip-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.clip-source {
  color: var(--color-text-faint);
  font-size: 0.8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.note-editor-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-3);
}
.note-editor-head button:not(.back) {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  border-radius: var(--radius-md);
  padding: 0.35rem 0.7rem;
  font-size: 0.82rem;
}
.note-title {
  font-size: 1.15rem;
  font-weight: 600;
  margin-bottom: var(--space-2);
}
.note-folder {
  margin-bottom: var(--space-3);
}
.note-preview-body {
  line-height: 1.7;
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  min-height: 240px;
}
.note-preview-body > :first-child {
  margin-top: 0;
}
.notes-export {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-divider);
  flex-wrap: wrap;
}

/* ── AI ─────────────────────────────────────────────────────────────────── */

.reader-chat {
  margin-bottom: var(--space-3);
}
.reader-chat:empty {
  display: none;
}
.ai-chat-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  border-radius: var(--btn-radius);
  padding: 0.4rem 0.9rem;
  font-size: 0.85rem;
}
.ai-chat-btn:hover {
  border-color: var(--color-primary);
}
.ai-field {
  display: block;
  margin-bottom: var(--space-3);
}
.ai-field > span {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  margin-bottom: 0.3rem;
}
.ai-field em {
  font-weight: 400;
  color: var(--color-text-faint);
}
#ai-status.is-error {
  color: var(--color-error, #e2777a);
}
.ai-prompts-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.ai-prompt-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.ai-prompt-editing {
  display: block;
}
.ai-prompt-meta h3 {
  margin: 0 0 0.2em;
  font-size: 0.95rem;
}
.ai-prompt-sub {
  font-size: 0.8rem;
  color: var(--color-text-faint);
}
.linkish {
  background: none;
  border: 0;
  color: var(--color-primary-text);
  padding: 0;
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
/* A .linkish that is a standalone action rather than a word inside a sentence
 * still has to be pressable: at the inherited font size it is 21px tall.
 * Padding out and margin back in grows the hit area without moving the text a
 * pixel, so the header's rhythm is unchanged. The in-prose ones are left
 * alone — WCAG 2.5.8 exempts a target inline in a block of text, and
 * inflating those would break the line spacing around them. */
.home-card-head-actions .linkish,
.card-head .linkish {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  /* Horizontal padding is pulled back with a negative margin so the text does
   * not move; vertical is NOT. A negative bottom margin here put the button's
   * box 6px below its baseline-aligned header, which then overflowed the card
   * — caught by the screenshot harness's clipping check on tablet. Growing the
   * line box is the version that costs nothing: the header simply becomes 24px
   * tall, which it has room for. */
  padding: 0 8px;
  margin: 0 -8px;
}

/* ── Book search results ────────────────────────────────────────────────── */

.found-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-divider);
}
.found-cover {
  flex: none;
  width: 44px;
  height: 62px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
}
.found-cover.is-blank {
  display: grid;
  place-items: center;
  color: var(--color-text-faint);
}
/* min-width:0 or the sub-lines refuse to shrink and run off the right edge. */
.found-meta {
  flex: 1;
  min-width: 0;
}
/* A book's title is what identifies it, so it gets two lines rather than an
   ellipsis — "The Confessions of St. Au…" is not a book you can recognise. */
.found-meta h3 {
  margin: 0;
  font-size: 0.98rem;
  font-weight: 600;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.found-sub {
  color: var(--color-text-faint);
  font-size: 0.85rem;
  margin-top: 0.1rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.found-src {
  color: var(--color-text-faint);
  font-size: 0.78rem;
  margin-top: 0.2rem;
}
.found-get {
  flex: none;
  align-self: flex-start;
}
.found-get.is-busy {
  opacity: 0.85;
}
.found-get.is-got {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}

/* ── Browse by category (Import screen discovery shelf) ─────────────────── */

.shelf-heading {
  margin: var(--space-5) 0 var(--space-3);
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--color-text);
}
.shelf-heading:first-of-type {
  margin-top: var(--space-3);
}

.browse-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.browse-chip {
  display: inline-flex;
  align-items: center;
  padding: 0.42em 0.9em;
  border-radius: var(--btn-radius);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-faint);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  transition:
    border-color var(--transition),
    color var(--transition);
}
.browse-chip:hover {
  border-color: var(--color-primary);
  color: var(--color-text);
}
.browse-chip.is-on {
  background: var(--color-primary-highlight);
  border-color: var(--color-primary);
  /* --color-primary-text, not --color-primary: amber text on the near-white
   * Day surface fails contrast (README calls this out directly — "a filled
   * amber button on paper fails contrast"), so Day resolves this token to a
   * darker, text-safe amber while Night's resolves to the same value as
   * --color-primary since it already passes there. */
  color: var(--color-primary-text);
  font-weight: 600;
}

/* A store shelf: a grid of cover cards, not a text list. Browsing is
   scanning covers; the auto-fill columns keep cards ~92px wide so a phone
   shows three across and a desktop five or six, without a breakpoint. */
.browse-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: var(--space-4) var(--space-2);
  margin-bottom: var(--space-5);
}
.browse-card {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.browse-card.is-loading {
  opacity: 0.5;
  pointer-events: none;
}
.browse-card:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
/* 2:3 is a book cover's aspect ratio. The icon sits behind the <img>; if the
   cover loads it covers the icon, and onerror removes the img so the icon
   shows through — one fallback, no flash of broken-image. */
.browse-cover {
  position: relative;
  aspect-ratio: 2 / 3;
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
  display: grid;
  place-items: center;
  color: var(--color-text-faint);
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
  transition:
    transform 0.12s ease,
    box-shadow 0.12s ease;
}
/* Podcast covers are square, not 2:3 like books. */
.browse-cover-sq {
  aspect-ratio: 1 / 1;
}
.browse-card:hover .browse-cover {
  transform: translateY(-2px);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
}
.browse-cover img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.browse-cover-icon {
  opacity: 0.5;
}
.browse-title {
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.browse-author {
  font-size: 0.72rem;
  color: var(--color-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Section heading for browse shelves (podcast directory, etc.). */
.browse-heading {
  font-size: 1.05rem;
  font-weight: 700;
  margin: var(--space-5) 0 var(--space-2);
  color: var(--color-text);
}
.browse-section {
  margin-bottom: var(--space-5);
}
/* Skeleton placeholders while a shelf loads — same shape as a real card. */
.browse-card.is-skeleton {
  pointer-events: none;
}
.browse-card.is-skeleton .browse-cover {
  background: var(--color-surface-2);
  animation: browse-pulse 1.4s ease-in-out infinite;
}
@keyframes browse-pulse {
  0%,
  100% {
    opacity: 0.55;
  }
  50% {
    opacity: 0.9;
  }
}
@media (prefers-reduced-motion: reduce) {
  .browse-card.is-skeleton .browse-cover {
    animation: none;
  }
  .browse-card:hover .browse-cover {
    transform: none;
  }
}

/* ── Discover: the query picks the scope (handoff §6) ────────────────────
 *
 * The kind select and the three sub-toggles that used to sit above the box
 * are gone — each was a decision demanded before the search ran, and each
 * could hide the thing being looked for. What is left is one field, and
 * scope chips that appear out of the results.
 */
.dsc-persona {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 9px 5px 7px;
  border: 1px solid var(--color-border-strong);
  border-radius: 999px;
  background: var(--color-surface-2);
  color: var(--color-text-2);
  font-size: 0.71875rem;
  font-weight: 500;
}
.dsc-persona-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-primary);
}
/* Taller than Library's field because it is this screen's primary action
 * rather than a filter over something already showing. */
.dsc-search {
  display: flex;
  align-items: center;
  gap: 9px;
  height: 44px;
  padding: 0 12px;
  /* No bottom margin. A margin between two sticky boxes is a band with no
   * background of its own, and results scrolled through it — a sliver of
   * thumbnail and half a line of text moving between the field and the chips.
   * The gap is now the chip row's top padding, which is part of a painted
   * box. */
  margin-bottom: 0;
  /* Sticky with the scope chips below it. One flick down the results and the
   * query you typed was off screen, along with every way to narrow it — so
   * refining a search meant scrolling back to the top first. These two are
   * the controls for what is below them; they belong above it.
   *
   * top: 0 is the top of .screen-scroll, which begins under the topbar — the
   * bar is sticky in the same container, so no offset is needed. */
  position: sticky;
  top: 0;
  z-index: var(--z-content);
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: 10px;
  color: var(--color-text-faint);
}
.dsc-search:focus-within {
  border-color: var(--color-primary);
}
.dsc-search input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.84375rem;
  border: 0;
  background: none;
  color: var(--color-text);
  padding: 0;
}
.dsc-search input:focus {
  outline: none;
}
/* type="search" gets the browser's own clear button, which sat 9px from ours
 * — two X's that do the same thing, one of which the app cannot style or
 * hide. The input stays type="search" for the keyboard hint it gives on iOS;
 * only the widget goes. */
.dsc-search input::-webkit-search-cancel-button,
.dsc-search input::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
}
#source-q-clear[hidden] {
  display: none;
}
#source-q-clear {
  flex: none;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--color-border);
  color: var(--color-text-2);
  cursor: pointer;
}

.dsc-scope {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
  overflow-x: auto;
  scrollbar-width: none;
  /* Flush under the 44px search field, with the space between them carried
   * as this element's own top padding so there is no unpainted band for
   * results to show through. Page-coloured rather than surface-coloured, so
   * the field above still reads as a pill floating on the page. */
  position: sticky;
  top: 44px;
  z-index: var(--z-content);
  background: var(--color-bg);
  padding-top: 12px;
  padding-bottom: 4px;
}
.dsc-scope:empty {
  display: none;
}
/* The pre-search type filter joins the same sticky stack between the field
 * and the results scopes: field 44px, this row ~46px (30px chips + padding),
 * so the results scopes pin below both. */
.dsc-types {
  top: 44px;
}
#search-scope {
  top: 90px;
}
.dsc-scope::-webkit-scrollbar {
  display: none;
}
.dsc-chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-2);
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}
.dsc-chip.is-on {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-primary);
}
.dsc-chip-count {
  font-size: 0.6875rem;
  opacity: 0.65;
  font-variant-numeric: tabular-nums;
}

.dsc-group + .dsc-group {
  margin-top: 18px;
}
.dsc-group-label {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding-bottom: 6px;
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}
.dsc-group-count {
  color: var(--color-text-dimmer);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
.dsc-group-more {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 9px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: none;
  color: var(--color-text-faint);
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}
.dsc-group-more:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* A result row matches Library's geometry so the two screens read as one
 * system: 46px art, a 13.5px title, an 11px source line. */
.dsc-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 0;
  border-top: 1px solid var(--color-divider);
  cursor: pointer;
}
.dsc-row-art {
  flex: none;
  width: 46px;
  height: 46px;
  border-radius: 5px;
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  display: grid;
  place-items: center;
  object-fit: cover;
}
.dsc-row-main {
  all: unset;
  flex: 1;
  min-width: 0;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.dsc-row-main:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
.dsc-row-title {
  display: block;
  font-size: 0.84375rem;
  font-weight: 500;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dsc-row-sub {
  display: block;
  font-size: 0.6875rem;
  color: var(--color-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Follow takes the kind's accent on hover only — a resting list of Follow
 * buttons in six colours is the confetti problem again. */
.dsc-follow {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 12px;
  border: 1px solid var(--color-border-strong);
  border-radius: 999px;
  background: none;
  color: var(--color-text-2);
  font-size: 0.71875rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}
.dsc-follow:hover {
  border-color: var(--accent, var(--color-primary));
  color: var(--accent, var(--color-primary));
}
.dsc-follow-youtube,
.dsc-follow-rumble {
  --accent: #e5533c;
}
.dsc-follow-text {
  --accent: #7fb2e5;
}
.dsc-follow.is-followed {
  background: var(--color-primary-highlight);
  border-color: #f2a11340;
  color: var(--color-primary-text, var(--color-primary));
  cursor: default;
}

/* A book's expanded detail — see bookResultHTML's comment for why this
 * exists instead of navigating anywhere. Sits directly under its row (the
 * two are adjacent siblings in the markup, not a shared panel at the list's
 * end), indented to read as belonging to that row rather than the next
 * one down. */
.dsc-book-detail {
  margin: 0 0 var(--space-2) 57px; /* aligns under dsc-row-main, past the 46px art + gap */
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-2);
  border-radius: var(--radius-md);
}
.dsc-book-detail[hidden] {
  display: none;
}
.dsc-book-detail-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px var(--space-2);
  margin: 0 0 var(--space-2);
}
.dsc-book-detail-facts dt {
  color: var(--color-text-faint);
  font-size: 0.75rem;
}
.dsc-book-detail-facts dd {
  margin: 0;
  color: var(--color-text-2);
  font-size: 0.75rem;
}
.dsc-book-detail-add {
  width: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.dsc-book-detail-add.is-followed {
  background: var(--color-primary-highlight);
  color: var(--color-primary-text, var(--color-primary));
  cursor: default;
}

/* ── The idle screen: paste a link, intent search, trending ─────────────── */

.dsc-paste {
  display: flex;
  gap: 12px;
  padding: 13px;
  border: 1px dashed var(--color-border-strong);
  border-radius: 10px;
}
.dsc-paste-tile {
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 7px;
  background: var(--color-surface-2);
  color: var(--color-primary-text);
}
.dsc-paste-title {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text);
}
.dsc-paste-sub {
  margin: 3px 0 0;
  font-size: 0.71875rem;
  line-height: 1.5;
  color: var(--color-text-faint);
}

.dsc-intent {
  margin-top: 12px;
  padding: 13px;
  background: linear-gradient(150deg, var(--color-surface-2), var(--color-surface));
  border: 1px solid var(--color-border);
  border-radius: 10px;
}
.dsc-intent-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.84375rem;
  font-weight: 500;
  color: var(--color-text);
}
.dsc-intent-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-primary);
}
.dsc-intent-sub {
  margin: 4px 0 10px;
  font-size: 0.71875rem;
  line-height: 1.5;
  color: var(--color-text-faint);
}
.dsc-intent-input {
  width: 100%;
  font: inherit;
  font-size: 0.78125rem;
  padding: 9px 11px;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: 7px;
  color: var(--color-text-2);
}
.dsc-intent-input:focus {
  outline: none;
  border-color: var(--color-primary);
}
.dsc-intent-examples {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 9px;
}
.dsc-example {
  padding: 5px 9px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: none;
  color: var(--color-text-faint);
  font: inherit;
  font-size: 0.6875rem;
  cursor: pointer;
  text-align: left;
}
.dsc-example:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text-2);
}
.dsc-intent-go {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 11px;
}
.dsc-intent-btn {
  padding: 8px 14px;
  border: 0;
  border-radius: 7px;
  background: var(--color-primary);
  color: var(--color-on-primary);
  font: inherit;
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
}
.dsc-intent-note {
  font-size: 0.6875rem;
  color: var(--color-text-dim);
}

#search-status {
  margin-bottom: var(--space-3);
}
#search-status.is-error {
  color: var(--color-error, #e2777a);
}
#search-status:empty {
  display: none;
}
.show-more-btn {
  display: block;
  width: 100%;
  padding: 0.7em;
  margin-top: var(--space-3);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text-faint);
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  text-align: center;
}
.show-more-btn:hover {
  border-color: var(--color-primary);
  color: var(--color-text);
}
/* "+ Follow" was wrapping onto two lines in the narrow actions column. */
.source-actions button {
  white-space: nowrap;
}
.source-actions button.is-followed {
  color: var(--color-primary-text);
}

/* #pk-player is a global mount the kit's HP engine uses to register podcast/
 * audio playback (HP.mount() writes HP.reg[id] there — the actual play()/
 * toggle() state machine depends on that registration existing, not on the
 * bar being visible) and to hold the inline "up next" list a playlist screen
 * shows while it's open. The inline transport bar it also renders duplicates
 * the persistent .hp-mini dock one screen away, showing up everywhere a
 * podcast/audio track plays — this app already has that dock plus the
 * full-screen player, so a third always-visible transport is redundant
 * clutter rather than a second useful surface. Hidden here rather than
 * removing the #pk-player element itself, which HP.mount() requires to
 * exist at all for playback to start. The .pk-upnext list HP/Queue also
 * renders into this same container is untouched — it's the one thing here
 * that's genuinely only visible on this screen. */
/* The inline "Up Next" list renders into #pk-player, a global mount outside
 * all screens — so without this it shows on every screen. The Queue icon on
 * the mini player opens the full panel; the inline list is only useful on
 * Home and Library, where you're browsing what to play next. */
#pk-player .pk-upnext {
  display: none;
}
body:has(#s-home:not([hidden])) #pk-player .pk-upnext,
body:has(#s-library:not([hidden])) #pk-player .pk-upnext {
  display: block;
}
#pk-player .hp-bar {
  display: none;
}

/* ── Help & FAQ ─────────────────────────────────────────────────────────── */
.help-section {
  margin-bottom: var(--space-4);
}
.help-section h2 {
  margin-bottom: var(--space-2);
}
.help-screens {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--space-2);
}
.help-screen-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}
.help-screen-head {
  display: flex;
  gap: var(--space-2);
}
.help-screen-icon {
  flex-shrink: 0;
  color: var(--color-primary-text);
}
/* Screenshots are phone captures, so they're tall — capped by height rather
 * than width so one doesn't run the length of the page on a wide screen, and
 * `object-fit: contain` keeps the aspect ratio whatever the source dimensions
 * turn out to be (the set mixes .webp and .png at different sizes). */
.help-screen-shot {
  display: block;
  width: 100%;
  max-height: 22rem;
  object-fit: contain;
  object-position: top;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
}
.help-screen-card strong {
  display: block;
  margin-bottom: 0.2rem;
}
.help-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}
.help-table th,
.help-table td {
  padding: var(--space-1) var(--space-2);
  text-align: left;
  border-bottom: 1px solid var(--color-border);
}
.help-table th {
  font-weight: 600;
  color: var(--color-text-2);
}
.help-faq-group {
  /* No group border, and deliberately not a <details> — see help-ui.js. The
   * individual FAQs are the interactive units; this is only a box. */
}
.help-faq {
  border-bottom: 1px solid var(--color-border);
  padding: var(--space-2) 0;
}
.help-faq summary {
  cursor: pointer;
  font-weight: 500;
  list-style: none;
}
.help-faq summary::-webkit-details-marker {
  display: none;
}
.help-faq summary::before {
  content: '+';
  display: inline-block;
  width: 1.2em;
  color: var(--color-primary-text);
}
.help-faq[open] summary::before {
  content: '\2013';
}
.help-faq p {
  margin: var(--space-1) 0 0 1.2em;
  color: var(--color-text-2);
  font-size: 0.8125rem;
  line-height: 1.6;
}
/* §19: an answer names its destination and goes there directly rather than
 * describing where to find it. Indented level with the answer text above. */
.help-faq-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-2) 0 0 1.2em;
}
.help-faq-links button {
  padding: 0.3em 0.75em;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-primary-text);
  font-size: 0.78rem;
}
.help-faq.is-hidden-by-search {
  display: none;
}
.help-glossary dt {
  font-weight: 600;
  margin-top: var(--space-2);
}
.help-glossary dd {
  margin-left: 1rem;
  color: var(--color-text-2);
}

/* ── The omnibar (handoff §12) ───────────────────────────────────────────
 *
 * One field; three intents, always in this order: Do, Open, Ask. It opens
 * from the top bar's glyph, ⌘K / Ctrl+K / `/`, and a swipe up from the tab
 * bar — never from pull-down, which keeps the gesture it already owns.
 */
.omni {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  justify-content: center;
  padding: 12vh 14px 14px;
}
.omni[hidden] {
  display: none;
}
.omni-backdrop {
  position: absolute;
  inset: 0;
  background: #05060ac9;
}
.omni-panel {
  position: relative;
  width: min(560px, 100%);
  max-height: 76vh;
  display: flex;
  flex-direction: column;
  animation: bp-rise 0.18s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .omni-panel {
    animation: none;
  }
}
.omni-field {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 46px;
  padding: 0 12px;
  background: var(--color-surface);
  border: 1px solid #f2a11359;
  border-radius: 11px;
  box-shadow: 0 0 0 3px #f2a11312;
  color: var(--color-primary-text);
}
.omni-field input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.875rem;
  border: 0;
  background: none;
  color: var(--color-text);
  padding: 0;
}
.omni-field input:focus {
  outline: none;
}
.omni-kbd {
  flex: none;
  padding: 3px 6px;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-surface-2);
  color: var(--color-text-faint);
  font-size: 0.59375rem;
  font-weight: 600;
}
/* #442: the two are alternatives, not companions. A ⌘K chip means nothing
 * without a hardware keyboard, and a phone has no Escape key to close the
 * panel with — so the phone layout trades the hint for a real close button and
 * the desktop layout keeps the hint. Either way the panel always shows exactly
 * one of them, and on the surface where dismissing was the problem, that one
 * is a control rather than a legend. */
.omni-close {
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-surface-2);
  color: var(--color-text-faint);
  font-size: 0.8125rem;
  line-height: 1;
  cursor: pointer;
}
:root[data-layout='desktop'] .omni-close {
  display: none;
}
:root:not([data-layout='desktop']) .omni-kbd {
  display: none;
}
.omni-hints {
  display: flex;
  gap: 14px;
  padding: 8px 4px 0;
  font-size: 0.6875rem;
  color: var(--color-text-dim);
}
.omni-results {
  margin-top: 10px;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 11px;
}
.omni-results:empty {
  display: none;
}
.omni-group + .omni-group {
  border-top: 1px solid var(--color-divider);
}
.omni-group-label {
  padding: 10px 12px 4px;
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}
.omni-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 12px;
  border: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.omni-row.is-on,
.omni-row:hover {
  background: var(--color-surface-2);
}
.omni-tile {
  flex: none;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border-radius: 6px;
  background: var(--color-surface-2);
  color: var(--color-primary-text);
}
.omni-tile-open {
  border-radius: 5px;
}
.omni-tile-ask {
  color: #7fb2e5;
}
.omni-row-text {
  flex: 1;
  min-width: 0;
}
.omni-row-label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.omni-row-sub {
  display: block;
  font-size: 0.6875rem;
  color: var(--color-text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.omni-run {
  flex: none;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--color-text-dimmer);
}
.omni-legend {
  padding: 12px;
}
.omni-legend-label {
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
  margin-bottom: 6px;
}
.omni-legend-row {
  font-size: 0.75rem;
  color: var(--color-text-2);
  padding: 3px 0;
}
.omni-legend code,
.omni-empty code {
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--color-surface-2);
  color: var(--color-primary-text);
  font-size: 0.6875rem;
}
.omni-note {
  margin: 10px 0 0;
  font-size: 0.6875rem;
  color: var(--color-text-dimmer);
}
.omni-empty {
  padding: 20px 14px;
  text-align: center;
}
.omni-empty-lead {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text-2);
}
.omni-empty-sub {
  margin: 6px 0 0;
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--color-text-dim);
}

/* ── The creator page (handoff §8) ────────────────────────────────────────
 *
 * The avatar is round, and it is the only round art in the app — every other
 * piece of artwork is square. That one difference is the whole signal that
 * this page is about a person rather than a thing.
 */
.cr-hero {
  position: relative;
  height: 132px;
  /* The scroll container is a column flexbox, and a flex item shrinks before
   * its container scrolls — which quietly took this 132px hero down to 41px
   * once the creator had enough work listed under it to overflow. Every
   * fixed-height band in a scrolling column needs to opt out of that. */
  flex: none;
  margin: 0 calc(-1 * var(--space-4));
  background: linear-gradient(160deg, var(--color-surface-2), var(--color-surface));
}
/* Navigation stays round; only transport is square. */
.cr-back {
  position: absolute;
  z-index: var(--z-control);
  top: 12px;
  left: 12px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  background: #08090dcc;
  color: var(--color-text);
  cursor: pointer;
}
/* Above the hero, not behind it: the avatar's negative margin pulls it up
 * over the art, and without a stacking context of its own the hero's
 * background paints on top of the half that overlaps. */
.cr-identity {
  position: relative;
  z-index: var(--z-content);
  text-align: center;
  padding: 0 14px 4px;
}
.cr-avatar {
  width: 72px;
  height: 72px;
  margin: -34px auto 0;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  box-shadow: 0 0 0 3px var(--color-bg);
  overflow: hidden;
}
.cr-name {
  margin: 10px 0 0;
  font-family: var(--font-display);
  font-size: 1.1875rem;
  font-weight: 500;
}
.cr-sub {
  margin-top: 2px;
  font-size: 0.71875rem;
  color: var(--color-text-faint);
}
.cr-actions {
  display: flex;
  gap: 8px;
  padding: 12px 0 4px;
}
.cr-primary,
.cr-secondary {
  flex: 1;
  padding: 10px 0;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  background: none;
  color: var(--color-text-2);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
}
.cr-primary {
  border-color: var(--color-primary);
  background: var(--color-primary);
  color: var(--color-on-primary);
}
.cr-secondary:hover {
  border-color: var(--color-border-strong);
}

.cr-group {
  margin-top: 18px;
}
.cr-group-label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 6px;
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}
.cr-group-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
}
.cr-group-count {
  color: var(--color-text-dimmer);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
.cr-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 0;
  border-top: 1px solid var(--color-divider);
  cursor: pointer;
}
.cr-row-art {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 5px;
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  display: grid;
  place-items: center;
  object-fit: cover;
}
.cr-row-main {
  all: unset;
  flex: 1;
  min-width: 0;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cr-row-main:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
.cr-row-title {
  display: block;
  font-size: 0.84375rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cr-row-role {
  display: block;
  font-size: 0.6875rem;
  color: var(--color-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cr-follow {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 12px;
  border: 1px solid var(--color-border-strong);
  border-radius: 999px;
  background: none;
  color: var(--color-text-2);
  font: inherit;
  font-size: 0.71875rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}
.cr-follow:hover {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
}
.cr-follow.is-followed {
  background: var(--color-primary-highlight);
  border-color: #f2a11340;
  color: var(--color-primary-text, var(--color-primary));
  cursor: default;
}
.cr-empty {
  margin: 0;
  padding: 14px 0;
  font-size: 0.8125rem;
  color: var(--color-text-dim);
}

/* ── Source detail (handoff §7) ──────────────────────────────────────────
 *
 * A 150px hero, the identity row's art pulled up over it, and an action row
 * of exactly three controls. Everything that used to stack above the episode
 * list is a setting rather than content and lives behind the ··· .
 */
/* The hero bleeds the full width of the screen, so it has to cancel <main>'s
 * own padding. A negative margin alone is not enough: `.screen-scroll` sets
 * `overflow-y: auto`, which computes `overflow-x` to `auto` as well, and the
 * bleed is then clipped at the scroll box rather than reaching the edge. So
 * the scroll box itself is widened and the padding moved onto it — the
 * content keeps the same inset, and the hero now has somewhere to bleed into.
 * Written against --space-4 rather than a hard-coded 16px, because the day
 * that token changes is the day a literal leaves a hairline down both edges. */
#s-source,
#s-author {
  margin: 0 calc(-1 * var(--space-4));
}
#s-source > .screen-scroll,
#s-author > .screen-scroll {
  padding: 0 var(--space-4);
}
.sd-hero {
  position: relative;
  height: 150px;
  flex: none; /* see .cr-hero — a fixed band in a scrolling column */
  margin: 0 calc(-1 * var(--space-4));
  overflow: hidden;
  background: var(--color-surface-2);
}
.sd-hero-art {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.55;
}
/* The kind's colour, once, washing the art down into the page — the same
 * trick Now Playing uses, at a quarter of the strength. Sits over the art so
 * the title below never lands on a bright patch of someone's cover. */
.sd-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom, transparent 38%, var(--color-bg) 100%),
    radial-gradient(
      120% 90% at 50% 0%,
      color-mix(in srgb, var(--sd-tint, var(--color-primary)) 14%, transparent),
      transparent 70%
    );
}
/* Navigation stays round; only transport is square. */
.sd-back {
  position: absolute;
  z-index: var(--z-control);
  top: 12px;
  left: 12px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  background: #08090dcc;
  color: #edeff4;
  cursor: pointer;
}
.sd-identity {
  position: relative;
  z-index: var(--z-content);
  padding-bottom: 14px;
  border-bottom: 1px solid var(--color-divider);
}
.sd-art {
  width: 68px;
  height: 68px;
  margin-top: -40px;
  display: grid;
  place-items: center;
  border-radius: 5px;
  object-fit: cover;
  background: var(--color-surface-2);
  color: var(--color-text-dim);
  box-shadow: 0 0 0 3px var(--color-bg);
}
.sd-title {
  margin: 10px 0 0;
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 500;
  line-height: 1.25;
}
.sd-meta {
  margin: 3px 0 0;
  font-size: 0.71875rem;
  color: var(--color-text-faint);
}
.sd-byline {
  margin-top: 4px;
  font-size: 0.71875rem;
}
.sd-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 0;
}
.sd-follow,
.sd-persona {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 0;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  background: none;
  color: var(--color-text-2);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
}
.sd-follow.is-on {
  border-color: var(--color-primary);
  background: var(--color-primary);
  color: var(--color-on-primary);
}
.sd-persona.is-on {
  border-color: #f2a11340;
  background: var(--color-primary-highlight);
  color: var(--color-primary-text, var(--color-primary));
}
.sd-more {
  flex: none;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  background: none;
  color: var(--color-text-dim);
  cursor: pointer;
}
.sd-more:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}
/* A labelled numeric field in a sheet: label left, input right, unit glued to
 * the input. One per row — three of these side by side was unreadable. */
.sheet-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 0;
  font-size: 0.8125rem;
  color: var(--color-text-2);
  cursor: pointer;
}
.sheet-field-input {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  color: var(--color-text-dim);
  font-size: 0.71875rem;
}
.sheet-field-input input {
  width: 62px;
  padding: 5px 7px;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-bg);
  color: var(--color-text);
  font: inherit;
  font-size: 0.8125rem;
  text-align: right;
}
.sheet-field-input i {
  font-style: normal;
}

/* A list quietly shorter than it should be, with the control that shortened
 * it one layer down in a sheet, is how you lose track of your own library. */
.item-hidden-note {
  display: block;
  width: 100%;
  padding: 8px 0;
  border: 0;
  background: none;
  color: var(--color-text-dim);
  font: inherit;
  font-size: 0.71875rem;
  text-align: left;
  cursor: pointer;
}
.item-hidden-note:hover {
  color: var(--color-primary-text);
}
.sd-episodes-label {
  padding: 6px 0;
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}

/* ── Settings → Voices (handoff §13) ─────────────────────────────────────
 *
 * Four rules in the order they resolve, with the order printed underneath.
 * The only unusual thing on the screen is the live preview under the
 * article-mode toggle: it renders the real preamble string the narrator will
 * speak, not a description of it, because the bug it fixes ("an RSS item is
 * announced as Chapter 1") is a bug about words.
 */
.vc-group {
  margin-bottom: 18px;
}
.vc-group-title {
  margin: 0 0 8px;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text);
}
/* One scrolling rail, not a seven-row wall: there are thirteen voices, and
 * wrapping them turns a choice into a paragraph. Same rail the kind chips on
 * Library use, for the same reason. */
.vc-chips {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
}
.vc-chips::-webkit-scrollbar {
  display: none;
}
/* Inside the picker sheet the rail becomes a list.
 *
 * A sheet exists to make one choice, and fifteen options on a hidden-scrollbar
 * rail showed about two and a half of them with nothing indicating the rest —
 * which is where the on-device voices were hiding, at positions nine to
 * fifteen. overflow goes back to visible or the wrap has nothing to wrap in. */
.vc-chips-wrap {
  flex-wrap: wrap;
  overflow-x: visible;
  gap: 8px;
}
.vc-pick-group + .vc-pick-group {
  margin-top: 16px;
}
/* ── Listening report ────────────────────────────────────────────────────── */

.stat-range {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
#settings-body .stat-range-btn,
.stat-range-btn {
  padding: 7px 12px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-2);
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}
#settings-body .stat-range-btn.is-on,
.stat-range-btn.is-on {
  border-color: var(--color-primary);
  background: var(--color-primary);
  color: var(--color-on-primary);
}
/* auto-fit rather than a fixed four, so the four cells become two rows on a
 * phone instead of four unreadable columns. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px;
  margin-bottom: 16px;
}
.stat-cell {
  padding: 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}
.stat-n {
  display: block;
  font-size: 1.4rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--color-text);
}
.stat-l {
  display: block;
  margin-top: 3px;
  font-size: 0.71875rem;
  color: var(--color-text-dim);
}
/* The source table can be long and its ids are not short. Scrolling it inside
 * its own box keeps the settings page itself from scrolling sideways. */
.stat-table {
  display: block;
  max-height: 46vh;
  overflow: auto;
  overscroll-behavior: contain;
}
.stat-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
}
#settings-body .vc-test-btn,
.vc-test-btn {
  margin-top: 10px;
  padding: 9px 14px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--btn-radius);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: 0.8125rem;
  cursor: pointer;
}
.vc-test-btn:hover:not(:disabled) {
  border-color: var(--color-primary);
  color: var(--color-primary-text);
}
.vc-test-btn:disabled {
  opacity: 0.55;
  cursor: default;
}
.vc-pick-group-title {
  margin: 0 0 8px;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}
.vc-chip {
  flex: none;
  white-space: nowrap;
}
.vc-chip {
  padding: 8px 11px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-2);
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}
.vc-chip:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}
.vc-chip.is-on {
  border-color: var(--color-primary);
  background: var(--color-primary);
  color: var(--color-on-primary);
}
.vc-caption {
  margin: 8px 0 0;
  font-size: 0.71875rem;
  line-height: 1.5;
  color: var(--color-text-dim);
}
.vc-card {
  margin-bottom: 18px;
  padding: 12px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
}
.vc-toggle {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
}
/* Amber, like every other checkbox that means "on" in this app — a browser
 * default blue in the middle of the palette reads as a control from another
 * application. */
.vc-toggle input[type='checkbox'] {
  flex: none;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  accent-color: var(--color-primary);
}
.vc-toggle > span {
  display: block;
}
.vc-toggle strong {
  display: block;
  font-size: 0.8125rem;
  font-weight: 500;
}
/* Recessed, because it is a quotation of the app rather than a control: what
 * you will hear, in the words you will hear it in. */
.vc-preview {
  margin-top: 10px;
  padding: 9px 11px;
  border: 1px solid var(--color-divider);
  border-radius: 6px;
  background: var(--color-bg);
}
.vc-preview-label {
  display: block;
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}
.vc-preview-text {
  margin: 5px 0 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--color-text-2);
}
.vc-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 0;
  border: 0;
  border-top: 1px solid var(--color-divider);
  background: none;
  color: var(--color-text-dim);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.vc-row:first-of-type {
  border-top: 0;
}
.vc-row-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 999px;
}
.vc-row-art {
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 4px;
  object-fit: cover;
  background: var(--color-surface-2);
}
.vc-row-main {
  flex: 1;
  min-width: 0;
}
.vc-row-name {
  display: block;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vc-row-voice {
  display: block;
  font-size: 0.6875rem;
  color: var(--color-text-dim);
}
/* The chain itself, on the screen. Every "the voice didn't stick" report is
 * a question about which rule won, so the screen states the order and then
 * names the winner. */
.vc-order {
  margin-top: 18px;
  padding: 12px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
}
.vc-order-label {
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}
.vc-order-chain {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 7px;
}
.vc-order-step {
  padding: 4px 8px;
  border: 1px solid var(--color-divider);
  border-radius: 5px;
  font-size: 0.71875rem;
  color: var(--color-text-2);
}
.vc-order-arrow {
  color: var(--color-text-dim);
  font-size: 0.71875rem;
}

/* ── Controls the redesign left with UA chrome ─────────────────────────────
 *
 * Three places render real <button>s that no stylesheet ever claimed, so they
 * fell through to the browser's default: ButtonFace grey, a 2px outset border
 * and square corners, ~27px tall, sitting beside 41px app buttons. Found by
 * screenshotting every screen and reading them — none of this shows up in a
 * DOM check, because the markup is perfectly correct.
 *
 * `.seg` is the sleep-timer's own class (ambience.js) and `.ep-filter-bar` its
 * row; neither existed in any loaded stylesheet. The note editor's footer and
 * the Playlists "Daily" button are bare <button>s in containers with no rule
 * of their own. */
.ep-filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.seg,
.ned-foot button,
.pl-head button:not(.primary) {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  min-height: 34px;
  padding: 7px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  background: var(--color-surface-2);
  color: var(--color-text);
  font: inherit;
  font-size: 0.8125rem;
  cursor: pointer;
}
.seg:hover,
.ned-foot button:hover,
.pl-head button:not(.primary):hover {
  border-color: var(--color-border-strong);
}
.seg[aria-checked='true'],
.seg.is-on {
  border-color: var(--color-primary);
  background: var(--color-primary);
  color: var(--color-on-primary);
}
/* Destructive, and readable: #9B2C2C on UA grey measured 2.4:1. On the app's
   own surface it is the border and the label that carry the meaning. */
.ned-foot button.danger,
.ned-foot button[data-danger] {
  border-color: var(--color-danger, #e5533c);
  color: var(--color-danger, #e5533c);
}

/* A file input renders "Choose File | No file chosen" in the UA's own chrome
   and cannot be styled directly. .file-btn already exists for exactly this and
   these two were not using it. */
#backup-file,
#opml-file {
  display: block;
  width: 100%;
  margin: var(--space-2) 0;
  padding: 10px 12px;
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--btn-radius);
  background: var(--color-surface-2);
  color: var(--color-text-2);
  font: inherit;
  font-size: 0.8125rem;
}
#backup-file::file-selector-button,
#opml-file::file-selector-button {
  margin-right: 10px;
  padding: 6px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: 0.8125rem;
  cursor: pointer;
}

/* The ducking slider was a raw range input — default Chrome blue on a dark
   surface, which reads as a control from a different application. */
#settings-body input[type='range'],
.snd-duck input[type='range'] {
  accent-color: var(--color-primary);
}
