/* 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: 48px;
  --tabbar-h: 48px;
  --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;
  /* ...and drops below the shell (1005) while the extras drawer is open.
     The drawer expands over the artwork, which includes the video stage, but
     a fixed iframe above the shell cannot be covered by a panel inside it —
     so the video has to move down rather than the drawer up. See
     #video-surface.vs-park-behind. */
  --z-player-shell-video-under: 1004;
  --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;

  /* Dynamic Type. 1 means "the size this app was designed at" and is the value
   * everywhere except an Apple device whose owner has moved the text-size
   * slider — the boot script in app.html overwrites it there, and only there.
   *
   * Deliberately a RATIO against the platform default rather than the
   * platform's own body size. `font: -apple-system-body` resolves to 17px on a
   * stock iPhone, so adopting it outright would have grown every screen in the
   * app by 6% for every iOS user who had never touched the setting — a
   * redesign nobody asked for, shipped under the name of an accessibility fix.
   * The ratio is 1.0 for those users and the design is untouched; it moves only
   * for someone who actually asked for bigger text. */
  --dt-scale: 1;
}

/* The 338 `rem` font sizes in this file all hang off this one number, which is
 * the entire reason Dynamic Type costs one declaration instead of a rewrite.
 * (There are 3 `px` font sizes left. They will not scale. They are decorative
 * glyph sizes, not text.)
 *
 * The scale is clamped to [1, 1.35] in the boot script, so the top of this is
 * 21.6px — see there for why that ceiling and not a bigger one. */
html {
  /* `100%`, never a px value. A percentage on the ROOT font size resolves
   * against the initial value, which every browser maps to the reader's own
   * default-font-size setting — so this multiplies that preference instead of
   * replacing it.
   *
   * `calc(16px * ...)` was the first version and it was an accessibility
   * regression wearing an accessibility fix's clothes: measured in Chromium
   * with the default font size raised to 24px, the app rendered its root at
   * 16px. Every rem on all 338 declarations shrank back to the design size, and
   * the reader's setting — which this file had honoured for free by never
   * declaring a root font size at all — was silently discarded. On iOS the page
   * default does not track Dynamic Type (that is the entire reason the probe in
   * app.html has to exist), so there is no double-counting here: the two
   * preferences are separate and both now apply. */
  font-size: calc(100% * var(--dt-scale));
  /* Stop iOS inflating text on its own during rotation. The app now has a real
   * answer for "the user wants bigger text", and WebKit's heuristic one fights
   * it — it resizes individual blocks rather than the root, so `rem` spacing
   * stays put while the text inside it grows, which is how a tuned row turns
   * into a clipped one. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

@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: #f2a1132e;
  --color-navy: #1f274c;
  --color-secondary: #9fb6ee;
}

@media (prefers-color-scheme: dark) {
  /* :not([data-theme]) — "system", which is stored as no attribute at all —
   * not :not([data-theme='light']). That looser form matched anything that
   * merely wasn't literally light, which was harmless while dark/light were
   * the only two explicit themes (this fallback's values equal dark's own
   * explicit rule, so double-matching dark was redundant, not wrong) but
   * started clobbering an explicitly-chosen Dusk the moment a third theme
   * existed: on an OS set to dark, this rule and Dusk's own tied on
   * specificity, and this one — appearing later — won, silently overriding
   * Dusk's palette with Dark's whenever the OS happened to prefer dark. */
  :root:not([data-theme]) {
    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: #f2a1132e;
    --color-navy: #1f274c;
    --color-secondary: #9fb6ee;
  }
}

/* Dusk: a gray twilight between Dark and Day, for a screen that isn't quite
 * near-black but shouldn't be cream/paper either — office lighting, an
 * overcast afternoon, anyone who finds true dark a little too dark and Day's
 * warm cream too bright. Same amber accent as the other two: the palette
 * that changes is the neutral scale, not the brand color, same as Dark and
 * Day already agree on --color-primary. */
:root[data-theme='dusk'] {
  color-scheme: dark;
  --color-bg: #2a2d34;
  --color-surface: #33363e;
  --color-surface-2: #3c4049;
  /* Deliberately close to --color-surface, not a bold "raised" step like
   * Dark's own offset (#22262f over a near-black #08090d bg) — Dusk's whole
   * neutral range sits in a much narrower band than Dark's or Light's, so a
   * jump as large as Dark's proportionally would land light enough that
   * --color-text-faint/-dim/-dimmer (used on it, e.g. .lib-chip-count)
   * couldn't clear 4.5:1 without themselves becoming brighter than
   * --color-text-3 — inverting the intended dim-to-bright order. Measured
   * against e2e_contrast.mjs, not guessed: the original #454955 failed at
   * 3.68:1. */
  --color-surface-offset: #363943;
  --color-border: #454a56;
  --color-border-strong: #4d5361;
  --color-divider: #383c46;
  --color-text: #e7e9ee;
  --color-text-2: #c3c7d1;
  --color-text-3: #a3a8b6;
  --color-text-faint: #abb1be;
  --color-text-dim: #a8aebb;
  --color-text-dimmer: #a4aab6;
  --color-primary: #f2a113;
  --color-primary-text: #f2a113;
  /* Lower alpha than Dark/Light's #f2a1132e (~18%): composited over Dusk's
   * lighter surfaces, the same alpha reads visibly warmer and pushed
   * --color-primary-text below AA on a filled control (.feed-card-tool-
   * primary, measured at 4.13:1). ~12% keeps the amber wash recognizable
   * while clearing 4.5:1 against Dusk's own surface tones specifically —
   * this is the one token dusk can't just copy from dark/light unchanged. */
  --color-primary-highlight: #f2a1131e;
  --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: #f2a1132e;
  --color-navy: #1f274c;
  --color-secondary: #2c3e6b;
}

@media (prefers-color-scheme: light) {
  /* Same fix as the dark fallback above, same reason: :not([data-theme])
   * (system only), not :not([data-theme='dark']) — the loose form is what
   * actually caused the observed bug (this file's Dusk block sits before
   * this one in source order, so on an OS set to light, this rule's
   * :not([data-theme='dark']) matched Dusk too and won the specificity tie
   * by appearing later, overriding Dusk's own gray palette with Light's
   * cream one — Settings showed "Dusk" selected while the page rendered
   * Light). */
  :root:not([data-theme]) {
    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: #f2a1132e;
    --color-navy: #1f274c;
    --color-secondary: #2c3e6b;
  }
}

* {
  box-sizing: border-box;
}

/* <img> and <a href> opt into the browser's native drag-out gesture by
 * default — no attribute needed, it's the element's own IDL `draggable`
 * defaulting to true. Nothing in this app uses HTML5 drag-and-drop (list
 * reordering is pointer/touch-driven, see playlists-ui.js), so that default
 * only ever shows up as a bug: a feed thumbnail or row link picking up and
 * sliding around under a touch-drag or mouse-drag instead of staying put. */
img,
a {
  -webkit-user-drag: none;
  user-drag: none;
}

/* 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) {
  /* :not([data-theme]) — same fix as the core token cascade above (#1284):
   * the loose :not([data-theme='dark']) form also matched Dusk, swapping in
   * the light (dark-ink) wordmark against Dusk's dark background. */
  :root:not([data-theme]) .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;
}
@media (hover: hover) {
  .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;
}
@media (hover: hover) {
  .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;
}
/* 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;
}
@media (hover: hover) {
  #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-details {
  margin-bottom: var(--space-3);
}
.source-features-details > summary {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-faint);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.35em;
}
.source-features-details > summary::-webkit-details-marker {
  display: none;
}
.source-features-details > summary::before {
  content: '▸';
  font-size: 0.65em;
  transition: transform 0.15s ease;
}
.source-features-details[open] > summary::before {
  transform: rotate(90deg);
}
.source-features-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.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:focus-visible,
.creator-link:focus-visible {
  text-decoration-color: currentColor;
}
@media (hover: hover) {
  .author-link:hover,
  .creator-link:hover {
    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;
}
@media (hover: hover) {
  .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);
}
/* The icon holder is a flex box, not a line box.
 *
 * hydrateIcons() injects an <svg> into this span, and an inline-level SVG
 * carries the font's descender space with it: an 18px glyph in a 16px line
 * box measures 23px, five of which hang outside the 44px grid cell. Nothing
 * looks wrong — overflow is visible — but qa_shots' SQUASH_PROBE reads
 * scrollHeight against height and reported it as clipped content on every
 * screen with a back button (60 failures, all this one element).
 *
 * Scoped to .back rather than every [data-icon]: holders elsewhere sit inline
 * beside their labels, where forcing the SVG out of the inline flow would
 * change alignment for a problem they do not have. */
.back > [data-icon] {
  display: flex;
}

/* ── 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 {
  min-height: 40px;
}
.screen-head button {
  min-height: 44px;
}

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: 44px;
  transition:
    background var(--transition),
    border-color var(--transition);
}
/* The container selectors above — `.row-actions button`, `.stack button`,
 * `#settings-body button` — are 0,1,1 and beat a bare `.primary` at 0,1,0, so
 * a button that declared itself primary in the markup rendered in the same
 * grey as everything beside it. On the playlist screen that meant "Play all"
 * was indistinguishable from "Delete" in a row of eight identical buttons, and
 * the markup had said which one mattered since it was written. Naming the
 * combination honours the intent rather than restating it. */
