/*
  Daily Cast — web app  ·  direction: "Warm cream editorial"

  The app now runs the same system as the marketing site (docs/DESIGN.md):
  an eggshell paper canvas (#fdfcfc), warm taupe secondary surfaces, stone
  hairlines, black ink type. Display type is Instrument Serif 400 *italic*,
  used only at 30px and above — the h1 on the auth pages and the section
  headings on each card. Inter 400/500 owns everything below that; IBM Plex
  Mono (the sanctioned Geist Mono substitute) carries technical micro-copy.

  The palette is otherwise achromatic on purpose. Brand colour is a single
  warm sunrise ramp (#f83636 → #ff8827 → #ffcccc → #fff5e3) and it is spent
  only on product moments: the sphere on the auth pages, the sun in the
  topbar wordmark, the scrubber that runs an episode, and the bars that pulse
  while an episode is being recorded. It never touches buttons, links,
  borders or body text.

  Dark mode is a warm companion, not an inversion: a deep warm near-black
  (#14110e — brown-black, never blue-black), surfaces lifted a step, an
  eggshell type ramp, and the *same* sunrise ramp, which reads even better
  against it. It follows the OS via prefers-color-scheme; nothing sets a
  data-theme attribute.

  Text/background pairs are all AA (4.5:1 body, 3:1 UI): ink 20.5:1 on
  eggshell, graphite 10.0:1, smoke 4.7:1. Ash never carries text — it is a
  decorative mark colour only (2.6:1).

  Self-hosted WOFF2, latin subset, OFL (see fonts/LICENSE.md). No
  third-party origins at runtime.
*/

/* ---------- Self-hosted type (OFL, latin subset) ---------- */

@font-face {
  font-family: "Inter";
  src: url("/static/fonts/inter-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("/static/fonts/inter-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Instrument Serif";
  src: url("/static/fonts/instrumentserif-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("/static/fonts/plexmono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ---------- Tokens ---------- */

:root {
  /* palette — docs/DESIGN.md */
  --eggshell: #fdfcfc;
  --taupe: #f5f3f1;
  --stone: #ebe8e4;
  --ink: #000000;
  --graphite: #44403b;
  --smoke: #777169;
  --ash: #a59f97;

  /* the only accent, and only inside product moments */
  --sunrise-core: #f83636;
  --sunrise-mid: #ff8827;
  --sunrise-blush: #ffcccc;
  --sunrise-haze: #fff5e3;

  /* semantic surfaces */
  --canvas: var(--eggshell);
  --surface: var(--eggshell); /* cards — separated by hairline + radius, not fill */
  --surface-2: var(--taupe); /* rows, inputs, banners: one step in from a card */
  --plate: var(--stone); /* read-only fields, icon plates */

  /* semantic type — see the AA table in the header comment */
  --text: var(--ink); /* headings, primary copy, filled buttons */
  --text-2: var(--graphite); /* labels, metadata, UI chrome, any size */
  --text-3: var(--smoke); /* quiet prose, >=14px, on eggshell only */
  --placeholder: #6e6860;
  --mark: var(--ash); /* decorative marks only — never text */

  --line: var(--stone); /* hairline dividers and card edges */
  --line-strong: #8f8a83; /* control boundaries (3.3:1 on eggshell) */

  --fill: var(--ink); /* filled button / play button */
  --fill-text: var(--eggshell);

  --track: var(--stone); /* scrubber rail */
  --cue: var(--ink); /* chapter dots — ink so they never sink into the fill */

  --danger: #9c332a;
  --danger-text-on-fill: var(--eggshell);
  --danger-bg: #fbeae5;

  --focus: var(--ink);
  --backdrop: rgba(23, 20, 17, 0.42);
  --shadow-whisper:
    rgba(0, 0, 0, 0.4) 0 0 1px 0,
    rgba(0, 0, 0, 0.04) 0 1px 1px 0,
    rgba(0, 0, 0, 0.04) 0 2px 4px 0;

  --font-display-serif: "Instrument Serif", Georgia, "Times New Roman", serif;
  --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: "Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;

  --radius-input: 4px;
  --radius-panel: 16px;
  --radius-card: 20px;
  --radius-pill: 9999px;

  --page-max: 1160px;
  --gutter: 1.5rem;

  color-scheme: light dark;
}

@media (min-width: 720px) {
  :root { --gutter: 2rem; }
}

/* Warm dark companion. Same sunrise ramp; everything else is a warm
   near-black ladder with an eggshell type ramp. */
@media (prefers-color-scheme: dark) {
  :root {
    --canvas: #14110e;
    --surface: #1e1a16;
    --surface-2: #272119;
    --plate: #332c25;

    --text: #f7f3ee;
    --text-2: #cfc7bd;
    --text-3: #a29a90;
    --placeholder: #a29a90;
    --mark: #7d746a;

    --line: #332c25;
    --line-strong: #786e63;

    --fill: #f7f3ee;
    --fill-text: #14110e;

    --track: #332c25;
    --cue: #f7f3ee;

    --danger: #ffa294;
    --danger-text-on-fill: #14110e;
    --danger-bg: #33201b;

    --focus: #f7f3ee;
    --backdrop: rgba(0, 0, 0, 0.62);
    --shadow-whisper:
      rgba(0, 0, 0, 0.5) 0 0 1px 0,
      rgba(0, 0, 0, 0.35) 0 2px 6px 0,
      rgba(0, 0, 0, 0.3) 0 8px 24px -12px;
  }
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  background: var(--canvas);
  color: var(--text);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.5;
  letter-spacing: 0.01em;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Sub-30px headings are Inter, not display type — the italic serif is
   reserved for the two display tiers below. */
h1, h2, h3 {
  font-family: var(--font-sans);
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.3;
}

.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}

a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-thickness: 2px; }

/* ---------- The sunrise: signal marks ---------- */

/* The four decorative spans are markup the app ships everywhere an "on air"
   mark used to sit. In this system they are a quiet achromatic tally by
   default; two places re-shape them into the brand's sun (topbar wordmark,
   auth-page sphere) and one place lights them (an episode being recorded). */
.signal-bars {
  display: inline-flex;
  align-items: flex-end;
  gap: 3px;
  height: 1em;
  vertical-align: middle;
}

.signal-bars span {
  display: block;
  width: 3px;
  background: var(--mark);
  border-radius: 1px;
}

/* default staggered heights (flex-end won't stretch zero-height children) */
.signal-bars span:nth-child(1) { height: 45%; }
.signal-bars span:nth-child(2) { height: 100%; }
.signal-bars span:nth-child(3) { height: 65%; }
.signal-bars span:nth-child(4) { height: 80%; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}

#success-state.toast-in { animation: toast-in 0.35s ease; }

@media (prefers-reduced-motion: reduce) {
  #success-state.toast-in { animation: none; }
}

/* ---------- Top bar ---------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 1.25rem var(--gutter);
  border-bottom: 1px solid var(--line);
}

/* Two images and nothing else since the word became the site's own raster
   lockup — the type declarations this rule used to carry styled a text node
   that no longer exists, so only the layout is left. */
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  text-decoration: none;
  color: var(--text);
}

.topbar nav {
  display: flex;
  align-items: center;
  gap: 1.4rem;
  font-size: 0.875rem;
  white-space: nowrap;
}

@media (max-width: 460px) {
  .topbar { padding: 1rem 1.25rem; }
  .topbar nav { gap: 0.95rem; font-size: 0.82rem; }
}

/* Inline text-button reset, usable anywhere a link needs a click handler
   (topbar nav, in-sentence CTAs like the past-due banner's "update your
   card"). Topbar overrides the color below to match its muted nav links. */
button.link {
  background: none;
  border: none;
  font: inherit;
  font-weight: 500;
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  padding: 0;
}
/* `background: none` has to be repeated on :hover — the generic
   `button:hover` fill below has equal specificity and comes later, so it
   would otherwise paint an ink pill behind an inline text link. */
button.link:hover { background: none; text-decoration-thickness: 2px; }

.topbar nav a,
.topbar nav button.link {
  color: var(--text-2);
  text-decoration: none;
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
  padding: 0;
  transition: color 0.16s ease;
}

.topbar nav a:hover,
.topbar nav button.link:hover { color: var(--text); text-decoration: none; }

main {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 2.5rem var(--gutter) 5rem;
}

/* ---------- Surfaces ---------- */

.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 2rem;
  overflow: hidden;
}

.card + .card { margin-top: 1.5rem; }

/* Section heads are the second display tier: Instrument Serif 400 italic,
   never below 30px, never faux-bold. */
.card h2 {
  margin: 0 0 0.4rem;
  font-family: var(--font-display-serif);
  font-style: italic;
  font-weight: 400;
  font-synthesis: none;
  font-size: 30px;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--text);
}

.hint {
  color: var(--text-3);
  font-size: 0.9rem;
  line-height: 1.6;
  margin: 0 0 1.25rem;
}

.greeting {
  color: var(--text-3);
  margin: -0.4rem 0 0.8rem;
  font-size: 0.95rem;
}