.primary,
.row-actions button.primary,
.row-actions a.primary,
.inline-form button.primary,
.stack button.primary,
#settings-body button.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;
}
@media (hover: hover) {
  .primary:hover {
    filter: brightness(1.06);
  }
}
@media (hover: hover) {
  .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;
}
/* 44, like the buttons it sits in a row with. */
.pl-sort-select {
  height: 44px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.8125rem;
  padding: 0 8px;
  flex-shrink: 0;
}
/* The one filter Home's "New" list and All Episodes share (#1254 follow-up)
 * — same visual treatment as .pl-sort-select, kept as its own class since the
 * two controls mean different things even though they look identical. */
/* 44px: every sibling in this cluster is min-height 44, this select was 32 —
 * visibly short, and the only control in the row a finger could miss. */
.kind-filter-select {
  height: 44px;
  width: 72px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.75rem;
  padding: 0 4px;
  flex-shrink: 0;
}
.all-eps-head-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}
/* One declaration, and the page title stops being the thing that shrinks.
 *
 * `.screen-head-title` is `flex: 1` — basis 0 — with `min-width: 0`, and the
 * h1 inside it carries `overflow: hidden; text-overflow: ellipsis`. So the
 * title started from no intrinsic width at all and every pixel of squeeze
 * landed on the one element that answers "what page am I on": at 390px it
 * rendered "All Episo…" (measured, scrollWidth 113 against clientWidth 97) to
 * protect a filter select and a checkbox. `flex: 0 1 auto` gives it its
 * content width to start from, and `.screen-head`'s own `flex-wrap: wrap`
 * then drops the actions onto their own line — which is what the Library
 * header does, and Library is the screen this one should have been copying.
 *
 * The first version of this fix also set `flex-shrink: 1; flex-wrap: wrap` on
 * the actions and `min-width: max-content` here. Both were measured out: at
 * 320, 390 and 430 the geometry is byte-identical without either, and the
 * defect still reproduces with both present if this line is removed. The one
 * that does the work is the one that is left. */
.all-eps-head .screen-head-title,
#s-all-episodes .screen-head-title {
  flex: 0 1 auto;
}
.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 */
}
/* #826: Per-playlist auto-remove toggle. */
.pl-options-row {
  margin-bottom: var(--space-3);
}
/* Same as `.lib-toggle`: the label is what a thumb aims at, not the 16px box
 * inside it, and it measured 209x18. */
.pl-toggle-opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  font-size: 0.86rem;
  color: var(--color-text-faint);
  cursor: pointer;
}
.pl-toggle-opt input {
  margin: 0;
}

/* 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) {
  /* :not([data-theme]) — same fix as above (#1284): the loose form also
   * matched Dusk, applying colors darkened for contrast on Light's cream
   * against Dusk's dark gray instead — the near-illegible dark-on-dark the
   * un-scoped rule above this block was written to avoid for Dark itself. */
  :root:not([data-theme]) [data-kind='podcast'],
  :root:not([data-theme]) [data-kind='audio'] {
    color: #876700;
  }
  :root:not([data-theme]) [data-kind='text'] {
    color: #2b5580;
  }
  :root:not([data-theme]) [data-kind='book'] {
    color: #54409f;
  }
  :root:not([data-theme]) [data-kind='youtube'],
  :root:not([data-theme]) [data-kind='rumble'] {
    color: #a82d1a;
  }
  :root:not([data-theme]) [data-kind='music'],
  :root:not([data-theme]) [data-kind='radio'] {
    color: #1c6e59;
  }
  :root:not([data-theme]) [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);
}
@media (hover: hover) {
  .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;
  min-width: 24px;
  min-height: 24px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-md);
}
@media (hover: hover) {
  #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;
}
@media (hover: hover) {
  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;
}
@media (hover: hover) {
  .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;
}
/* A radio result's .source-meta (stationResultHTML) is a real <button>, not
 * the <div> every other kind's meta block still is — same reasoning as
 * .row-title-btn below: a tap target the row's onclick handler covers isn't
 * automatically keyboard/screen-reader operable, an <article> isn't. Without
 * this, the UA's default button chrome (grey background, outset border) shows
 * through on top of `.source-meta`'s flex/min-width, since neither of those
 * properties touches appearance/background/border. `button.source-meta`
 * beats the bare `.source-meta` rule above on specificity, so this only ever
 * matches the button case — the div-based rows elsewhere are untouched. */
button.source-meta {
  all: unset;
  flex: 1;
  min-width: 0;
  display: block;
  text-align: left;
  cursor: pointer;
}
button.source-meta:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* 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;
  /* #644: WCAG 2.5.8 — touch targets must be at least 24×24px. The button's
   * natural height (from font-size + line-height) is ~21px, so min-height
   * pads it to the minimum without changing the visual text size. */
  min-height: 24px;
}
.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);
  background: var(--color-primary-highlight);
  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;
}
@media (hover: hover) {
  .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;
  /* #557: --reader-font-size is set from JS (books.js's _applyAppearance) to
   * whichever size the reading-appearance sheet last picked; the fallback is
   * the size this reader shipped with, so a reader that never opens that
   * sheet is unaffected. */
  font-size: var(--reader-font-size, 1.14rem);
  line-height: 1.72;
  max-width: 34rem;
  margin: 0 auto;
}
/* #557: the "Sans" option in the appearance sheet. Kept to the same stack
 * already used for the reader's own chrome (eyebrow, nav) rather than
 * introducing a third font family into the app. */
.reader.reader-font-sans {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* #557: the four fixed page themes in the appearance sheet — White, Sepia,
 * Gray and Black, each a pre-designed text+background pair rather than a
 * free color picker (a curated set can't produce an unreadable combination;
 * two independent color pickers could). Independent of the app's own
 * light/dark theme, the way most ebook readers offer it, and scoped to the
 * reading pane itself, not the screen chrome around it (header buttons, tab
 * bar), which stays on the app's normal theme. Every pair is chosen for
 * comfortably-AA-or-better contrast: White and Black are both well past
 * 15:1, Sepia and Gray both comfortably past 8:1. */
.reader.reader-theme-white,
.reader.reader-theme-sepia,
.reader.reader-theme-gray,
.reader.reader-theme-black {
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.reader.reader-theme-white {
  background: #fafaf7;
  color: #1a1a1a;
}
.reader.reader-theme-white .reader-eyebrow {
  color: #6b6b66;
}
.reader.reader-theme-sepia {
  background: #f4ecd8;
  color: #3b2f1e;
}
.reader.reader-theme-sepia .reader-eyebrow {
  color: #7a6a4f;
}
.reader.reader-theme-gray {
  background: #2e2e32;
  color: #d6d6d6;
}
.reader.reader-theme-gray .reader-eyebrow {
  color: #8f8f94;
}
.reader.reader-theme-black {
  background: #0d0d0d;
  color: #e6e6e6;
}
.reader.reader-theme-black .reader-eyebrow {
  color: #8a8a8a;
}
/* Translate and Chapters/Aa render directly against the page background
 * with no fill of their own (unlike Listen, which is always amber-filled,
 * and the Previous/Next nav buttons, which always carry --color-surface-2)
 * — their border/text color comes from the app's own theme tokens by
 * default, which is exactly wrong whenever the page theme and the app
 * theme disagree: a White or Sepia page under a dark app theme, or a Gray
 * or Black page under a light one, would otherwise render both almost
 * invisibly. Pinned per page-theme instead, so it's correct regardless of
 * which app theme happens to be active. */
.reader.reader-theme-white .reader-chapter-translate,
.reader.reader-theme-white .reader-chapters-toggle,
.reader.reader-theme-white .reader-chapter-nav button,
.reader.reader-theme-sepia .reader-chapter-translate,
.reader.reader-theme-sepia .reader-chapters-toggle,
.reader.reader-theme-sepia .reader-chapter-nav button {
  color: #3a3a3a;
  border-color: rgba(0, 0, 0, 0.28);
  background: rgba(0, 0, 0, 0.04);
}
.reader.reader-theme-gray .reader-chapter-translate,
.reader.reader-theme-gray .reader-chapters-toggle,
.reader.reader-theme-gray .reader-chapter-nav button,
.reader.reader-theme-black .reader-chapter-translate,
.reader.reader-theme-black .reader-chapters-toggle,
.reader.reader-theme-black .reader-chapter-nav button {
  color: #d6d6d6;
  border-color: rgba(255, 255, 255, 0.28);
  background: rgba(255, 255, 255, 0.06);
}
.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;
}
/* Wraps, because at 390px it does not fit: four buttons need 367px of a 358px
 * row, `nowrap`, and `overflow-x: visible` — so "Chapters" was cut off at the
 * screen edge with nothing to scroll and no way to reach it. */
.reader-chapter-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  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;
}
@media (hover: hover) {
  .reader-chapter-listen:hover {
    filter: brightness(1.1);
  }
}
/* Its own surface and its own text colour, because it never had either.
 *
 * With no `background` declared this picked up a mid grey — rgb(107,107,107)
 * — while every sibling button on the screen sits on `--color-surface`, and
 * `--color-text-faint` on that grey measured 1.68:1 in dark and 2.48:1 in
 * dusk against a 4.5 floor. "Chapters" and "Aa" were the two least readable
 * labels in the app, on the screen you look at longest. */