/* Smoke (--text-3) is 4.71:1 on eggshell but only 4.36:1 on taupe, i.e. below
   AA. Anywhere a .hint lands on a taupe surface it steps up to graphite
   (9.29:1). Listed defensively — some of these compositions do not exist in
   today's markup, and the point is that adding one cannot quietly ship
   sub-AA text. */
.digest-row:first-child .hint,
.chooser-option .hint,
.price-option .hint,
.pro-banner .hint {
  color: var(--text-2);
}

.grid-two {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
}

@media (min-width: 820px) {
  .grid-two {
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
    align-items: start;
  }
}

/* Past the old 880px cap the sources column stops growing and the extra
   width all lands in the episodes column, where the player and summaries
   actually use it. */
@media (min-width: 1040px) {
  .grid-two {
    grid-template-columns: minmax(320px, 400px) minmax(0, 1fr);
    gap: 2rem;
  }
}

/* ---------- Forms ---------- */

label {
  display: block;
  font-family: var(--font-sans);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-2);
  margin-bottom: 0.45rem;
}

input[type="email"],
input[type="url"],
input[type="number"],
input[type="text"] {
  width: 100%;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-input);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 0.95rem;
}

input[readonly] {
  background: var(--plate);
  color: var(--text-2);
}

input::placeholder { color: var(--placeholder); opacity: 1; }

input:focus-visible,
button:focus-visible,
a:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

input:focus-visible,
textarea:focus-visible {
  border-color: var(--text);
  outline-offset: 1px;
}

.field-row {
  display: flex;
  flex-wrap: wrap; /* narrow column: category + button drop below the URL field instead of crushing it */
  gap: 0.6rem;
  align-items: flex-end;
}

.field-row > div { flex: 1 1 13rem; min-width: 0; }

/* Must out-specify `.field-row > div` above, or its flex:1 wins and the
   category field competes with the URL field for space (the min-width floor
   then crushes the URL input to nothing in narrow columns). */
.field-row > div.category-field { flex: 0 0 auto; width: 9rem; min-width: 0; }

.category-field select {
  width: 100%;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-input);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 0.95rem;
}

.error-text {
  color: var(--danger);
  font-size: 0.85rem;
  margin: 0.5rem 0 0;
}

.error-banner {
  background: var(--danger-bg);
  color: var(--danger);
  border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
  border-radius: var(--radius-panel);
  padding: 0.8rem 1rem;
  font-size: 0.9rem;
  margin-bottom: 1rem;
}

/* ---------- Buttons ---------- */

/* One hierarchy only: an ink-filled pill and an outline pill. No coloured
   CTA fills — the sunrise ramp is never a button. */
button,
.btn {
  font-family: var(--font-sans);
  background: var(--fill);
  color: var(--fill-text);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  /* 0.8rem y-padding puts the pill at the same 44px height as an input, so
     the add-source row's URL field, category select and button share a
     baseline box. */
  padding: 0.8rem 1.3rem;
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background-color 0.16s ease, color 0.16s ease, border-color 0.16s ease;
}

.btn { display: inline-block; text-decoration: none; }

button:hover,
.btn:hover { background: color-mix(in srgb, var(--fill) 82%, var(--canvas)); }

/* `.accent` is legacy naming from the retired direction — in this system the
   primary action is the same ink pill as the default. */
button.accent {
  background: var(--fill);
  color: var(--fill-text);
}

/* `.btn.ghost` is here because the onboarding Back control is an <a class="btn
   ghost">, not a <button> — without it the anchor falls through to the filled
   ink pill and Back reads as the primary action. */
button.ghost,
.btn.ghost {
  background: transparent;
  color: var(--text-2);
  border: 1px solid var(--line-strong);
}
button.ghost:hover,
.btn.ghost:hover { background: var(--surface-2); color: var(--text); }

button.danger {
  background: transparent;
  border-color: var(--danger);
  color: var(--danger);
}
button.danger:not(.ghost) {
  background: var(--danger);
  color: var(--danger-text-on-fill);
  border-color: var(--danger);
}
button.danger:not(.ghost):hover {
  background: color-mix(in srgb, var(--danger) 85%, var(--canvas));
}
/* button.ghost:hover sets color: var(--text), which would otherwise wash
   out the red on a ghost+danger button (e.g. settings' Regenerate link) —
   keep it danger-colored on hover too. */
button.ghost.danger:hover {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}

button:disabled { opacity: 0.5; cursor: default; }

@media (prefers-reduced-motion: reduce) {
  button, .btn { transition: none; }
  /* A media query adds no specificity: `.topbar nav a` out-specifies the
     bare `button`/`.btn` above, so the nav's colour transition has to be
     switched off by name. */
  .topbar nav a,
  .topbar nav button.link { transition: none; }
}

/* ---------- Sign in / error card ---------- */

.login-shell {
  /* Leave room for the sibling .app-footer (~89px tall) so the Privacy and
     Terms links sit above the fold on the pre-sign-in page. */
  min-height: calc(100vh - 6rem);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}

.login-card {
  width: 100%;
  max-width: 420px;
  text-align: center;
  padding: 2.6rem 2.2rem;
  /* .card's overflow: hidden would clip the language menu's absolutely
     positioned panel, which opens below the card's own bottom edge. */
  overflow: visible;
}

/* The one display-xl on the app: Instrument Serif italic, floored at 30px. */
.login-card h1 {
  font-family: var(--font-display-serif);
  font-style: italic;
  font-weight: 400;
  font-synthesis: none;
  font-size: clamp(30px, 7vw, 38px);
  line-height: 1.08;
  letter-spacing: -0.01em;
  margin: 0 0 0.6rem;
}

.login-card p.tagline {
  color: var(--text-3);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.6;
  margin: 0 0 1.9rem;
}

.login-card form { text-align: left; }

.login-card button[type="submit"] {
  width: 100%;
  margin-top: 1rem;
}

.login-card .btn { width: 100%; margin-top: 0.5rem; text-align: center; }

.check-inbox { display: none; text-align: left; }
.check-inbox p { margin: 0 0 0.6rem; }
.check-inbox strong { font-family: var(--font-sans); font-weight: 500; font-size: 1.05rem; }
.check-inbox .hint { margin-bottom: 1.2rem; }
.check-inbox button { width: 100%; }
.code-form {
  display: grid;
  gap: 0.5rem;
  margin-top: 1.25rem;
  text-align: left;
}
.code-form input {
  font-family: var(--font-mono);
  font-size: 1.25rem;
  letter-spacing: 0.18em;
}

/* Language menu under the login card — quiet chrome, not a product moment,
   so it stays on the graphite type token (AA at any size in both themes)
   and never borrows the sunrise accent. Same zero-JS <details> disclosure
   as the site (site/style.css), on app tokens; opens downward, centered
   under the card since it sits mid-page rather than in a topbar/footer. */
.lang-toggle {
  position: relative;
  margin: 1.6rem 0 0;
  text-align: center;
}

.lang-toggle summary {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 8px 10px;
  border-radius: 999px;
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  white-space: nowrap;
  cursor: pointer;
  list-style: none;
}

.lang-toggle summary::-webkit-details-marker { display: none; }

.lang-toggle summary:hover,
.lang-toggle[open] summary {
  background: var(--surface-2);
  color: var(--text);
}

.lang-toggle .lang-menu {
  position: absolute;
  left: 50%;
  top: calc(100% + 6px);
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  flex-direction: column;
  width: 172px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 12px 28px rgba(20, 17, 14, 0.1);
  text-align: left;
}

.lang-menu span,
.lang-menu a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 10px;
  border-radius: 6px;
  font-family: var(--font-sans);
  font-size: 14px;
}

.lang-menu span[aria-current] {
  font-weight: 500;
  color: var(--text);
}

.lang-menu a {
  color: var(--text-2);
  text-decoration: none;
}

.lang-menu a:hover { background: var(--surface-2); }

/* ---------- Welcome (sign-in) ---------- */

/* The brand mark is now a single raster asset — the same logo the marketing
   site, the favicon and the PWA icons are cut from — so the sun is one file
   instead of three hand-tuned gradients that could drift apart. */
.wordmark .wordmark-mark { display: block; width: 22px; height: 22px; }

/* The word beside the sphere is the marketing site's own brand lockup,
   cropped out of site/img/dailycast-wordmark.png and keyed to transparency
   by scripts/make_pwa_icons.py — the site sets it in Instrument Serif, and
   rasterising the site's own artwork is what keeps app and site identical
   rather than approximating one in the other's font. */
.wordmark-text { display: block; height: 18px; width: auto; }

/* The crop is ink on transparent. On the warm brown-black canvas it has to
   read as eggshell instead, so invert it and rotate the hue back — cheaper,
   and impossible to drift, compared with committing a second raster. */
@media (prefers-color-scheme: dark) {
  .wordmark-text { filter: invert(1) hue-rotate(180deg); }
}

/* The welcome screen is deliberately card-less: mark, one headline, one
   subtitle, one full-width action, a quiet language row and the legal
   footnote — nothing framed, so the page reads as an opening rather than a
   form. .login-card stays for error.html / billing_success.html. */
.welcome {
  width: 100%;
  max-width: 420px;
  text-align: center;
  padding: 1rem 0.5rem;
}