.reader-chapters-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.5em 0.8em;
  font-size: 0.85rem;
  background: var(--color-surface);
  color: var(--color-text-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
@media (hover: hover) {
  .reader-chapters-toggle:hover {
    color: var(--color-text);
    border-color: var(--color-primary);
  }
}
.reader-article-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}
.reader-article-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.5em 0.9em;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--color-text);
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition:
    color 0.15s,
    border-color 0.15s;
}
@media (hover: hover) {
  .reader-article-btn:hover {
    color: var(--color-primary);
    border-color: var(--color-primary);
  }
}
.reader-article-btn:active {
  filter: brightness(0.95);
}
.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: color-mix(in srgb, var(--color-primary) 8%, transparent);
  box-shadow: inset 2px 0 0 var(--color-primary);
  border-radius: 2px;
}
.reader-para.vs-sel {
  text-decoration: underline dotted var(--color-primary);
  text-underline-offset: 3px;
}

/* Floating action toolbar for text selection (ported from Haven). */
.vs-bar {
  position: fixed;
  left: 0;
  right: 0;
  z-index: var(--z-modal);
  display: flex;
  justify-content: center;
  padding: 0 var(--space-3);
  pointer-events: none;
}
.vs-bar[hidden] {
  display: none;
}
.vs-bar-inner {
  pointer-events: auto;
  width: 100%;
  max-width: 480px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.22);
  padding: var(--space-2) var(--space-3);
}
.vs-bar-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-text-2);
  padding: 2px 4px 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.vs-bar-actions {
  display: flex;
  align-items: stretch;
  gap: 6px;
}
.vs-btn {
  flex: 1 1 0;
  min-height: 44px;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
  padding: 5px 2px;
}
.vs-btn:active {
  background: var(--color-surface-2);
}
.vs-btn-close {
  flex: 0 0 44px;
}
/* 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:active {
  color: var(--color-primary-text);
}
@media (hover: hover) {
  .reader-chapters-listen:hover {
    color: var(--color-primary-text);
  }
}

/* #557: the reading-appearance sheet. Same body-level bottom-sheet pattern as
 * #pl-picker (.picker-inner/.picker-head/.picker-close, reused as-is) — only
 * the overlay positioning is redeclared per-id, same as #pl-picker does. */
#reader-appearance {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  background: rgba(8, 9, 16, 0.55);
  display: flex;
  align-items: flex-end;
}
#reader-appearance[hidden] {
  display: none;
}
.reader-appearance-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.reader-appearance-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.reader-appearance-label {
  font-family: -apple-system, sans-serif;
  font-size: 0.9rem;
  color: var(--color-text-2);
}
.reader-appearance-stepper {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.reader-appearance-stepper button {
  width: 2.2rem;
  height: 2.2rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  color: var(--color-text);
  font-weight: 600;
}
.reader-appearance-value {
  font-family: -apple-system, sans-serif;
  font-size: 0.85rem;
  color: var(--color-text-faint);
  min-width: 5.5em;
  text-align: center;
}
.reader-appearance-toggle {
  display: inline-flex;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  overflow: hidden;
}
.reader-appearance-toggle button {
  padding: 6px 14px;
  font-size: 0.85rem;
  font-weight: 600;
  background: none;
  border: 0;
  color: var(--color-text-faint);
}
.reader-appearance-toggle button[aria-pressed='true'] {
  background: var(--color-primary);
  color: var(--color-on-primary);
}

/* The Page row: five theme choices don't fit the label-left/control-right
 * layout the other rows use without crushing the swatches, so this row
 * stacks the label above them and lets them wrap instead. */
.reader-appearance-row-wrap {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
}
.reader-appearance-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.reader-appearance-swatch-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 6px 10px;
  min-width: 4rem;
  font-size: 0.72rem;
  color: var(--color-text-faint);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
.reader-appearance-swatch-btn[aria-pressed='true'] {
  color: var(--color-text);
  border-color: var(--color-primary);
}
.reader-appearance-swatch {
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 50%;
  border: 1px solid var(--color-border);
}
/* "Default" has no fixed color of its own — it follows the app theme — so
 * its swatch shows that instead of a color it can't commit to: half the
 * app's light surface, half its dark one. */
.reader-appearance-swatch-default {
  background: linear-gradient(135deg, #fafaf7 50%, #1f2233 50%);
}

/* ── 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
/* Sticky header inside the settings scroll container: the search bar and
 * section tabs stay visible while the settings content scrolls under them.
 * The wrapper has a solid background so content doesn't show through. */
.settings-sticky {
  position: sticky;
  top: 0;
  z-index: var(--z-control);
  background: var(--color-bg);
  padding: var(--space-3) 0 var(--space-2);
  margin-bottom: var(--space-3);
}
.settings-find {
  margin-bottom: var(--space-2);
}
.settings-find input {
  width: 100%;
}
/* Standard horizontal tab bar — underline tabs, not pill chips. Scrolls
 * horizontally on narrow screens; the scrollbar is hidden so the active
 * tab's underline reads cleanly. */
.settings-groups {
  display: flex;
  gap: 0;
  overflow-x: auto;
  margin-bottom: 0;
  border-bottom: 1px solid var(--color-border);
}
.settings-groups::-webkit-scrollbar {
  display: none;
}
#settings-body .settings-group,
.settings-group {
  flex: none;
  padding: 8px 12px;
  min-height: 44px;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: none;
  color: var(--color-text-dim);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}
@media (hover: hover) {
  .settings-group:hover {
    color: var(--color-text);
  }
}
#settings-body .settings-group.is-on,
.settings-group.is-on {
  border-bottom-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);
}
.setting-sub {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-divider);
}
.setting-sub h3 {
  font-size: 0.95rem;
  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: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--color-primary);
}
.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);
}
@media (hover: hover) {
  #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;
}

/* ── The selective-import picker (import-picker-ui.js) ─────────────────── */

.impk-summary {
  margin: 0 0 var(--space-3);
  color: var(--color-text-faint);
  font-size: 0.9rem;
}
.impk-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
}
.impk-filter {
  flex: 1 1 160px;
  min-width: 0;
}
.impk-toolbar .linkish[disabled] {
  color: var(--color-text-faint);
  text-decoration: none;
  cursor: default;
}
.impk-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.impk-group + .impk-group {
  margin-top: var(--space-3);
}
.impk-group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-1) 0;
}
.impk-group-head h3 {
  margin: 0;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--color-text-faint);
}
.impk-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-1);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
@media (hover: hover) {
  .impk-row:hover {
    background: var(--color-surface-2);
  }
}
.impk-row-disabled {
  cursor: default;
}
@media (hover: hover) {
  .impk-row-disabled:hover {
    background: none;
  }
}
.impk-cb {
  flex: none;
  accent-color: var(--color-primary);
}
/* A tint, not the kind system's full-saturation colour — right for text on a
 * small badge, same reasoning row.js gives for the equivalent choice on
 * Home's resume card: a dot can be saturated, running text needs the
 * contrast a light background gives it. */