.welcome-logo { display: block; margin: 0 auto 1.6rem; width: clamp(96px, 26vw, 120px); height: auto; }
.welcome-logo-sm { width: 96px; margin-bottom: 1.2rem; }

/* The one display-xl on the app: Instrument Serif italic, floored at 30px. */
.welcome h1 {
  font-family: var(--font-display-serif);
  font-style: italic;
  font-weight: 400;
  font-synthesis: none;
  font-size: clamp(30px, 7vw, 38px);
  line-height: 1.08;
  letter-spacing: -0.01em;
  margin: 0 0 0.5rem;
}

.welcome p.tagline { color: var(--text-3); font-size: 1rem; line-height: 1.6; margin: 0 0 1.8rem; }
.welcome form { text-align: left; }
.welcome form input[type="email"] { width: 100%; }
.welcome-cta { width: 100%; margin-top: 0.8rem; padding-block: 0.95rem; }
.welcome .check-inbox { text-align: center; }

/* The language row, Klarna-style: a full-width panel whose label sits left
   and whose current value sits right. It reuses the <details> disclosure and
   menu from .lang-toggle above — these rules follow it deliberately, since
   they override at equal specificity by source order. */
.welcome-row {
  position: relative;
  margin-top: 1.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  text-align: left;
}

.welcome-row summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  padding: 0.8rem 1rem;
  border-radius: var(--radius-panel);
  font-family: var(--font-sans);
  font-size: 0.9rem;
  letter-spacing: normal;
  /* .lang-toggle summary above is muted for a footer pill; this row is a
     full-width control, so its label reads at full contrast. */
  color: var(--text);
  cursor: pointer;
  list-style: none;
}

.welcome-row summary::-webkit-details-marker { display: none; }
.welcome-row-value { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--text-2); }

/* The row spans the column, so the menu hangs from its right edge rather
   than being centred under a pill. */
.welcome-row .lang-menu { left: auto; right: 0; transform: none; }

.welcome-legal { color: var(--text-3); font-size: 0.8rem; line-height: 1.5; margin: 2.4rem 0 0; }
.welcome-legal a { color: inherit; }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---------- Sources ---------- */

.source-list,
.digest-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.source-row {
  display: flex;
  flex-wrap: wrap; /* narrow column: select + Remove wrap under the name instead of overflowing */
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 0.75rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--line);
}

.source-row > div:first-child { flex: 1 1 10rem; min-width: 0; }
.source-row:last-child { border-bottom: none; }

.source-row .name {
  font-weight: 500;
  font-size: 0.95rem;
  color: var(--text);
}

.source-row .url {
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0;
  word-break: break-all;
}

.source-row select.category-select {
  /* A select's intrinsic min width is its WIDEST option ("Product &
     Engineering Leadership" ≈ 280px) — without these caps it refuses to
     shrink and crushes the source name column. Long labels truncate in the
     closed control; the opened dropdown always shows full text. */
  flex: 0 1 auto;
  min-width: 0;
  max-width: 11rem;
  background: var(--surface-2);
  color: var(--text-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-input);
  padding: 0.35rem 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0;
}

/* The settings page's three selects (#voice-select, #language-select,
   #country-select) and the onboarding Name step's #ob-country have no
   base box rule elsewhere — unlike the category selects above, which
   inherit from .category-field/.source-row — so give them the full
   input-like treatment here, ahead of the shared chevron rule below. */
#voice-select,
#language-select,
#country-select,
#ob-country {
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-input);
  background-color: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 0.95rem;
  padding: 0.6rem 2.2rem 0.6rem 0.8rem;
}

/* Styled selects: one shared treatment for every <select> in the app —
   both category dropdowns (the add-source form's .category-field select
   and each source row's .category-select, both above) and the three settings
   selects just above — appearance suppressed, with an inline chevron
   replacing the browser's own arrow so it matches the lang-toggle's
   chevron. Declared after all four selectors' own rules (some of which set
   `background`/`padding` shorthands that would otherwise wipe these
   longhands back out); dark override just below swaps only the stroke
   color. */
.category-field select,
.source-row select.category-select,
#voice-select,
#language-select,
#country-select,
#ob-country {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10' fill='none' stroke='%2344403b' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 3.5 L5 6.5 L8 3.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.8rem center;
  background-size: 11px;
}

@media (prefers-color-scheme: dark) {
  .category-field select,
  .source-row select.category-select,
  #voice-select,
  #language-select,
  #country-select,
  #ob-country {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10' fill='none' stroke='%23cfc7bd' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 3.5 L5 6.5 L8 3.5'/%3E%3C/svg%3E");
  }
}

/* Muted plate look for any disabled select — same treatment as
   input[readonly] above — so a server-rendered disabled <select> (e.g.
   #voice-select on Free plans) doesn't read as an active, clickable
   control before app.js's .locked class ever runs. The id+:disabled
   variants are needed to out-specify #voice-select/#language-select's own
   background-color above (a bare id always beats a bare type+pseudo-class
   selector on specificity, regardless of source order). */
select:disabled,
#voice-select:disabled,
#language-select:disabled,
#country-select:disabled,
#ob-country:disabled {
  background-color: var(--plate);
  color: var(--text-2);
  cursor: not-allowed;
}

.source-row button.remove {
  background: none;
  border: 1px solid var(--line-strong);
  color: var(--text-2);
  border-radius: var(--radius-pill);
  padding: 0.32rem 0.75rem;
  font-size: 0.8rem;
  font-weight: 500;
  flex-shrink: 0;
}
.source-row button.remove:hover {
  color: var(--danger);
  border-color: var(--danger);
  background: none;
}

/* ---------- Paused sources (issue #35) ---------- */

.paused-sources {
  margin-top: 1.6rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--line);
}
/* An author display value beats the UA's [hidden] rule — restore hiding
   (same trap as .show-more[hidden] / .instant-recording[hidden] above). */
.paused-sources[hidden] {
  display: none;
}

.paused-sources h3 {
  margin: 0 0 0.35rem;
  font-size: 1rem;
  color: var(--text);
}

.paused-source-list {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
}

.paused-source-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 0.75rem;
  padding: 0.8rem 0;
  border-bottom: 1px solid var(--line);
}
.paused-source-row:last-child { border-bottom: none; }

.paused-source-row .name {
  font-weight: 500;
  font-size: 0.95rem;
  color: var(--text);
}

.paused-source-row .category-label {
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0;
}

.paused-source-row button.reactivate {
  background: none;
  border: 1px solid var(--line-strong);
  color: var(--text);
  border-radius: var(--radius-pill);
  padding: 0.32rem 0.85rem;
  font-size: 0.8rem;
  font-weight: 500;
  flex-shrink: 0;
}
.paused-source-row button.reactivate:hover {
  background: var(--surface-2);
  border-color: var(--text);
}

/* ---------- Episodes: today's card, then the archive ---------- */

/* The newest episode is a taupe card — the same object the marketing hero
   shows. Everything older is a hairline-separated archive row, so the list
   reads as "today, then the back catalogue" without any accent chrome. */
.digest-row {
  padding: 1.4rem 0;
}

.digest-row + .digest-row {
  border-top: 1px solid var(--line);
}

.digest-row:first-child {
  margin-top: 0;
  margin-bottom: 0.5rem;
  padding: 1.6rem;
  background: var(--surface-2);
  border-radius: var(--radius-card);
}

.digest-row .meta {
  display: flex;
  flex-wrap: wrap; /* narrow viewports: Transcript button wraps under the date instead of overflowing */
  align-items: center;
  gap: 0.5rem 0.6rem;
  margin-bottom: 0.7rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0;
  color: var(--text-2);
}

.digest-row .meta .date { color: var(--text); font-weight: 400; }

/* Top line: [bars] [Today?] ............ [real date] [Transcript]. The date
   sits on the right of every card; "Today" (first card only, when it really
   is today) takes the left. */
.digest-row .meta .today-label { color: var(--text); font-weight: 400; }
.digest-row .meta .date { margin-left: auto; }

.digest-row .summary {
  margin: 0 0 0.95rem;
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--text);
}

.digest-row .meta .transcript-btn {
  background: none;
  border: 1px solid var(--line-strong);
  color: var(--text-2);
  border-radius: var(--radius-pill);
  padding: 0.22rem 0.65rem;
  font-family: var(--font-sans);
  font-size: 0.75rem;
  font-weight: 500;
  flex-shrink: 0;
}
.digest-row .meta .transcript-btn:hover {
  color: var(--text);
  border-color: var(--text);
  background: none;
}

.show-more {
  display: block;
  width: 100%;
  margin-top: 1.25rem;
  background: none;
  border: 1px solid var(--line-strong);
  color: var(--text-2);
  border-radius: var(--radius-pill);
  padding: 0.65rem 1rem;
  font-size: 0.85rem;
  font-weight: 500;
}
.show-more:hover {
  color: var(--text);
  border-color: var(--text);
  background: var(--surface-2);
}

/* An author display value beats the UA's [hidden] rule — restore hiding. */
.show-more[hidden] {
  display: none;
}

/* ---------- Transcript modal ---------- */

dialog.transcript-modal {
  margin: auto;
  width: min(560px, calc(100vw - 2rem));
  max-height: min(85vh, 48rem);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  color: var(--text);
  padding: 1.6rem 1.8rem;
  box-shadow: var(--shadow-whisper);
}
/* display must stay scoped to [open]: an unconditional display:flex here is
   an author-origin rule, which beats the UA stylesheet's
   dialog:not([open]) { display: none } regardless of specificity, so the
   empty dialog would render on the dashboard even when never opened. */
dialog.transcript-modal[open] {
  display: flex;
  flex-direction: column;
}
dialog.transcript-modal::backdrop {
  background: var(--backdrop);
  backdrop-filter: blur(2px);
}

dialog.transcript-modal .questionnaire-head {
  flex-shrink: 0;
}

#transcript-title {
  font-size: 1.1rem;
  margin: 0;
}

#transcript-body {
  overflow-y: auto;
  min-height: 0;
}

#transcript-body h3 {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-2);
  margin: 1.4rem 0 0.5rem;
}
#transcript-body h3:first-child { margin-top: 0; }

#transcript-body p {
  margin: 0 0 0.9rem;
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--text);
}
#transcript-body p:last-child { margin-bottom: 0; }

#transcript-body .loading-text {
  color: var(--text-3);
}

/* ---------- Install-instructions modal (PWA) ---------- */
/* Same native <dialog> pattern as the transcript modal above. */

dialog.install-instructions-modal {
  margin: auto;
  width: min(420px, calc(100vw - 2rem));
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  color: var(--text);
  padding: 1.6rem 1.8rem;
  box-shadow: var(--shadow-whisper);
}
/* display must stay scoped to [open] — see the identical note on
   dialog.transcript-modal above: an unconditional display:flex here would
   beat the UA stylesheet's dialog:not([open]) { display: none }, so the
   empty dialog would render on the page even when never opened. */
dialog.install-instructions-modal[open] {
  display: flex;
  flex-direction: column;
}
dialog.install-instructions-modal::backdrop {
  background: var(--backdrop);
  backdrop-filter: blur(2px);
}

/* Confirm-before-remove modal (owner request 2026-09-22): same native
   <dialog> treatment as the install modal above. */
dialog.confirm-modal {
  margin: auto;
  width: min(420px, calc(100vw - 2rem));
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  color: var(--text);
  padding: 1.6rem 1.8rem;
  box-shadow: var(--shadow-whisper);
}
dialog.confirm-modal::backdrop {
  background: var(--backdrop);
  backdrop-filter: blur(2px);
}
#remove-source-title {
  font-size: 1.1rem;
  margin: 0 0 0.5rem;
  overflow-wrap: anywhere;
}
.confirm-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-top: 1.2rem;
}

#install-instructions-title {
  font-size: 1.1rem;
  margin: 0;
}

#install-instructions-body ol {
  margin: 0 0 0.9rem;
  padding-left: 1.3rem;
}
#install-instructions-body li {
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--text);
}
/* :not(.hint) so the iOS note (<p class="hint">) keeps .hint's own muted
   --text-3 color and metrics — an ID selector here would otherwise beat
   .hint's class selector regardless of source order. */
#install-instructions-body p:not(.hint) {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--text);
}

/* ---------- Custom player: the one lit surface ---------- */

/* Playing an episode is a product moment, so the scrubber is where the
   sunrise ramp lives: the played rail warms from mid to core, and the
   play-head is the core itself. Chapter cues stay ink so they never
   disappear into the fill. The real <audio> stays present
   (loadable/inspectable) but visually hidden — the controls below drive it. */
.player {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  min-height: 56px;
}

/* Visually hidden but present and NOT display:none. The custom controls drive
   this element; its native controls are clipped away. It KEEPS the `controls`
   attribute on purpose: Chromium's UA sheet applies
   `audio:not([controls]) { display: none !important }`, which no author rule
   (even !important) can override — a control-less <audio> would be display:none
   and the Playwright suite's wait_for_selector (default state "visible") would
   time out. `controls` sidesteps that; clip-path removes the native UI
   visually; aria-hidden + tabindex="-1" (set in markup) keep the native
   controls out of the a11y tree and tab order so our custom controls are the
   sole accessible surface. */
.player audio {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
}

.player-toggle {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
  background: var(--fill);
  color: var(--fill-text);
  border: none;
}
.player-toggle:hover { background: color-mix(in srgb, var(--fill) 82%, var(--canvas)); }

/* Play glyph: a border triangle. In the .playing state it flips to a pair of
   pause bars (one element + a box-shadow twin) with no extra markup. */
.player-icon-play {
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 8px 0 8px 13px;
  border-color: transparent transparent transparent var(--fill-text);
  margin-left: 3px;
}
.player.playing .player-icon-play {
  width: 4px;
  height: 15px;
  border: none;
  margin-left: 0;
  transform: translateX(-3px);
  background: var(--fill-text);
  box-shadow: 6px 0 0 var(--fill-text);
}

.player-strip {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* The bar is a 32px hit target; the visible 8px track is a pseudo-element so
   the padded click/tap area stays generous without fattening the rail. */
.player-bar {
  position: relative;
  height: 32px;
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
  /* Drag-to-scrub: a horizontal drag on the bar must not scroll the page
     (vertical scrolling elsewhere is untouched). */
  touch-action: none;
}
/* Invisible vertical extension of the touch target; the dots sit above it. */
.player-bar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -10px;
  bottom: -10px;
}
.player-bar::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 8px;
  transform: translateY(-50%);
  background: var(--track);
  border-radius: var(--radius-pill);
}
.player-bar:hover { background: none; }
.player-bar:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* mid → core left to right, so the leading edge beside the play-head is the
   hottest colour: sunrise-core is what clears 3:1 against the rail (3.09
   light, 3.65 dark), and it is the edge — and the knob — that marks the
   playhead position. The mid end of the ramp is decorative warmth behind
   already-played audio and is not load-bearing. */
.player-progress {
  position: absolute;
  left: 0;
  top: 50%;
  width: 0;
  height: 8px;
  transform: translateY(-50%);
  background: linear-gradient(90deg, var(--sunrise-mid), var(--sunrise-core));
  border-radius: var(--radius-pill);
  pointer-events: none;
}

.player-knob {
  position: absolute;
  left: 0;
  top: 50%;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--sunrise-core);
  /* the ring separates the head from the rail; --surface-2 is the panel
     behind the most prominent player (today's episode card). */
  border: 2px solid var(--surface-2);
  transform: translate(-50%, -50%);
  pointer-events: none;
  transition: width 0.1s ease, height 0.1s ease;
}
.player-bar.scrubbing .player-knob { width: 18px; height: 18px; }

/* Chapter cue: ink at rest — smaller than the play-head, and never at risk of
   sinking into the sunrise fill it sits on — then lighting to sunrise core on
   hover/focus, which is where the ramp belongs. Reset the global button
   styling first. */
.chapter-dot {
  position: absolute;
  top: 50%;
  width: 11px;
  height: 11px;
  padding: 0;
  border-radius: 50%;
  background: var(--cue);
  border: 2px solid var(--surface-2);
  transform: translate(-50%, -50%);
  cursor: pointer;
  z-index: 1;
  transition: transform 0.1s ease;
}
.chapter-dot:hover,
.chapter-dot:focus-visible {
  background: var(--sunrise-core);
  transform: translate(-50%, -50%) scale(1.3);
}
.chapter-dot:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Pure-CSS label bubble above the cue — works on hover (desktop) and on the
   focus a tap leaves behind (touch). */
.chapter-tip {
  position: absolute;
  bottom: calc(100% + 9px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  max-width: min(16rem, 60vw);
  background: var(--text);
  color: var(--canvas);
  font-family: var(--font-sans);
  font-size: 0.72rem;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: 0;
  white-space: normal;
  text-align: center;
  padding: 0.3rem 0.55rem;
  border-radius: 8px;
  box-shadow: var(--shadow-whisper);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.1s ease;
}
.chapter-tip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 4px solid transparent;
  border-top-color: var(--text);
}
.chapter-dot:hover .chapter-tip,
.chapter-dot:focus-visible .chapter-tip {
  opacity: 1;
  visibility: visible;
}

.player-time {
  font-size: 0.76rem;
  letter-spacing: 0;
  color: var(--text-2);
}
.player-time .player-elapsed { color: var(--text); }

@media (prefers-reduced-motion: reduce) {
  .chapter-dot { transition: none; }
  .chapter-tip { transition: none; }
}

.empty-state {
  text-align: center;
  padding: 2.5rem 1rem;
  color: var(--text-3);
  font-size: 0.95rem;
  line-height: 1.6;
}

/* ---------- Onboarding: two-path chooser ---------- */

.chooser {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}

/* An author display value beats the UA's [hidden] rule — restore hiding
   (same trap as .show-more[hidden] / .instant-recording[hidden] /
   .paused-sources[hidden]). Without this, app.js's
   `chooser.hidden = !(count === 0)` had no effect and users who already had
   sources kept seeing the onboarding chooser. */