.impk-kind {
  flex: none;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.15em 0.5em;
  border-radius: var(--radius-sm);
  /* --impk-kind-color is supplied inline per row by import-picker-ui.js, from
   * row.js's own kindColor() — genuinely external to this stylesheet, so the
   * fallback is what keeps a row legible if it is ever rendered without one,
   * rather than silently dropping the whole declaration (see this file's own
   * kit-css-tokens test for why a bare var() on a runtime-supplied property
   * is the one thing to avoid). Falls back to the podcast tint, matching
   * kindColor()'s own JS-side default. */
  color: var(--impk-kind-color, #f2a113);
  background: color-mix(in srgb, var(--impk-kind-color, #f2a113) 16%, transparent);
}
.impk-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.92rem;
}
.impk-row-disabled .impk-title {
  color: var(--color-text-faint);
}
.impk-already {
  flex: none;
  font-size: 0.78rem;
  color: var(--color-text-faint);
}
.impk-footer {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
  position: sticky;
  bottom: 0;
  padding-top: var(--space-2);
  background: var(--color-surface);
}

/* ── 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;
}
@media (hover: hover) {
  .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:focus-visible,
.item-ext-lite:focus-visible {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
@media (hover: hover) {
  .item-ext:hover,
  .item-ext-lite:hover {
    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);
}
@media (hover: hover) {
  .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;
}
/* The "tap the video to start it" hint the video surface shows when the queue
 * auto-advances onto a Rumble embed, which cannot be started programmatically.
 * pointer-events:none is load-bearing rather than cosmetic: the tap it is
 * asking for has to reach Rumble's own player underneath, so this must never
 * be able to intercept one. Docked to the top so it is nowhere near the
 * centre play button it is pointing at. */
.vs-rumble-tap {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  pointer-events: none;
  padding: var(--space-2) var(--space-3);
  text-align: center;
  font-size: 0.85rem;
  color: #f5efe0;
  background: rgba(0, 0, 0, 0.72);
}
.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;
  text-decoration: none;
}
.vs-full-channel:focus-visible {
  color: var(--color-primary);
  text-decoration: underline;
}
@media (hover: hover) {
  .vs-full-channel:hover {
    color: var(--color-primary);
    text-decoration: underline;
  }
}
.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:focus-visible {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
@media (hover: hover) {
  .vs-actions button:hover {
    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:focus-visible,
.vs-ext-lite:focus-visible {
  color: var(--color-primary-text);
  border-color: var(--color-primary);
}
@media (hover: hover) {
  .vs-ext:hover,
  .vs-ext-lite:hover {
    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);
}
@media (hover: hover) {
  .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;
}
/* Parked, but underneath the shell instead of above it.
 *
 * The extras drawer (.ps-extras, kit/ui/player-shell.css) opens as an opaque
 * panel that covers the artwork — and the video stage is part of the artwork
 * it covers. But the drawer is a child of .ps, so it paints inside the shell's
 * stacking context at 1005, while this element is fixed at 1006 in the root
 * one. No z-index the drawer could carry reaches over a sibling of its own
 * ancestor: opening About left the video floating over the description text
 * it was supposed to be behind.
 *
 * So the video yields instead. Below 1005 the shell's own panel — and the
 * drawer with it — covers the stage rect completely, which is what "expand
 * over the artwork" already meant for every other stage kind.
 *
 * The park geometry is deliberately left alone: this is a stacking change, not
 * a layout one, so nothing re-measures and the iframe is never touched. The
 * embed keeps playing behind the drawer and comes back in place on close. */
#video-surface.vs-parked.vs-park-behind {
  z-index: var(--z-player-shell-video-under);
}
#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 app layers nothing over the embed at all.
 *
 * A full-surface drag layer came first and blocked every control; #666
 * replaced it with top/side edge zones on the theory that leaving the
 * centre open handed the controls back. It did not — YouTube's captions,
 * settings and volume live along the TOP and its fullscreen control at the
 * RIGHT, so the zones covered the controls and left only the centre's
 * play/pause, which the transport below already offers. Both layers are
 * gone; the swipe lives on the shell's title block instead, wired for every
 * track kind in player-shell.js's _wireStageSwipe. */
#video-surface.vs-parked .vs-drag-layer {
  display: none;
}
#video-surface.vs-parked .vs-frame {
  height: 100%;
  aspect-ratio: auto;
  border-radius: 0;
}
/* Parked inside the turntable's monitor, the whole rect is the embed — same
 * as parked in the shell now, so the drag-layer rule above already covers
 * this state and no second copy of it is needed.
 *
 * The small radius follows the glass it sits in — the monitor's inner corner
 * is 6px, and a square-cornered video inside a rounded one reads as a
 * misplaced overlay, which is exactly what it must not look like. */
#video-surface.vs-parked.vs-park-nodrag {
  border-radius: 4px;
}

/* 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;
}

/* The reverse of #pl-picker: opened from an already-open playlist to search
 * and add episodes into it, rather than opened from a track to choose which
 * playlist. Same bottom-sheet shape and the same .picker-* classes for the
 * shared parts (head, body, empty state, item rows) — only the search input
 * and each row's two-line label are new. */
#pl-add-episodes {
  position: fixed;
  inset: 0;
  z-index: var(--z-player-sheet);
  background: rgba(8, 9, 16, 0.55);
  display: flex;
  align-items: flex-end;
}
#pl-add-episodes[hidden] {
  display: none;
}
.picker-search {
  margin-bottom: var(--space-3);
}
.picker-search input {
  width: 100%;
}
.picker-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.picker-item-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.picker-item-sub {
  color: var(--color-text-faint);
  font-size: 0.8125rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

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

.tabs {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-tabs);
  /* The swipe-up-to-open-omnibar gesture lives on this bar, and it needs the
   * touch. html/body carry `touch-action: manipulation`, which permits both
   * pan axes; Chromium then claims a vertical drag that starts here and
   * delivers `pointerdown` → `pointercancel`, never `pointerup`. The handler
   * in app.js clears its start point on cancel — it has to, that is #442's
   * random-omnibar-pop fix — so the gesture simply never completed.
   *
   * `none` rather than a pan axis: this bar is a fixed five-cell grid with
   * nothing to scroll, so there is no native panning to preserve, and the
   * same treatment is already why .ps-context and .vs-head work. Scoped to
   * the bar, so the page keeps `manipulation` and pinch-zoom stays available
   * in browser tabs — which is what #443 was about. */
  touch-action: none;
  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.45rem + 2px) 0.1rem calc(0.45rem - 2px);
  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;
}
.tab-badge {
  position: absolute;
  top: 2px;
  right: 50%;
  margin-right: -1.75rem;
  min-width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #e8453c;
  border: 1.5px solid var(--color-surface, #0c0e14);
}
:root[data-layout='desktop'] .tab-badge {
  top: auto;
  right: var(--space-2);
  margin-right: 0;
  bottom: 50%;
  margin-bottom: 0.7rem;
}

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

.home-card {
  margin-bottom: var(--space-5);
}
.home-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  /* "New in <persona>" plus the unplayed count, refresh and Card feed
   * controls does not fit one line at phone width — align-items: baseline
   * on an un-wrapped row then let the title's own text wrap independently
   * of the actions cluster, staggering the two against each other and
   * wrapping "Card feed" itself mid-word (reported: title split across two
   * lines with the controls floating above it, none of it aligned). Wrapping
   * the row itself, rather than letting each side wrap on its own, keeps
   * the title one block and the actions another — whichever runs out of
   * room drops to its own line intact instead of both fragmenting. */
  flex-wrap: wrap;
  gap: var(--space-2) 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.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-text-dim, var(--color-text-faint));
  margin: 0;
  /* The one thing allowed to shrink — the actions cluster is a fixed set of
   * tap targets that must stay whole, so a long title is what gives up room
   * first, down to wrapping its own text across two lines rather than
   * colliding with the controls. No min-width floor: this card's title is
   * now the constant short string "New" (was the persona-length-dependent
   * "New in <persona>"), and a percentage floor here — sized for the old,
   * longer phrase — was itself claiming enough of the row's hypothetical
   * flex size to force an unwanted wrap even for a three-character title. */
  flex: 1 1 auto;
}
.home-card-head-actions {
  display: flex;
  align-items: center;
  /* 6px rather than --space-2 (8px): with the kind filter now in this row,
   * five controls at 8px gaps measured 335px against a 328px card at 360px
   * wide — 7px over, which forced a wrap and made the header TALLER than the
   * separate filter row it was meant to replace. The four gaps give back
   * exactly the 8px needed to keep it one line there. */
  gap: 6px;
  /* Prefers to stay one unwrapped line — see .home-card-head's flex-wrap
   * note. If it doesn't fit beside the title it drops to its own line whole;
   * margin-left: auto keeps it flush right there too, matching where
   * space-between would have put it on a single line.
   *
   * It may now wrap internally as a last resort, which it could not when it
   * held four controls. The kind filter moved in here (it used to own a row
   * below the head), and at 360px and under the five controls are wider than
   * the card: with `flex: none` that overflowed the card's right edge
   * outright. Wrapping puts the filter — last in source order — on a second
   * line instead, which is the same right-aligned position it used to have,
   * reached only on the narrow screens that actually need it. */
  flex: 0 1 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
  margin-left: auto;
}
/* Two breakpoints, both sized from measurement rather than from round
 * numbers, because the failure they exist to prevent is a layout that comes
 * out differently on different machines.
 *
 * At 360px the title, the head's 12px gap and the five controls came to
 * exactly the card's 328px — zero slack — so which of two layouts you got
 * depended on the font metrics of the machine rendering it. Review measured
 * 106px where this branch measured 68px, on the same commit. Neither number
 * was wrong; the CSS was.
 *
 * 24rem: below it the cluster takes a line of its own rather than competing
 * with the title for one. Left to `flex: 0 1 auto` it does not drop cleanly —
 * the title shrinks to its min-content "NEW" first, and then the cluster,
 * still nominally fitting, shrinks and wraps INTERNALLY, putting the filter
 * on a third line and making the header taller than the separate row this
 * change removed. Above 24rem the cluster shares the title's line with at
 * least 25px to spare; below it, it has its own line with 48px to spare at
 * 360px. (At 320px that margin is 8px, and if a wider font tips it the
 * filter wraps to a second cluster line — 92px, still shorter than the
 * 108px this replaced.)
 *
 * 27rem: below it the count drops its word (see _syncNewCount). "unplayed"
 * costs 47px, and carrying it needs a 401px card — 433px of viewport — to
 * keep 32px of slack. The word stays in the accessible name at every width;
 * only the pixels go.
 *
 * In rem, not px, so both track the user's text size: the wider the type,
 * the wider the cluster, and the sooner it should stop sharing a line. */