.chooser[hidden] {
  display: none;
}

@media (min-width: 480px) {
  .chooser { grid-template-columns: 1fr 1fr; }
}

.chooser-option {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  text-align: left;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid transparent;
  border-radius: var(--radius-panel);
  padding: 0.95rem 1rem;
  font-weight: 400;
}

.chooser-option:hover {
  background: var(--surface-2);
  border-color: var(--text);
}

.chooser-option-mark {
  flex-shrink: 0;
  color: var(--text-2);
  font-family: var(--font-mono);
  line-height: 1.3rem;
}

.chooser-option .signal-bars { height: 1.05rem; margin-top: 0.2rem; }
.chooser-option .signal-bars span { background: var(--text-2); }

.chooser-option-title {
  display: block;
  font-weight: 500;
  font-size: 0.92rem;
}

.chooser-option-sub {
  display: block;
  color: var(--text-2);
  font-size: 0.8rem;
  margin-top: 0.15rem;
}

/* ---------- Source counter + Pro notice ---------- */

.source-meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 1rem 0;
}

.source-counter {
  color: var(--text-2);
  font-size: 0.8rem;
  letter-spacing: 0;
  margin: 0;
}

.pro-banner {
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--text-2);
  border-radius: var(--radius-panel);
  padding: 0.85rem 1rem;
  font-size: 0.88rem;
  line-height: 1.6;
  margin: 0 0 1.1rem;
}

/* ---------- Questionnaire ---------- */

dialog.questionnaire {
  margin: auto;
  width: min(560px, calc(100vw - 2rem));
  max-height: min(85vh, 48rem);
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  color: var(--text);
  padding: 1.6rem 1.8rem;
  box-shadow: var(--shadow-whisper);
}
dialog.questionnaire::backdrop {
  background: var(--backdrop);
  backdrop-filter: blur(2px);
}
/* position: sticky is clamped to its containing block's CONTENT box, so a
   footer cannot reach the sheet's padding-box bottom by pulling itself down
   with a negative bottom margin — the browser pushes it straight back up by
   exactly this padding, leaving a dead band beneath it and overlapping the
   content above. The sheet drops its bottom padding instead and
   .questionnaire-footer pays it back as its own, and only when that footer
   is actually there to pay. The footer is hidden until the viewer holds a
   source (owner UI round 2), so :not([hidden]) is load-bearing: while Done
   is away the sheet keeps its own bottom padding, with nothing to pay it
   back. Do not "simplify" this into a negative margin. */
dialog.questionnaire:has(.questionnaire-footer:not([hidden])) { padding-bottom: 0; }
.questionnaire-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.3rem;
}
.dialog-close {
  background: none;
  border: none;
  color: var(--text-2);
  font-family: var(--font-sans);
  font-size: 1.35rem;
  line-height: 1;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-pill);
  cursor: pointer;
}
.dialog-close:hover { color: var(--text); background: var(--surface-2); }

.questionnaire h3 {
  font-size: 1.1rem;
  margin: 0;
}

/* The sheet scrolls, and a long recommendation list pushes the header's ×
   off screen. Done rides along at the bottom instead. The negative side
   margins back out dialog.questionnaire's 1.8rem side padding so the bar's
   rule and ground span the sheet edge to edge rather than floating inside
   the text column; there is no matching negative bottom margin because the
   sheet has no bottom padding to back out — see the :has() rule above. This
   footer's own bottom padding is what the sheet ends in, which is why it
   carries the safe-area inset. */
.questionnaire-footer {
  position: sticky;
  bottom: 0;
  margin: 1rem -1.8rem 0;
  padding: 0.9rem 1.8rem calc(0.9rem + env(safe-area-inset-bottom, 0px));
  background: var(--surface);
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: flex-end;
}
/* An author display value beats the UA's [hidden] rule — restore hiding
   (same trap as .show-more[hidden] / .chooser[hidden] elsewhere here).
   Without this the footer's `hidden` attribute would be cosmetic and the
   :has() rule above would be the only thing that changed. */
.questionnaire-footer[hidden] {
  display: none;
}

.questionnaire .field { margin-bottom: 1.2rem; }

.chip-select {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.chip-toggle {
  background: var(--surface-2);
  color: var(--text-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  padding: 0.4rem 0.9rem;
  font-family: var(--font-sans);
  font-size: 0.82rem;
  font-weight: 500;
  cursor: pointer;
  transition: none;
}

.chip-toggle:hover { color: var(--text); background: var(--surface-2); }

.chip-toggle[aria-pressed="true"] {
  background: var(--fill);
  color: var(--fill-text);
  border-color: var(--fill);
}

textarea {
  width: 100%;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-input);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 0.95rem;
  resize: vertical;
}

textarea::placeholder { color: var(--placeholder); opacity: 1; }

.recommendation-list {
  list-style: none;
  margin: 1.2rem 0 0;
  padding: 0;
}

/* "More sources": reveals the next page of the list the server already sent. */
.recommendation-more {
  display: block;
  margin: 1rem auto 0;
}
.recommendation-more[hidden] { display: none; }

.recommendation-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.9rem;
  padding: 0.95rem 0;
  border-bottom: 1px solid var(--line);
}
.recommendation-row:last-child { border-bottom: none; }

.recommendation-row .name {
  font-weight: 500;
  font-size: 0.95rem;
  color: var(--text);
}

.recommendation-row .description {
  color: var(--text-2);
  font-size: 0.85rem;
  line-height: 1.55;
  margin: 0.25rem 0;
}

.recommendation-row .topics {
  color: var(--text-2);
  font-size: 0.72rem;
  letter-spacing: 0;
}

.recommendation-row .cadence {
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 0.08rem 0.55rem;
  margin-left: 0.45rem;
  color: var(--text-2);
  white-space: nowrap;
}

.recommendation-row button.add-recommendation {
  flex-shrink: 0;
  background: var(--fill);
  color: var(--fill-text);
  border-radius: var(--radius-pill);
  padding: 0.42rem 1rem;
  font-size: 0.82rem;
}

/* Added: the same pill greyed out, but still a live button — tapping it
   removes the source again (owner decision 2026-09-22). Matches the
   disabled look so "Added" reads the same before and after other adds. */
.recommendation-row button.add-recommendation.added {
  opacity: 0.5;
  cursor: pointer;
}
.recommendation-row button.add-recommendation.added:hover:not(:disabled) { opacity: 0.6; }

/* ---------- Settings ---------- */

.settings-form .field { margin-bottom: 1.7rem; }

/* Voice pack: locked-with-upsell state for free-plan users (Task 5 layers
   the full upgrade CTA on top; this is just the semantic disabled look). */
.settings-form .field.locked select { opacity: 0.6; cursor: not-allowed; }
.settings-form .field.locked .hint { color: var(--text); font-weight: 500; }

/* The length slider appears on the settings page and in the wizard's length
   step; un-scoping keeps the browser's default blue off both. */
.settings-form input[type="range"],
.ob-step input[type="range"] {
  width: 100%;
  accent-color: var(--text);
}

.settings-form .range-value,
.ob-step .range-value {
  font-family: var(--font-mono);
  font-size: 1.15rem;
  letter-spacing: 0;
  margin-top: 0.6rem;
  color: var(--text);
}
.settings-form .range-value .unit,
.ob-step .range-value .unit { color: var(--text-2); font-size: 0.85rem; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  padding: 0.3rem 0.85rem;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  letter-spacing: 0;
  color: var(--text);
}
.chip::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--mark);
}

.status-text {
  font-size: 0.88rem;
  margin-top: 0.9rem;
  min-height: 1.2em;
}
.status-text.success { color: var(--text); font-weight: 500; }
.status-text.error { color: var(--danger); }

.danger-text { color: var(--danger); }

.rotate-feed { margin-top: 0.85rem; }

/* ---------- Billing (Daily Cast Pro plan card) ---------- */

#plan-card #past-due-banner { margin-bottom: 1.25rem; }

.price-toggle {
  display: grid;
  gap: 0.7rem;
  margin: 1.1rem 0 1.2rem;
}

@media (min-width: 480px) {
  .price-toggle { grid-template-columns: 1fr 1fr; }
}

/* These are <label> elements, so the global uppercase/tracking label rule
   would otherwise shout "$10/MO" at the reader — reset it here. */
.price-option {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: 0;
  text-transform: none;
  letter-spacing: 0.01em;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid transparent;
  border-radius: var(--radius-panel);
  padding: 0.95rem 1.1rem;
  cursor: pointer;
  transition: border-color 0.16s ease, box-shadow 0.16s ease;
}

.price-option input[type="radio"] {
  position: absolute;
  top: 0.95rem;
  right: 1.1rem;
  accent-color: var(--text);
}

.price-option-title {
  font-weight: 500;
  font-size: 0.92rem;
}

.price-option-amount {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0;
  color: var(--text-2);
}

/* The yearly option is the verified conversion lever (PRD-v2 §3) — it must
   read as the obvious, pre-selected default, not just "checked" under the
   hood. In an achromatic system that weight comes from ink: a 2px ink edge
   and a filled ink badge, not a coloured tint. */
.price-option-highlight {
  border-color: var(--text);
  border-width: 2px;
  padding: calc(0.95rem - 1px) calc(1.1rem - 1px);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--text) 8%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .price-option { transition: none; }
}

.badge {
  display: inline-block;
  background: var(--fill);
  color: var(--fill-text);
  font-size: 0.65rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-radius: var(--radius-pill);
  padding: 0.14rem 0.55rem;
  margin-left: 0.4rem;
  vertical-align: middle;
}

.hint.upsell { color: var(--text); font-weight: 500; }
.hint.upsell a { color: inherit; }

#plan-pro-panel #plan-renewal { margin-top: 0.5rem; }

/* Success-state "toast": a distinct, on-brand callout for the trial-end
   date on /billing/success, not just another paragraph — appears once the
   client-side poll confirms entitlements are live (see app.js
   initBillingSuccess). */
#success-state #trial-end-note {
  margin-top: 1.1rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  padding: 0.7rem 0.9rem;
  color: var(--text);
  font-weight: 500;
}

/* --- Instant first episode: live "recording" state on the episodes card --- */

/* Recording is a product moment, so this is the one panel where the tally
   bars keep their light — in sunrise, not amber. */
.instant-recording {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  background: var(--surface-2);
  margin-bottom: 1.25rem;
}

/* An author display value beats the UA's [hidden] rule — restore hiding. */
.instant-recording[hidden] {
  display: none;
}

.instant-recording .instant-title {
  margin: 0 0 0.2rem;
  font-weight: 500;
  color: var(--text);
}

/* Smoke is only legal on eggshell (4.71:1); on this taupe panel the quiet
   voice steps up to graphite (9.29:1) — see the guard near `.hint`. */
.instant-recording .hint {
  margin: 0;
  color: var(--text-2);
}

.instant-recording .signal-bars.recording {
  margin-top: 0.25rem;
}

/* The existing .signal-bars span rules set height as a percentage and rely
   on the container's `align-items: flex-end` to sit the bars on a shared
   baseline. A scaleY transform's default origin (center) would scale each
   bar away from that baseline instead of toward it — floating bars of
   different heights out of alignment as they pulse. Pulsing opacity instead
   sidesteps that entirely: the bars stay put and only fade, keeping the
   stagger without touching layout-sensitive geometry. */
.instant-recording .signal-bars.recording span {
  background: var(--sunrise-mid);
  animation: instant-bar-pulse 1.1s ease-in-out infinite;
}

.instant-recording .signal-bars.recording span:nth-child(2) { animation-delay: 0.15s; }
.instant-recording .signal-bars.recording span:nth-child(3) { animation-delay: 0.3s; }
.instant-recording .signal-bars.recording span:nth-child(4) { animation-delay: 0.45s; }

@keyframes instant-bar-pulse {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .instant-recording .signal-bars.recording span { animation: none; }
}

/* ---------- Policy links ---------- */

.app-footer {
  display: flex;
  gap: 20px;
  justify-content: center;
  padding: 40px 20px 32px;
  font-size: 13px;
}

.app-footer a {
  color: var(--text-2);
  text-decoration: none;
}

.app-footer a:hover { text-decoration: underline; }

/* ---------- Responsive ---------- */

@media (max-width: 720px) {
  .card { padding: 1.5rem; }
  .digest-row:first-child { padding: 1.25rem; }
  .login-card { padding: 2.2rem 1.6rem; }
  dialog.questionnaire,
  dialog.transcript-modal,
  dialog.install-instructions-modal,
  dialog.confirm-modal { padding: 1.4rem 1.4rem; }
}

/* ---------- Mobile app shell: bottom nav + tabs ---------- */

/* Below the 820px column breakpoint the dashboard stops being two columns
   side by side and becomes two tabs — Episodes first, because the episode
   that recorded overnight is what the person opened the app for. The nav is
   fixed chrome in the same key as the topbar: canvas fill, one stone
   hairline, ink for the section you are in and graphite for the ones you
   are not. It is built from semantic tokens only, so the warm-dark ladder
   flips it along with everything else — no separate dark rule below.

   Everything here is inert above 820px: .bottomnav is display:none and the
   body tab class matches nothing outside the media query, so desktop renders
   exactly as it did before this shell existed. */

/* The bar's own height, declared once: the mini now-playing bar stacks on top
   of it and has to know exactly how tall it is. Keeping it a token means the
   two can't drift apart — a padding change here would otherwise leave the
   mini bar overlapping the nav by a couple of pixels. */
:root { --bottomnav-h: 56px; }

.bottomnav {
  display: none;
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 40;
  background: var(--canvas);
  border-top: 1px solid var(--line);
  /* iOS home indicator: the bar grows downward, the hit targets stay put */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.bottomnav-item {
  position: relative;
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  /* icon 22 + gap 5 + label ~11 + padding 14 = 52, so --bottomnav-h governs
     and the bar really is the height the mini player assumes it is */
  min-height: var(--bottomnav-h);
  padding: 6px 4px 8px;
  color: var(--text-2);
  text-decoration: none;
  font-family: var(--font-sans);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1;
  -webkit-tap-highlight-color: transparent;
}

.bottomnav-item[aria-current] { color: var(--text); }

/* The active tab lights its own slice of the hairline. That, plus the ink
   step, is the whole indicator — no pill, no fill, no accent. */
.bottomnav-item[aria-current]::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 50%;
  width: 28px;
  height: 2px;
  transform: translateX(-50%);
  background: var(--text);
}

/* The global focus ring sits 2px outside its element, which would clip
   against the viewport edge down here; inset it instead. */
.bottomnav-item:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -4px;
  border-radius: 4px;
}

.bottomnav-icon {
  display: block;
  width: 22px;
  height: 22px;
}

@media (max-width: 819.98px) {
  .bottomnav { display: flex; }

  /* The bottom nav owns navigation now, so the topbar's Dashboard and
     Settings links are the same two destinations twice on one screen. Hide
     them by href — no markup change, and it stays correct on both app pages,
     which share _topbar.html. "Log out" stays: it is the one thing up there
     the bottom nav has no home for. */
  .topbar nav a[href="/app"],
  .topbar nav a[href="/app/settings"] { display: none; }

  /* The footer is the nav's next sibling (base.html renders it after the
     block body the include sits in), so it clears the fixed bar without any
     page-level class. */
  .bottomnav ~ .app-footer {
    padding-bottom: calc(var(--bottomnav-h) + 28px + env(safe-area-inset-bottom, 0px));
  }

  /* The tabs themselves: one class on <body>, keyed against the two columns
     that already exist. Episodes is the default — no class, no Sources. */
  body:not(.mobile-tab-sources) .grid-two > [aria-labelledby="sources-heading"] { display: none; }
  body.mobile-tab-sources .grid-two > [aria-labelledby="episodes-heading"] { display: none; }

  /* Today's episode is the hero of the Episodes tab: a play control sized
     for a thumb and one step up in summary type. The card's structure is
     untouched — this is scale, not a different component. */
  .digest-row:first-child .player {
    min-height: 64px;
    gap: 1rem;
  }
  .digest-row:first-child .player-toggle {
    width: 56px;
    height: 56px;
  }
  .digest-row:first-child .player-icon-play {
    border-width: 10px 0 10px 16px;
    margin-left: 4px;
  }
  /* `border: none` has to be restated: the rule above is exactly as specific
     as the base .player.playing rule and comes later in the file, so it would
     otherwise re-introduce the play triangle's borders on the pause glyph. */
  .digest-row:first-child .player.playing .player-icon-play {
    border: none;
    width: 5px;
    height: 19px;
    margin-left: 0;
    transform: translateX(-4px);
    box-shadow: 8px 0 0 var(--fill-text);
  }
  .digest-row:first-child .summary {
    font-size: 1.02rem;
    line-height: 1.62;
  }
}

/* ---------- Mini now-playing bar ---------- */

/* Rides directly above the bottom nav once an episode has played. It is a
   proxy for the row that is playing — the row keeps its own <audio> — so it
   borrows the row's own parts: .player-toggle at the same 44px, the same
   sunrise ramp for progress, the same mono for elapsed time. The one new
   idea is where the progress goes: instead of a rail inside the bar it *is*
   the bar's top edge, a 2px line the episode fills left to right, so the bar
   stays one line tall and never competes with the row's real scrubber.

   Taupe on the nav's eggshell, exactly like today's episode card on the page
   canvas, so the two fixed layers read as two surfaces and not one slab. All
   semantic tokens: the warm-dark ladder flips it with everything else. */

.miniplayer {
  display: none;
  position: fixed;
  right: 0;
  left: 0;
  /* Sits on the nav, clear of its border-box: --bottomnav-h is the nav's
     content height, so the +1px is its border-top — without it the bar
     covers the hairline the active-tab mark is drawn on. Plus whatever the
     home indicator asked for. */
  bottom: calc(var(--bottomnav-h) + 1px + env(safe-area-inset-bottom, 0px));
  z-index: 39;
  align-items: center;
  gap: 0.75rem;
  padding: 0.55rem 0.9rem;
  background: var(--surface-2);
  border-top: 1px solid var(--line);
}