@media (max-width: 24rem) {
  .home-card-head-actions {
    flex-basis: 100%;
  }
}
@media (max-width: 27rem) {
  .home-new-count-word {
    display: none;
  }
}
/* 320px is the narrowest width this app supports, and the five controls fit
 * it with 8px to spare — measured here. On the review machine the same
 * cluster measures 284px, so the same 8px is 4px there, under the floor
 * scripts/e2e_home_kind_filter_inline.mjs sets for exactly this reason
 * (#1319). A floor the layout only clears on one machine is the defect that
 * suite exists to catch, so this widens the margin rather than lowering the
 * bar: the four gaps give back 8px, roughly doubling what is left over.
 * Measured after the fix: 16px here, 12px on the review machine — the same
 * 4px spread, now sitting well clear of the floor instead of straddling it.
 * Both numbers are recorded deliberately; a single number here would be the
 * machine-specific claim that caused this in the first place.
 *
 * The gaps are the only slack worth taking. The two icon buttons are 44px
 * because that is the tap-target size, and shrinking a tap target to win
 * layout is what caused #1320 — 4px between two 44px targets still leaves
 * them distinct and comfortably over the 24px floor. */
@media (max-width: 20rem) {
  .home-card-head-actions {
    gap: 4px;
  }
}

/* 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 {
  position: relative;
  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);
}
/* Swipe-to-remove on the Continue card (reported: it should be dismissable
 * the same way New-in-subscriptions rows are). .swipe-content's own rule
 * further down this file is written for the flat, divider-separated list-row
 * shape (background: var(--color-bg), 9px/14px padding, 11px gap) — .home-resume
 * is deliberately a bordered, gradient-backed "hero" card instead, and equal
 * selector specificity would otherwise let source order silently decide
 * which one wins. The wrapper also needs the card's own border-radius so a
 * partial swipe doesn't show a square corner poking out from behind the
 * rounded card. */
.home-resume-row.swipe-row {
  border-radius: var(--radius-md);
}
.home-resume-row .swipe-content {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  background: linear-gradient(180deg, var(--color-surface-2), var(--color-surface));
}
@media (hover: hover) {
  .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;
}
.home-resume-prog {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: rgba(0, 0, 0, 0.2);
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  overflow: hidden;
}
.home-resume-prog-fill {
  display: block;
  height: 100%;
  background: var(--color-primary);
  transition: width 0.3s linear;
}

/* 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;
}
.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 .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;
  min-width: 44px;
  min-height: 44px;
  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:focus-visible {
  background: none;
  border-color: var(--accent, var(--color-primary));
  color: var(--accent, var(--color-primary));
}
@media (hover: hover) {
  button.home-row-action:hover {
    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;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text-dim, var(--color-text-faint));
  display: grid;
  place-items: center;
}
@media (hover: hover) {
  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));
}
.home-new-count-btn {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  /* WCAG 2.5.8: at its own text height (14px) this fell under the 24px
   * floor — masked before only by the header-wrap bug accidentally
   * stretching it across two lines. Its row-mates (.home-refresh-btn,
   * .home-cardfeed-btn) already claim 44px, which the row is already tall
   * enough for; matching that rather than the 24px floor means this tap
   * target actually fills the space it visually sits in instead of being a
   * small hot-zone floating inside a taller row. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  /* WCAG 2.5.8 sizes width as well as height, and this label is now short
   * enough to fall under it: "7 ▾" measures 13px wide, "20 ▾" 21px. The
   * height was already handled; nothing checked the other axis because the
   * label used to read "View N unplayed ▾" and was never near the floor.
   * 24px is that floor — the same number scripts/e2e_contrast.mjs asserts.
   *
   * It also makes the cluster's width independent of how many digits the
   * count has: 1, 2 and 3 digits now all measure 24px, so the header cannot
   * be laid out one way for a subscriber with 9 unplayed and another way for
   * one with 900. */
  min-width: 24px;
}
@media (hover: hover) {
  .home-new-count-btn:hover {
    /* --color-text-soft is the KIT's name for this step (see kit/ui/player.css's
     * token list); app.css's own scale is --color-text / --color-text-2 /
     * --color-text-faint, so the kit name resolves to nothing here. An undefined
     * custom property with no fallback drops the whole declaration, so this hover
     * had no colour at all — caught by test_kit_css_tokens. Brightening to
     * --color-text matches every other :hover in this file. */
    color: var(--color-text);
  }
}
.home-new-count-btn.expanded {
  font-weight: 600;
}
/* Tappable artwork on Home rows — wraps the ep-row-art in a button */
.ep-row-art-btn {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.ep-row-art-btn:active {
  opacity: 0.7;
}
.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;
  /* The row owns horizontal drags (swipe left/right); vertical scrolling
   * stays native so the list scrolls when a drag starts on a row. pan-y
   * lets the browser handle vertical from gesture start — touch-action:none
   * would suppress native scroll entirely, and wireRowSwipe() yielding on
   * |dy|>|dx| can't re-enable what the UA already decided not to start. */
  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;
  /* .item-detail (the expanded show-notes box, appended as a sibling of the
   * art/title/actions — see episodeRow()'s `after` slot in row.js) declares
   * flex-basis: 100% so it takes its own full-width line when expanded.
   * That only works with flex-wrap: wrap here — without it, a 100%-basis
   * item still shares the single line every other flex item is confined to,
   * squeezed into whatever width is left instead of wrapping below. The
   * collapsed row (art + title button + actions) has always fit on one
   * line and continues to; this only changes what happens once a fourth,
   * wide item is added. */
  flex-wrap: wrap;
  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. */
/* Stretch to the row, not to the text.
 *
 * `all: unset` leaves this sizing to its content, so the primary tap target on
 * every list row measured 34px tall inside a 65px row — half the row was dead
 * space on the one control the user aims at most. `align-self: stretch`
 * overrides the parent's `align-items: center` and fills the flex LINE, which
 * with `.ep-row`'s flex-wrap means the collapsed row only; an expanded
 * .item-detail takes its own line and is not swallowed. `justify-content:
 * center` keeps the text where it already sat, so nothing moves. */
.ep-row-main {
  all: unset;
  flex: 1;
  min-width: 0;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-self: stretch;
  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;
  /* 14px, not 6px, so the 44px hit areas below tile edge-to-edge instead of
     overlapping. Two 30px buttons 6px apart sit 36px centre-to-centre, and two
     44px targets at that spacing overlap by 8px — the later one in DOM order
     would swallow taps aimed at the glyph of the earlier one, which is worse
     than the small target it replaced. The 8px this costs comes out of the
     title, which already truncates; a mis-tap that plays the wrong thing is a
     bigger tax than eight pixels of a title you can still read. */
  gap: 14px;
}
/* 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,
/* 30px is what you SEE; 44px is what you can HIT.
 *
 * Measured on All Episodes at 390px: 48 sub-44px targets on one screen, 24 of
 * them these buttons at 30x30 — on a phone-first app, two of them 6px apart.
 * Growing the buttons themselves would cost ~28px of every row's title, so the
 * visual box stays 30px and a pseudo-element carries the touch target out to
 * 44x44. Row height is untouched: the row is 65px, so 44 fits with room.
 *
 * `position: relative` on the button and a centred, transform-positioned
 * ::after rather than negative insets on all four sides. A swipeable row is
 * `.ep-row.swipe-row`, which IS `overflow: hidden` — every list that offers
 * swipe actions draws one — and inset-based growth is the kind that gets
 * silently clipped at that edge. (Plain `.ep-row` does not clip; an earlier
 * version of this comment said it did.) */
.ep-row-action {
  position: relative;
  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::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.ep-row-action svg {
  display: block;
}
button.ep-row-action:focus-visible,
.ep-row-action:focus-visible {
  background: none;
  border-color: var(--accent, var(--color-primary));
  color: var(--accent, var(--color-primary));
}
@media (hover: hover) {
  button.ep-row-action:hover,
  .ep-row-action:hover {
    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);
}
.ep-row-action-dl {
  --accent: #5b8def;
}
.ep-row-action-dl.is-downloaded {
  --accent: #4caf50;
  border-color: var(--accent);
  color: var(--accent);
}
.ep-row-action-dl.is-downloading {
  opacity: 0.5;
  cursor: wait;
}
/* 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:focus-visible {
  border: 0;
  color: var(--color-text);
}
@media (hover: hover) {
  button.ep-row-quick:hover {
    border: 0;
    color: var(--color-text);
  }
}
@media (hover: hover) {
  button.ep-row-quick.is-on:hover {
    color: var(--color-primary-text);
  }
}
button.ep-row-more:focus-visible {
  border: 0;
  color: var(--color-text);
}
@media (hover: hover) {
  button.ep-row-more:hover {
    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;
}
/* 36x32, not 44x44, and the shortfall is deliberate.
 *
 * These two sit in a segmented control 2px apart — 30px centre to centre — so
 * 44px targets would overlap by 14px and each would swallow taps meant for the
 * other. Growing the buttons themselves to 44 would make a list/grid toggle
 * 94px wide in a header that is already tight at 320px. 36x32 clears WCAG
 * 2.5.8's 24x24 with room and is a 58% bigger target than 28x26; it does not
 * reach 2.5.5's 44x44, which is a real gap and is recorded as one rather than
 * quietly rounded up. */
.lib-layout-btn {
  width: 36px;
  height: 32px;
  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-primary-highlight);
  color: var(--color-primary-text);
}
.lib-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 14px;
}
/* The pill is 44 tall and the field inside it fills the pill.
 *
 * `height: 38px` on the wrapper with a text input left to its own line box
 * measured 163x21 for the thing a finger actually has to land on: the wrapper
 * is a plain div with no label association, so tapping its padding focuses
 * nothing. Same shape on `.notes-search`, `.help-search` and `.dsc-search`. */