.miniplayer-rail {
  position: absolute;
  top: -1px;
  right: 0;
  left: 0;
  height: 2px;
  background: var(--track);
  touch-action: none; /* drag-to-scrub: no page scroll from a horizontal drag */
}
/* Invisible touch target: the visible rail stays 2px. */
.miniplayer-rail::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -16px;
  bottom: -10px;
}
.miniplayer-rail:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
.miniplayer-knob {
  position: absolute;
  left: 0;
  top: 50%;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--sunrise-core);
  border: 2px solid var(--surface-2);
  transform: translate(-50%, -50%) scale(0);
  pointer-events: none;
  transition: transform 0.1s ease;
}
.miniplayer-rail.scrubbing .miniplayer-knob { transform: translate(-50%, -50%) scale(1.3); }

.miniplayer-fill {
  display: block;
  width: 0;
  height: 100%;
  background: linear-gradient(90deg, var(--sunrise-mid), var(--sunrise-core));
}

/* The toggle is .player-toggle — the 44px ink disc — so only the pause glyph
   needs restating: the base flip is scoped to `.player.playing`, and this bar
   is deliberately not a .player. */
.miniplayer.playing .player-icon-play {
  border: none;
  width: 4px;
  height: 15px;
  margin-left: 0;
  transform: translateX(-3px);
  background: var(--fill-text);
  box-shadow: 6px 0 0 var(--fill-text);
}

/* One line, ellipsized: "Today — Chip supply, rate cuts and a very good dog".
   Tapping it takes you to the row that is playing. */
.miniplayer-text {
  flex: 1;
  min-width: 0;
  display: block;
  padding: 0.35rem 0;
  background: none;
  border: none;
  text-align: left;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 0.85rem;
  font-weight: 400;
  letter-spacing: 0.01em;
}
.miniplayer-text:hover { background: none; }

.miniplayer-title {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.miniplayer-elapsed {
  flex-shrink: 0;
  font-size: 0.78rem;
  color: var(--text-2);
}

@media (max-width: 819.98px) {
  /* `[hidden]` alone would lose to an author `display` — same trap as
     .show-more and .instant-recording — so the display rule is written
     against :not([hidden]) rather than overridden after the fact. */
  .miniplayer:not([hidden]) { display: flex; }

  /* Room to scroll past the two fixed layers. The footer carries the
     page-level clearance (it is the last thing in the document); the
     episodes column carries its own so the last row never sits flush under
     the bar while the column is still scrolling. */
  /* Same 28px of breathing room, plus 64px for the bar (its ~62px, rounded
     up) — so 92px over the nav's own height. */
  body.mobile-mini-open .bottomnav ~ .app-footer {
    padding-bottom: calc(var(--bottomnav-h) + 92px + env(safe-area-inset-bottom, 0px));
  }
  /* Both columns: the bar is fixed chrome, and it covers the foot of
     whichever tab is open — the Sources tab's Remove buttons as readily as
     the last episode row. */
  body.mobile-mini-open .grid-two > [aria-labelledby="episodes-heading"],
  body.mobile-mini-open .grid-two > [aria-labelledby="sources-heading"] {
    margin-bottom: 64px;
  }
}

/* ---------- "Now playing" sheet (phones only) ----------
   Tapping the mini bar opens a full-screen player over the dashboard
   (_nowplaying.html). A proxy for the active row's <audio>, like the bar.
   Owner-approved mockup 2026-10-15: header (close chevron, label, grabber),
   the sunrise sphere as a liquid canvas centred in the free space, the
   current chapter + the episode date, a large scrubber with chapter dots,
   transport and an extras row. The sunrise ramp is spent on the artwork and
   the scrubber fill only — product moments, per docs/DESIGN.md. Every rule
   is inside the mobile query; above 820px the dialog is never opened (its
   JS declines) and the UA's `dialog:not([open])` keeps it display: none. */

@media (max-width: 819.98px) {
  .np-sheet {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    margin: 0;
    border: 0;
    border-radius: 0;
    padding:
      calc(env(safe-area-inset-top, 0px) + 22px)
      22px
      calc(env(safe-area-inset-bottom, 0px) + 22px);
    background: var(--canvas);
    color: var(--text);
    overflow: hidden;
    /* Nothing in the sheet scrolls; this keeps a vertical drag from scrolling
       the page underneath on iOS. The rail and the header handle their own
       pointer gestures. */
    touch-action: none;
    transform: translateY(100%);
    transition: transform 340ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  .np-sheet[open] {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto auto;
  }
  .np-sheet.is-open { transform: translateY(0); }
  .np-sheet.dragging { transition: none; }
  .np-sheet::backdrop {
    background: var(--backdrop);
    opacity: 0;
    transition: opacity 280ms ease;
  }
  .np-sheet.is-open::backdrop { opacity: 1; }
  body.np-open { overflow: hidden; }

  .np-grabber {
    position: absolute;
    top: calc(env(safe-area-inset-top, 0px) + 8px);
    left: 50%;
    width: 38px;
    height: 5px;
    margin-left: -19px;
    border-radius: 3px;
    background: var(--plate);
  }
  .np-head {
    display: grid;
    grid-template-columns: 44px 1fr 44px;
    align-items: center;
    cursor: grab;
  }
  .np-icon-btn {
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: none;
    color: var(--text);
  }
  .np-label {
    text-align: center;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-2);
  }

  .np-art-wrap {
    display: grid;
    place-items: center;
    min-height: 0;
    padding: 8px 0;
  }
  .np-art {
    display: block;
    width: min(78vw, 46dvh, 320px);
    aspect-ratio: 1;
  }

  .np-meta {
    display: grid;
    gap: 4px;
    min-width: 0;
    padding-bottom: 14px;
  }
  .np-chapter {
    min-height: 1.1em;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-2);
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .np-title {
    font-family: var(--font-display-serif);
    font-style: italic;
    font-weight: 400;
    font-size: 2rem;
    line-height: 1.05;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .np-controls { display: grid; gap: 14px; }
  .np-rail {
    position: relative;
    height: 22px;
    cursor: pointer;
    touch-action: none;
  }
  .np-rail:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 4px;
    border-radius: 4px;
  }
  .np-bar {
    position: absolute;
    left: 0;
    right: 0;
    top: 9px;
    height: 4px;
    border-radius: 2px;
    background: var(--track);
    overflow: hidden;
  }
  .np-fill {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 0;
    background: linear-gradient(90deg, var(--sunrise-mid), var(--sunrise-core));
  }
  .np-dot {
    position: absolute;
    top: 8px;
    width: 6px;
    height: 6px;
    margin-left: -3px;
    border-radius: 50%;
    background: var(--cue);
    pointer-events: none;
  }
  .np-knob {
    position: absolute;
    top: 3px;
    left: 0;
    width: 16px;
    height: 16px;
    margin-left: -8px;
    border-radius: 50%;
    background: var(--fill);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
    pointer-events: none;
    transition: transform 0.1s ease;
  }
  .np-rail.scrubbing .np-knob { transform: scale(1.25); }
  .np-times {
    display: flex;
    justify-content: space-between;
    margin-top: -6px;
    font-size: 0.75rem;
    color: var(--text-2);
    font-variant-numeric: tabular-nums;
  }

  .np-transport {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 2px;
  }
  .np-skip {
    position: relative;
    width: 48px;
    height: 48px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: none;
    color: var(--text);
  }
  .np-skip:disabled { color: var(--mark); background: none; cursor: default; }
  /* Hover tints only where hover exists: on a touchscreen :hover sticks to
     the last button tapped. The global button:hover fill is undone too. */
  .np-icon-btn:hover,
  .np-skip:hover,
  .np-pill:hover { background: none; }
  .np-play:hover { background: var(--fill); }
  @media (hover: hover) {
    .np-icon-btn:hover,
    .np-skip:not(:disabled):hover,
    .np-pill:hover { background: var(--surface-2); }
    .np-play:hover { background: color-mix(in srgb, var(--fill) 82%, var(--canvas)); }
  }
  .np-skip small {
    position: absolute;
    top: 17px;
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
  }
  .np-play {
    width: 72px;
    height: 72px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--fill);
    color: var(--fill-text);
  }
  .np-ic-pause { display: none; }
  .np-sheet.playing .np-ic-play { display: none; }
  .np-sheet.playing .np-ic-pause { display: inline; }

  .np-extras {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .np-pill {
    padding: 7px 14px;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-pill);
    background: none;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
  }
  .np-pill[hidden] { display: none; }

  .np-sheet button:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
  }

  /* The bar is the way in: it reads as tappable along its whole length. */
  .miniplayer { cursor: pointer; }
}

@media (max-width: 819.98px) and (prefers-reduced-motion: reduce) {
  .np-sheet,
  .np-sheet::backdrop { transition: none; }
}

/* Source discovery results (smart add field) */
#discovery-results { margin-top: 0.75rem; display: grid; gap: 0.5rem; }
.discovery-card {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.65rem 0.8rem; border: 1px solid var(--line); border-radius: 8px;
}
.discovery-card-info { min-width: 0; }
.discovery-card-meta, .discovery-card-desc { font-size: 0.82rem; opacity: 0.75; margin-top: 2px; }
.discovery-miss { font-size: 0.9rem; opacity: 0.85; }
.discovery-suggest-heading {
  margin: 12px 0 4px;
  font-size: 0.82rem;
  color: var(--text-2);
}