.lib-search {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  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;
  /* Fills the pill rather than sitting on its own line box — the wrapper is a
     plain div, so its padding focuses nothing and the input's own 21px box was
     the whole target. */
  align-self: stretch;

  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: 24px;
  height: 24px;
  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);
}
@media (hover: hover) {
  .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;
}
/* Matches .browse-chip / .dsc-chip: 44 tall, in a horizontal scroller where
 * height cannot collide with anything. */
.lib-chip {
  flex: none;
  min-height: 44px;
  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;
}
@media (hover: hover) {
  .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.is-selected {
  background: var(--color-surface);
}
@media (hover: hover) {
  .lib-row:hover {
    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;
}
/* Same as .ep-row-main above, same reason. */
.lib-row-main {
  all: unset;
  flex: 1;
  min-width: 0;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-self: stretch;
  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;
}
/* Kind, not state — deliberately unlike `.ep-row-dot`.
 *
 * The episode row's dot marks "new"; this one marks what KIND of source the
 * row is, and sits in the meta line beside the word for it ("Daily · 5
 * episodes", "YouTube · 3 videos") where it reads as a bullet. A source is
 * never unread, so there is no state for it to carry. Two dots, two meanings,
 * on two different kinds of row. */
.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-3);
  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;
}
@media (hover: hover) {
  .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);
}
/* The label is the target: a 16px checkbox inside a label measured 109x18,
 * and it is the label a thumb aims at, not the box. */
.lib-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  min-height: 44px;
  font-size: 0.85rem;
  cursor: pointer;
}
/* #816: Accordion sections for the source options sheet. */
.lib-sheet-accordion {
  border-top: 1px solid var(--color-divider);
  padding: 10px 0;
}
.lib-sheet-accordion:first-child {
  border-top: 0;
  padding-top: 0;
}
.lib-sheet-accordion > summary {
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-3);
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.35em;
}
.lib-sheet-accordion > summary::-webkit-details-marker {
  display: none;
}
.lib-sheet-accordion > summary::before {
  content: '▸';
  font-size: 0.8em;
  transition: transform 0.15s ease;
}
.lib-sheet-accordion[open] > summary::before {
  transform: rotate(90deg);
}
.lib-sheet-accordion-body {
  padding-top: 10px;
}
.lib-sheet-accordion-body 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-sheet-linked-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
}
.lib-sheet-linked-name {
  flex: 1;
  font-size: 0.8125rem;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A source reached through a third source, rather than one this page was
 * linked to directly. It belongs in the list — it is part of the creator —
 * but there is no edge here to sever, so it states how it arrived instead of
 * offering a button that would remove nothing. */
.lib-sheet-linked-via {
  font-size: 0.6875rem;
  color: var(--color-text-dim);
  white-space: nowrap;
}
.lib-sheet-linked-unlink,
.lib-sheet-linked-link {
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 4px 8px;
  font-size: 0.75rem;
  background: var(--color-surface);
  color: var(--color-text);
  cursor: pointer;
}
@media (hover: hover) {
  .lib-sheet-linked-unlink:hover,
  .lib-sheet-linked-link:hover {
    border-color: var(--color-border-strong);
  }
}
.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-source-link {
  background: none;
  border: 0;
  /* WCAG 2.5.8 floors a pointer target at 24×24. At font-size 0.82rem this
   * button measured 16–17px tall, so it needs the height explicitly; the
   * inline-flex centring keeps the label sitting on the meta row's baseline
   * exactly where it did before, since .feed-card-meta is already
   * align-items: center. Horizontal padding stays 0 — the label is wider
   * than 24px on its own, and padding here would break the "Show · 12 min"
   * dot spacing. */
  min-height: 24px;
  display: inline-flex;
  align-items: center;
  padding: 0;
  font: inherit;
  font-size: 0.82rem;
  color: var(--color-text-2);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 2px;
  transition: text-decoration-color 0.15s;
}
@media (hover: hover) {
  .feed-card-source-link:hover {
    text-decoration-color: var(--color-primary);
    color: var(--color-primary);
  }
}
.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;
}
@media (hover: hover) {
  .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);
}
.feed-card-action-btn:disabled,
.feed-card-action-btn.is-disabled {
  opacity: 0.5;
  cursor: default;
}
/* 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);
}
@media (hover: hover) {
  .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;
}
@media (hover: hover) {
  .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);
}
@media (hover: hover) {
  .feed-card-tool-keep:hover {
    border-color: #4fbfa0;
    color: #4fbfa0;
  }
}
@media (hover: hover) {
  .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;
}
.feed-card-tool-autoplay {
  flex: 0 0 auto;
  width: 42px;
  display: grid;
  place-items: center;
  color: var(--color-text-dimmer);
}
.feed-card-tool-autoplay.is-on {
  background: var(--color-primary-highlight);
  color: var(--color-primary-text);
}

/* 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);
}
/* #813: 2-column grid for the player's More menu — episode-specific actions
 * only, laid out as a clean grid rather than a wrapping flex row. */
.track-detail-actions-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.track-detail-actions-grid .feed-card-action-btn {
  justify-content: flex-start;
}

/* 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. */
/* The Settings → Personas section reuses .persona-row (shared with the top
 * chip's sheet, see personaRowsHTML() in app.js), but the sheet supplies its
 * own boxed/scrollable container — inline in Settings there is none, so this
 * gives the list the same bordered-box treatment as the Voices section's
 * .vc-order rather than rows floating directly on the settings background. */
#setting-personas-body {
  margin-top: 12px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  overflow: hidden;
}
.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 {
  background: var(--color-primary-highlight);
  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);
}
@media (hover: hover) {
  .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;
}
@media (hover: hover) {
  .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);
}
/* Reorder toggle button in the persona sheet header */
.persona-sheet-reorder {
  position: absolute;
  top: var(--space-4);
  right: calc(var(--space-4) + 28px);
  background: none;
  border: 0;
  color: var(--color-primary-text);
  font-size: 0.9rem;
  font-weight: 500;
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
}
.persona-sheet-reorder.is-active {
  color: var(--color-primary);
}
/* Reorder-mode rows */
.persona-reorder-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-1);
}
.persona-reorder-row.is-dragging {
  opacity: 0.4;
}
.persona-drag-handle {
  flex: none;
  background: none;
  border: 0;
  color: var(--color-text-faint);
  cursor: grab;
  padding: var(--space-1);
  touch-action: none;
}
.persona-drag-handle:active {
  cursor: grabbing;
}
@media (hover: hover) {
  .persona-drag-handle:hover {
    color: var(--color-text);
  }
}
.persona-reorder-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
}
.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;
}
@media (hover: hover) {
  .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);
}
/* #809: Persona multi-select dropdown from the source detail header. */
.persona-dropdown {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  background: rgba(8, 9, 16, 0.55);
  display: flex;
  align-items: flex-end;
}
.persona-dropdown[hidden] {
  display: none;
}
.persona-dropdown-backdrop {
  position: absolute;
  inset: 0;
}
.persona-dropdown-panel {
  position: relative;
  width: 100%;
  max-height: 50vh;
  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-dropdown-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-divider);
  font-size: 0.9rem;
  font-weight: 600;
}
.persona-dropdown-close {
  background: var(--color-surface-2);
  border: 0;
  border-radius: var(--btn-radius);
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  color: var(--color-text-faint);
  cursor: pointer;
}
.persona-dropdown-body {
  overflow-y: auto;
  padding: var(--space-3) var(--space-4);
  padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
}

/* 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;
}
#s-reader > .screen-scroll {
  overflow-x: hidden;
}

.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;
}
@media (hover: hover) {
  .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;
  min-height: 44px;
  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;
}
@media (hover: hover) {
  .home-chip:hover {
    border-color: var(--color-primary);
  }
}
/* `.home-chip` sets `display`, so the `hidden` attribute alone loses to it and
 * a chip marked hidden renders anyway — this sheet has no global
 * `[hidden] { display: none }` to fall back on. Nothing hides a chip today,
 * which is exactly why this is worth writing down: the first code that tries
 * would fail silently and look like a state bug rather than a CSS one. Found
 * by a revert that refused to redden (#1418). */
.home-chip[hidden] {
  display: none !important;
}
.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;
}

/* ── First run (home.js's firstRunHTML) ─────────────────────────────────── */

.home-first-run {
  padding-top: var(--space-2);
}
.home-fr-lede {
  margin: 0 0 var(--space-5);
  font-size: 1rem;
  line-height: 1.55;
  color: var(--color-text);
}
/* A decoy field, not an input: tapping it opens the omnibar, which owns the
 * real search and focuses its own input. A <button> so it is one tab stop and
 * one tap target — which means resetting the UA button chrome explicitly,
 * since that is exactly how .source-meta ended up with a grey background and
 * an outset border while every behavioural test stayed green. Styled off
 * .dsc-search so search looks the same everywhere it appears, minus the
 * sticky positioning that only makes sense over a result list. */
.home-fr-search {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  height: 44px;
  padding: 0 12px;
  margin: 0 0 var(--space-5);
  font: inherit;
  font-size: 0.9375rem;
  text-align: left;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: 10px;
  color: var(--color-text-faint);
  cursor: pointer;
}
.home-fr-search [data-icon],
.home-fr-search svg {
  flex: none;
}
@media (hover: hover) {
  .home-fr-search:hover {
    border-color: var(--color-primary);
  }
}

/* Five items, one row of equal weight — the whole point being that none of
 * them reads as the headline. auto-fit rather than a fixed column count so
 * it reflows from a single column on a phone to a full row on desktop
 * without a breakpoint to maintain. */
.home-fr-caps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}
.home-fr-cap {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  font-size: 0.875rem;
  line-height: 1.35;
  color: var(--color-text-2, var(--color-text));
}
.home-fr-cap [data-icon],
.home-fr-cap svg {
  flex: none;
  color: var(--color-primary-text);
}
.home-fr-sub {
  margin: 0 0 var(--space-2);
  color: var(--color-text-faint);
  font-size: 0.875rem;
  line-height: 1.5;
}
/* The featured shelf's Listen affordance — a small pill over the cover's
 * bottom edge, not a full-cover hover treatment, because the cards need to
 * work identically on touch (no hover at all) and pointer. Always visible
 * rather than a hover-reveal for the same reason: the entire point of this
 * shelf is proving the Listen action exists, so it cannot be the one thing
 * hidden until you already knew to look. */
.home-fr-listen {
  position: absolute;
  left: var(--space-2);
  right: var(--space-2);
  bottom: var(--space-2);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35em;
  padding: 0.3em 0;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--color-primary) 88%, transparent);
  color: #1c1810;
  font-size: 0.72rem;
  font-weight: 600;
}
.browse-card.is-loading .home-fr-listen {
  opacity: 0.6;
}

/* ── 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;
  /* The kit docks its player bar and full-screen shell to the viewport.
   * Left, not just bottom: without this the bar runs under the sidebar and
   * the player shell's video stage spans the full viewport width, making
   * a 16:9 video overflow the viewport height and shift off-screen. */
  --pk-dock-bottom: 0px;
  --pk-dock-left: var(--sidebar-w);
}

/* §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 */
  }
}
: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.is-dragging {
  background: var(--color-primary);
  opacity: 0.5;
}
@media (hover: hover) {
  :root[data-layout='desktop'] .rail-resizer:hover {
    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;
}
@media (hover: hover) {
  :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;
  /* Rows size to what is in them, and the container scrolls when that is more
   * than fits — which is what `.screen-scroll` is for. Without this, a grid row
   * whose item is a horizontal scroller can be squeezed below its content,
   * because a scroll container's automatic minimum size is zero: Library's
   * collection strip took a 61px row for 73px of card and lost the sub-line
   * off every one of them the moment the chip row above it grew by 12px. It is
   * the same failure the `flex: none` on `.lib-collections` was added for, one
   * layout along, where a flex property no longer says anything. */
  grid-auto-rows: max-content;
}
/* 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: 48px;
    --pk-dock-bottom: 48px;
    --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;
}
#offline-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(140, 100, 20, 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);
}
#offline-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;
}
@media (hover: hover) {
  .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);
  background: var(--color-primary-highlight);
  color: var(--color-primary-text);
}
@media (hover: hover) {
  .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);
  background: var(--color-primary-highlight);
  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;
  min-height: 44px;
  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;
  /* Fills the pill rather than sitting on its own line box — the wrapper is a
     plain div, so its padding focuses nothing and the input's own 21px box was
     the whole target. */
  align-self: stretch;

  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;
}
@media (hover: hover) {
  .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;
}
@media (hover: hover) {
  .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;
}
@media (hover: hover) {
  .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:active {
  background: var(--color-surface-2);
  color: var(--color-text);
}
@media (hover: hover) {
  .ned-tool:hover {
    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: 24px;
  height: 24px;
  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);
  background: var(--color-primary-highlight);
}
.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;
}
@media (hover: hover) {
  .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;
}
/* Refresh is icon-only — a text label next to "Card feed" made two actions
 * read as one phrase ("Refresh Card feed"). The icon alone plus its
 * aria-label/title covers both sighted and AT users. */
/* Play all: icon-only, same 44px target and treatment as Refresh beside it.
 * Deliberately not a labelled chip like "Card feed" — this header row only
 * just stopped wrapping at phone width, and a fourth worded control would
 * spend the margin that fix bought. */
.home-playall-btn,
.home-refresh-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-2, var(--color-text));
  cursor: pointer;
}
@media (hover: hover) {
  .home-playall-btn:hover,
  .home-refresh-btn:hover {
    color: var(--color-primary-text);
  }
}
/* Card feed reads as a destination (it opens a whole screen), not a word in a
 * sentence, so it gets a bordered chip like .item-archive-btn rather than
 * .linkish's underlined-text treatment. */
.home-cardfeed-btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.75rem;
  cursor: pointer;
}
@media (hover: hover) {
  .home-cardfeed-btn:hover {
    border-color: var(--color-primary);
    color: var(--color-primary-text);
  }
}

/* ── 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);
}
/* min-height 44, not padding: a chip row is 21 targets on the Sources screen
 * and every one of them measured 44x31. Height is the axis with slack here —
 * these wrap, so a taller chip costs a taller block and nothing else, while
 * widening them would push the row into a scroll. */
.browse-chip {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  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);
}
@media (hover: hover) {
  .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;
}
@media (hover: hover) {
  .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);
}
/* Sub-shelf heading within a browse section (e.g. "Trending This Week"). */
.browse-shelf {
  margin-bottom: var(--space-4);
}
.browse-shelf-heading {
  font-size: 0.875rem;
  font-weight: 600;
  margin: 0 0 var(--space-2);
  color: var(--color-text-2);
}
/* 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;
  /* Fills the pill rather than sitting on its own line box — the wrapper is a
     plain div, so its padding focuses nothing and the input's own 21px box was
     the whole target. */
  align-self: stretch;

  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;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 12px;
  /* 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 wraps to ~76px (2 rows of
 * 30px chips + padding), so the results scopes pin below both. */
.dsc-types {
  top: 44px;
}
#search-scope {
  top: 120px;
}
/* 36 → 44 tall and a 44 floor on width: measured 37x36, the narrowest chip in
 * the app, and 17 of them on one screen. This row scrolls horizontally, so
 * neither axis can collide with a neighbour. */
.dsc-chip {
  flex: none;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  min-height: 44px;
  min-width: 44px;
  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;
}
@media (hover: hover) {
  .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;
}
/* #453: Discover episode/video/archive rows — tapping the meta opens a
 * detail sheet. Only the Discover result rows, not every .source-meta. */