/* ---------- Onboarding wizard ---------- */

.topbar-plain { justify-content: center; }

.ob-main {
  max-width: 760px;
  margin: 0 auto;
  padding: 2rem var(--gutter) 4rem;
}

.ob-step h1 {
  font-family: var(--font-display-serif);
  font-style: italic;
  font-weight: 400;
  font-synthesis: none;
  font-size: clamp(30px, 6vw, 38px);
  line-height: 1.08;
  letter-spacing: -0.01em;
  margin: 0.2rem 0 0.6rem;
}

/* The step's own header: a back arrow and a progress rail, the shape every
   phone setup flow uses. It replaces a "Step n of N" line — the rail says
   how far along you are at a glance, and the sentence stays for screen
   readers. On the first step the arrow is swapped for a same-size
   placeholder so the rail does not jump left when you move off it. */
.ob-nav { display: flex; align-items: center; gap: 0.9rem; margin: 0 0 1.4rem; }
.ob-nav-back {
  display: inline-flex;
  width: 36px;
  height: 36px;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--text);
  text-decoration: none;
}
.ob-nav-back:hover { background: var(--surface-2); }
.ob-nav-back-placeholder { width: 36px; height: 36px; }
.ob-progress-bar {
  flex: 1;
  height: 4px;
  border-radius: 999px;
  background: var(--track);
  overflow: hidden;
}
.ob-progress-fill { display: block; height: 100%; background: var(--fill); border-radius: 999px; }

/* The Name step's fields stack like the settings form's: a gap between
   name and country, and the country select as wide as the name input. */
.ob-form .field { margin-bottom: 1.2rem; }
#ob-country { width: 100%; }

.ob-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  justify-content: flex-end;
  align-items: center;
  margin-top: 1.6rem;
}

/* All set's app hand-off (spec 2026-10-30 §4.4; APP_HANDOFF_ENABLED). The
   store links show at every width. On a phone, "Open the app" hands this
   session to the installed app; on a desktop it would have nowhere to go, so
   the QR codes stand in for it there. 820px is the same breakpoint as the
   mobile app shell (.grid-two / @media max-width: 819.98px). */
.ob-handoff {
  margin-top: 1.4rem;
  padding: 1.2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  display: grid;
  gap: 0.9rem;
}
.ob-handoff-title { margin: 0; font-weight: 500; }
.ob-open-app { width: 100%; }
.ob-stores { display: grid; gap: 0.6rem; grid-template-columns: 1fr; }
.ob-store { display: grid; gap: 0.6rem; justify-items: center; }
.ob-store .btn { width: 100%; text-align: center; }
/* The QR sits on a white tile in both themes: segno draws black modules on
   a transparent ground, and a camera needs dark-on-light to read it. */
.ob-qr {
  display: none;
  background: #fff;
  padding: 0.6rem;
  border-radius: var(--radius-panel);
  line-height: 0;
}
.ob-qr svg { width: 132px; height: 132px; }
.ob-qr-caption { display: none; margin: 0; font-size: 0.8rem; color: var(--text-2); text-align: center; }
@media (min-width: 820px) {
  .ob-open-app { display: none; }
  .ob-stores { grid-template-columns: 1fr 1fr; }
  .ob-qr { display: block; }
  .ob-qr-caption { display: block; }
}

/* The offer. Two side-by-side plan cards asked the reader to compare before
   they had anything to compare with, and led with Free — so there is one
   card now, Pro, and the only choice inside it is how to pay. Free is not
   hidden, it is demoted: an outline pill under the card with its limits in
   one caption line. Yearly is the pre-selected tile, same reasoning as the
   settings page's highlight (PRD-v2 §3). */
.ob-plans { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 640px) { .ob-plans { grid-template-columns: 1fr 1fr; } }
.ob-plan {
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1.6rem 1.5rem;
  display: flex;
  flex-direction: column;
}
.ob-plan-pro { background: var(--surface-2); border-color: transparent; }
.ob-plan-name { letter-spacing: 0.14em; text-transform: uppercase; font-size: 0.75rem; color: var(--text-2); margin: 0 0 1rem; display: flex; align-items: center; gap: 0.6rem; }
.ob-features { list-style: none; padding: 0; margin: 0 0 1.6rem; display: grid; gap: 0.7rem; }
.ob-features li { padding-left: 1.4rem; position: relative; }
.ob-features li::before { content: "✓"; position: absolute; left: 0; color: var(--text-3); font-size: 0.85em; }

.ob-sub { color: var(--text-3); margin: 0 0 1.2rem; }

.ob-offer {
  background: var(--surface-2);
  border-radius: var(--radius-card);
  padding: 1.25rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
@media (min-width: 640px) { .ob-offer { padding: 1.5rem 1.4rem; } }
/* Inside the offer the flex gap does the spacing, so the two shared blocks
   drop the bottom margins they carry in the already-Pro card. */
.ob-offer .ob-plan-name,
.ob-offer .ob-features { margin: 0; }

.ob-tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.6rem; }

/* These are <label>s, so the global uppercase/tracking label rule has to be
   reset here — same reason as .price-option. */
.ob-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: 0;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  padding: 0.85rem 0.9rem 0.75rem;
  cursor: pointer;
  transition: border-color 0.16s ease;
}
/* The radio is drawn by .ob-tile-radio, but it stays in the tab order and
   keeps its native arrow-key behaviour — clipped, never display:none. */
.ob-tile input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.ob-tile:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
/* The pick reads as ink weight, not a coloured tint; the padding gives back
   the pixel the second border takes, so nothing shifts on change. */
.ob-tile:has(input:checked) {
  border-color: var(--text);
  border-width: 2px;
  padding: calc(0.85rem - 1px) calc(0.9rem - 1px) calc(0.75rem - 1px);
}
.ob-tile-radio {
  width: 18px;
  height: 18px;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--radius-pill);
}
.ob-tile:has(input:checked) .ob-tile-radio {
  border: 5.5px solid var(--text);
  background: var(--surface);
}
.ob-tile-save { position: absolute; top: 0.75rem; right: 0.8rem; margin-left: 0; }
.ob-tile-price { font-size: 1.4rem; font-weight: 500; letter-spacing: -0.02em; line-height: 1.1; margin-top: 0.25rem; }
.ob-tile-per { font-size: 0.75rem; font-weight: 400; color: var(--text-3); }

.ob-cta { width: 100%; height: 48px; padding: 0 1.3rem; font-size: 0.95rem; }
/* Smoke would land at 4.36:1 on taupe — this line sits inside the offer
   card, so it steps up to graphite, same as .price-option .hint. */
.ob-fineprint { margin: -0.55rem 0 0; text-align: center; font-size: 0.8rem; color: var(--text-2); }
/* The friends-offer fineprint is a long sentence, not a short centered line
   like the trial's — left-align it so it reads as a paragraph. */
.ob-fineprint-long { text-align: left; }
.ob-rule { border: 0; height: 1px; background: var(--line); margin: 0; }
.ob-plus { margin: 0; font-size: 0.9rem; font-weight: 500; }

.ob-free { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; margin-top: 1.2rem; }
.ob-free-caption { margin: 0; font-size: 0.75rem; color: var(--text-3); }

@media (prefers-reduced-motion: reduce) {
  .ob-tile { transition: none; }
}

/* Voice tiles */
.ob-voices { display: grid; gap: 0.75rem; grid-template-columns: 1fr; margin-top: 1rem; }
@media (min-width: 640px) { .ob-voices { grid-template-columns: 1fr 1fr; } }
.ob-voice {
  display: flex; gap: 0.9rem; align-items: flex-start;
  border: 1px solid var(--line); border-radius: var(--radius-panel); padding: 0.9rem 1rem;
}
.ob-voice:has(input:checked) { border-color: var(--line-strong); background: var(--surface-2); }
.ob-voice:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.ob-voice.locked { opacity: 0.72; }
.ob-voice-play {
  width: 44px; height: 44px; padding: 0; flex: none; display: inline-flex; align-items: center; justify-content: center;
}
.ob-voice-play.playing { background: var(--surface-2); color: var(--text); border-color: var(--line-strong); }
.ob-voice-body { display: flex; flex-direction: column; gap: 0.2rem; cursor: pointer; color: inherit; margin-bottom: 0; }
/* .ob-voice-body is a <label>, so it inherits the global form-label rule —
   uppercase and letter-spaced. These tiles are content, not field labels. */
.ob-voice-body,
.ob-voice-body span { text-transform: none; letter-spacing: normal; }
.ob-voice-body input { position: absolute; opacity: 0; pointer-events: none; }
.ob-voice-label { font-weight: 500; font-size: 0.95rem; display: flex; align-items: center; gap: 0.5rem; }
.ob-voice-desc { color: var(--text-3); font-size: 0.9rem; }

/* All set */
.ob-sources-panel .chooser { margin-top: 0; }