.dsc-group .source-row .source-meta {
  cursor: pointer;
}
.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;
}
@media (hover: hover) {
  .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-head {
  display: flex;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.dsc-book-detail-cover {
  flex: none;
  width: 48px;
  height: 72px;
  object-fit: cover;
  border-radius: 4px;
}
.dsc-book-detail-info {
  min-width: 0;
}
.dsc-book-detail-title {
  margin: 0 0 4px;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.3;
}
.dsc-book-detail-author {
  margin: 0;
  font-size: 0.75rem;
  color: var(--color-text-faint);
}
.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-suggestions {
  margin-top: 14px;
}
.dsc-suggestions-label {
  margin: 0 0 8px;
  font-size: 0.71875rem;
  color: var(--color-text-faint);
}
.dsc-suggestion-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.dsc-suggestion-chip {
  font-size: 0.8125rem;
}
.dsc-suggestions-below {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--color-border);
}
.dsc-suggestion-type {
  font-size: 0.625rem;
  color: var(--color-text-faint);
  margin-left: 4px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.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;
}
@media (hover: hover) {
  .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);
}
#pd-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;
}
@media (hover: hover) {
  .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;
  /* align-items defaults to stretch, which would size .omni-panel to this
   * container's full height regardless of how little the panel actually has
   * to show (reported: taps below a short results list landed on that empty
   * stretched panel box instead of the backdrop, so they didn't dismiss it).
   * flex-start lets the panel size to its own content, up to its max-height
   * cap, so only the panel's real visible extent is there to be tapped. */
  align-items: flex-start;
  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;
  /* Without this, a result list long enough to hit max-height would spill
   * past the panel's own box instead of being clipped by it — the results
   * list below is the part meant to scroll internally at that point. */
  overflow: hidden;
  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;
  /* The shrinkable part of .omni-panel's column: a column flex item's
   * min-height defaults to auto (its content size), which would otherwise
   * keep growing this past the panel's max-height instead of scrolling
   * internally once the panel's own cap is hit. */
  flex: 1 1 auto;
  min-height: 0;
  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 {
  background: var(--color-surface-2);
}
@media (hover: hover) {
  .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;
}
/* Artwork fills the tile; the kind colour underneath is what shows while the
 * image loads and what is left when there is none. `overflow: hidden` so a
 * non-square cover is cropped to the tile rather than squashed into it —
 * podcast art is square, YouTube avatars are square, Feedly icons are
 * whatever the blog uploaded. */
.omni-tile-art {
  overflow: hidden;
}
.omni-tile-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The Discover group's rows are things you do not have yet. Same shape as
 * Open — this is a list of sources either way — with the accent used
 * everywhere else in the app for "not yours yet". */
.omni-tile-find {
  border-radius: 5px;
}
.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;
  flex-shrink: 0;
}
.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;
  flex-shrink: 0;
}
.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);
}
@media (hover: hover) {
  .cr-secondary:hover {
    border-color: var(--color-border-strong);
  }
}

.cr-group {
  margin-top: 18px;
  flex-shrink: 0;
}
.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;
}
@media (hover: hover) {
  .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);
}

/* Why a source did not load, listed under the rows that did — or on its own
 * when nothing loaded at all. Dim rather than alarming: a channel the server
 * could not list is a normal outcome on Rumble, and the reader's next move is
 * to try again shortly, not to panic. */
.cr-problems {
  margin: 6px 0 0;
  padding-left: 18px;
  font-size: 0.75rem;
  color: var(--color-text-dim);
}
.cr-problems li {
  margin: 2px 0;
}
.cr-problems-note {
  padding: 10px 0 4px;
}

/* ── Creator page tabs ─────────────────────────────────────────────────── */
.cr-tabs {
  display: flex;
  gap: 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  border-bottom: 1px solid var(--color-border);
  margin: 12px 0 0;
  padding: 0;
  flex-shrink: 0;
}
.cr-tabs::-webkit-scrollbar {
  display: none;
}
.cr-tab {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 14px;
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--color-text-dim);
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition:
    border-bottom-color 0.15s,
    color 0.15s;
}
.cr-tab.is-on {
  color: var(--color-primary-text);
  border-bottom-color: var(--color-primary);
}
/* Needs an ID selector to win, exactly as #source-open-art above does: a
 * single class (.cr-tabs) has the same specificity as [hidden], so the
 * later-declared `display: flex` applies regardless of the attribute. The
 * show-detail strip is emitted hidden and stays hidden for a single-format
 * show — and without this it kept its 12px margin and 1px bottom border and
 * drew a stray horizontal rule under the Episodes label. Measured at 1px
 * tall with `hidden` set, which is why asserting the property rather than
 * the computed style missed it. */
#source-tabs[hidden] {
  display: none;
}
.cr-tab-count {
  font-size: 0.625rem;
  color: var(--color-text-dimmer);
  font-variant-numeric: tabular-nums;
}
.cr-tab-body {
  min-height: 60px;
}
/* ── Creator source link suggestions ───────────────────────────────────── */
.cr-suggestions {
  margin-top: 18px;
  padding: 12px;
  border-radius: 8px;
  background: var(--color-surface-2);
  flex-shrink: 0;
}
.cr-suggestion-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 0;
}
.cr-suggestion-names {
  font-size: 0.8125rem;
  color: var(--color-text-dim);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cr-suggestion-btn {
  flex: none;
  padding: 4px 12px;
  border: 1px solid var(--color-primary);
  border-radius: 6px;
  background: none;
  color: var(--color-primary-text);
  font-size: 0.75rem;
  cursor: pointer;
}
.cr-tab-body {
  min-height: 60px;
  flex-shrink: 0;
}
/* ── 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,
.sd-playall {
  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;
}
/* One filled button per header, and it goes to the action. The amber used to
 * sit on `.sd-follow.is-on`, making a status toggle the loudest thing on a show
 * page; Following drops to the tint `.sd-persona.is-on` already uses. */
.sd-playall {
  border-color: var(--color-primary);
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-weight: 600;
}
.sd-follow.is-on,
.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;
}
@media (hover: hover) {
  .sd-more:hover {
    border-color: var(--color-border-strong);
    color: var(--color-text);
  }
}
.sd-prefer-audio {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 0;
  font-size: 0.8125rem;
  color: var(--color-text-dim);
  cursor: pointer;
}
.sd-prefer-audio input {
  margin: 0;
}
/* 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;
}
@media (hover: hover) {
  .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;
}

/* #555 diagnostic panel CSS removed — issue diagnosed (Kokoro disabled on iOS Safari). */
/* ── 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;
}
.stat-opt-out {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.9em;
  color: var(--color-text-dim);
}
#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;
}
@media (hover: hover) {
  .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;
}
@media (hover: hover) {
  .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);
}

.vc-override-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--color-divider);
}
.vc-override-row:first-of-type {
  border-top: 0;
}
.vc-override-label {
  flex: 0 0 auto;
  min-width: 5rem;
  max-width: 10rem;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vc-override-row .voice-picker-row {
  flex: 1 1 auto;
  margin-top: 0;
}
/* 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;
}
@media (hover: hover) {
  .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);
}

/* ── The station page (#s-station) ───────────────────────────────────────── */
/* Its own screen because a station is not a list. See station-ui.js's header
   for why it isn't a kind inside source detail. */

#station-body {
  padding: var(--space-4) var(--space-3) var(--space-6);
}
.st-head {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.st-back {
  position: absolute;
  top: 0;
  left: 0;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--color-surface-2);
  color: var(--color-text);
  cursor: pointer;
}
.st-art {
  width: 112px;
  height: 112px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  object-fit: cover;
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
  color: var(--color-text-faint);
}
.st-title {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.25;
}
.st-sub {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--color-text-faint);
}

/* Listen leads. Follow is deliberately the quieter of the two: the reason to
   open a station page is to hear it, and joining it is the second thought. */
.st-actions {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.st-listen {
  flex: 1 1 auto;
}
.st-listen,
.st-follow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
}
.st-follow {
  flex: 0 0 auto;
  padding: 0 var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--btn-radius);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
}
.st-follow.is-followed {
  background: var(--color-primary-highlight);
  color: var(--color-primary-text, var(--color-primary));
  border-color: transparent;
}

.st-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: var(--space-3);
}
.st-tag {
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--color-surface-2);
  color: var(--color-text-2);
  font-size: 0.75rem;
}

.st-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px var(--space-3);
  margin: 0 0 var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface-2);
}
.st-facts dt {
  color: var(--color-text-faint);
  font-size: 0.8125rem;
}
.st-facts dd {
  margin: 0;
  color: var(--color-text-2);
  font-size: 0.8125rem;
}

.st-site {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: var(--space-3);
  color: var(--color-primary);
  font-size: 0.8125rem;
  text-decoration: none;
}
.st-note {
  margin: 0;
  color: var(--color-text-faint);
  font-size: 0.75rem;
  line-height: 1.5;
}

/* The same trick, for text that is also a control — at 24, not 44.
 *
 * These five are words in a sentence: a byline under a title, the sort word
 * after "5 episodes", a source name on a feed card, a "Show credits" line, a
 * playlist name in a row. Their height is a line box, so they measured 14 to
 * 24px tall — `.snd-credits-toggle` 65x14, `.author-link` 45x15, `.sd-sort`
 * 54x20 — and every one of those is under WCAG 2.5.8's 24x24 floor, the line
 * e2e_contrast says nothing may cross. Padding would move the text off the
 * baseline it shares with the words around it, which is the whole reason they
 * read as part of the sentence, so the target grows and the paint does not.
 *
 * 24 rather than 44 because 44 was tried and does not work here: measured with
 * elementFromPoint, a 44px overlay on `.row-title-btn` and
 * `.snd-credits-toggle` is simply covered by whatever sits 21px away in text
 * this dense — it either fails to claim the space or takes it from a
 * neighbour. Same argument e2e_contrast already makes for chip rows: 44 is the
 * aim, and it is the wrong design where the content is a sentence.
 *
 * `width: 100%` rather than a fixed 24: these are wider than 24 already, and a
 * fixed width would make a short one claim the words beside it. */
.author-link,
.sd-sort,
.feed-card-source-link,
.snd-credits-toggle,
.row-title-btn {
  position: relative;
}
.author-link::after,
.sd-sort::after,
.feed-card-source-link::after,
.snd-credits-toggle::after,
.row-title-btn::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  height: 24px;
  transform: translate(-50%, -50%);
}
