/* AudioPress — tenant podcast-site components.
 *
 * The foundation (design tokens, reset, base typography, focus rings, skip-link,
 * .sr-only) lives in base.css and MUST be loaded first (see layout.rs). This
 * file holds only tenant-site components (banner, episode list/detail, player,
 * transcripts, search, section-nav, …) — it is NOT loaded on the platform
 * landing page, so its class names can't collide with it. */

/* Self-linking headings (page section headers + show-notes headings): clickable
   and deep-linkable, but visually unchanged — the anchor inherits the heading's
   colour and only reveals its link affordance on hover/focus. */
.heading-link {
  color: inherit;
  text-decoration: none;
}

.heading-link:hover,
.heading-link:focus-visible {
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.15em;
}

/* Small "open external" affordance beside a show-notes heading whose pseudo-
   heading was a link (e.g. `<a>Netstack.FM</a>`). The heading text itself stays
   the self-permalink; this is the way out to the linked site. Sized one step
   below the heading so it reads as auxiliary, not primary. */
.heading-extlink {
  display: inline-flex;
  align-items: center;
  margin-inline-start: var(--space-2xs);
  padding-inline: var(--space-3xs);
  font-size: var(--step--1);
  font-weight: 400;
  color: var(--muted);
  text-decoration: none;
  vertical-align: middle;
}

.heading-extlink:hover,
.heading-extlink:focus-visible {
  color: var(--accent);
}

/* Keep the stacked sticky banners from covering a target jumped to via its
   anchor. Deep targets (headings, cues) sit below the *stuck* dock, so clear the
   whole stack (`--stack-h`, kept in sync by episode-dock.js).
   NB: the player is intentionally *not* listed — it's sticky (never an anchor
   target), and a scroll-margin on it would be applied to the browser's
   focus-scroll, jerking the page when you click the progress bar. */
.show-notes :where(h2, h3, h4, h5, h6),
.transcript h2[id] {
  scroll-margin-block-start: var(--stack-h, 5rem);
}

/* The reading-column section headers ("Show notes" / "Transcript") sit a clear
   step above their content — between the episode title (h1, step-3) and the
   in-notes headings — so the page reads title → section → sub-section. */
.show-notes > h2,
.transcript > h2[id] {
  font-size: var(--step-2);
  /* Don't let the first line of content hug the section header. */
  margin-block-end: var(--space-s);
}

/* Show-notes headings are demoted (the feed's h1/h2 become h3/h4 …) so the page
   outline stays h1 → h2 → h3. They need a visible hierarchy that *starts below*
   the section header above — otherwise a "## Chapters" inherits the browser
   default and collapses to body size, reading as bold text rather than a title.
   h3/h4 (a feed's top sections, however it nested them) share the top content
   size; h5/h6 cascade down. */
.show-notes :where(h3, h4, h5, h6) {
  margin-block: var(--space-m) var(--space-2xs);
  line-height: 1.2;
}

.show-notes :where(h3, h4) {
  font-size: var(--step-1);
}

.show-notes h5 {
  font-size: var(--step-0);
}

.show-notes h6 {
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* The hero ("Top") has only the site banner above it — the dock is still below
   it in flow, not yet stuck — so it clears just the banner. */
#episode-top {
  scroll-margin-block-start: calc(var(--banner-h, 3.5rem) + var(--space-2xs));
}

/* ---- Layout regions ----------------------------------------------------- */

.site-footer {
  background: var(--surface);
  padding-inline: var(--space-s);
}

main {
  flex: 1 1 auto;
  inline-size: 100%;
  /* Wide by default (the episode grid fills it); reading-heavy pages opt back
     down to `--measure` via `.episode-detail`. Side padding grows with the
     viewport so wide screens get breathing room rather than edge-to-edge text. */
  max-inline-size: var(--measure-wide);
  margin-inline: auto;
  padding-block: var(--space-l) var(--space-xl);
  padding-inline: clamp(var(--space-s), 4vw, var(--space-xl));
}

/* Reading-heavy pages (a single episode) stay at the comfortable prose
   measure, centred within the wider main. */
.episode-detail {
  max-inline-size: var(--measure);
  margin-inline: auto;
}

.site-footer {
  border-block-start: 1px solid var(--border);
  color: var(--muted);
  font-size: var(--step--1);
  padding-block: var(--space-xs);
}

.site-footer small {
  display: block;
  max-inline-size: var(--measure-wide);
  margin-inline: auto;
  /* `margin-inline: auto` centres the block; this centres the text in it. */
  text-align: center;
}

/* ---- Podcast banner (all pages) ----------------------------------------- */

.site-banner {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-header);
  background: var(--surface);
  border-block-end: 1px solid var(--border);
}

/* ---- Language-mismatch banner ------------------------------------------ */

/* A courtesy notice shown (in the visitor's own language) when their browser
   doesn't list the show's language — see `service::i18n`. Sits above the
   sticky site banner in normal flow, full-bleed, tinted with the accent so
   it reads as a system message without alarming. `dir="rtl"` is set on the
   element for RTL languages, so logical properties handle the mirroring. */
.lang-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs) var(--space-s);
  padding: var(--space-2xs) var(--space-s);
  font-size: var(--step--1);
  color: var(--on-accent);
  background: var(--accent);
  text-align: center;
}

.lang-banner-text {
  margin: 0;
  max-inline-size: 70ch;
}

/* The dismiss control reads as a button against the accent fill: outline
   style so it doesn't compete with the page's primary accent buttons. */
.lang-banner-dismiss {
  flex: none;
  min-block-size: var(--min-tap);
  padding-block: var(--space-3xs);
  padding-inline: var(--space-s);
  font: inherit;
  font-weight: 600;
  color: var(--on-accent);
  background: transparent;
  border: 1px solid currentColor;
  border-radius: 999px;
  cursor: pointer;
}

.lang-banner-dismiss:hover,
.lang-banner-dismiss:focus-visible {
  color: var(--accent);
  background: var(--on-accent);
}

.banner-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs) var(--space-m);
  inline-size: 100%;
  max-inline-size: var(--measure-wide);
  margin-inline: auto;
  padding-block: var(--space-2xs);
  padding-inline: clamp(var(--space-s), 4vw, var(--space-xl));
}

.banner-brand {
  margin: 0;
  font-size: var(--step-0);
  line-height: 1.1;
}

.banner-brand a {
  color: var(--text);
  font-weight: 700;
  text-decoration: none;
}

.banner-brand a:hover {
  color: var(--accent);
  text-decoration: none;
}

/* Compact, single-row search in the banner (keep the input + button inline). */
.site-banner form[role="search"] {
  flex: 1 1 12rem;
  flex-wrap: nowrap;
  max-inline-size: 26rem;
  margin-block: 0;
}

/* Let the input shrink so it never pushes the button onto a second row. */
.site-banner input[type="search"] {
  flex: 1 1 6rem;
}

/* The label is redundant next to the placeholder; keep it for AT, hide it. */
.site-banner form[role="search"] label {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  flex-basis: auto;
}

/* ---- Theme toggle (banner) ---------------------------------------------- */

/* A subtle round icon button. It never shrinks/grows, so it stays put while the
   search field flexes; on a phone it tucks beside the brand. */
.theme-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--min-tap);
  block-size: var(--min-tap);
  padding: 0;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 999px;
  cursor: pointer;
}

.theme-toggle:hover {
  background: color-mix(in oklab, var(--text) 9%, transparent);
}

.theme-glyph {
  inline-size: 1.25rem;
  block-size: 1.25rem;
}

/* Show the glyph for the *current* effective scheme: default follows the OS
   (prefers-color-scheme); an explicit override (`:root[data-theme]`) wins. */
.theme-icon {
  display: none;
  line-height: 0;
}

.theme-icon-sun {
  display: inline-flex;
}

@media (prefers-color-scheme: dark) {
  .theme-icon-sun {
    display: none;
  }

  .theme-icon-moon {
    display: inline-flex;
  }
}

:root[data-theme="light"] .theme-icon-sun {
  display: inline-flex;
}

:root[data-theme="light"] .theme-icon-moon {
  display: none;
}

:root[data-theme="dark"] .theme-icon-sun {
  display: none;
}

:root[data-theme="dark"] .theme-icon-moon {
  display: inline-flex;
}

/* ---- Feed header (home) ------------------------------------------------- */

.feed-header {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  gap: var(--space-m);
  margin-block-end: var(--space-l);
}

.feed-cover {
  inline-size: clamp(96px, 28vw, 160px);
  block-size: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.feed-meta {
  flex: 1 1 18rem;
}

/* Keep the show blurb at a readable line length even when the hero is wide. */
.feed-description {
  max-inline-size: 60ch;
}

/* Branded "listen on" buttons. */
.platforms ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-block-start: var(--space-s);
  padding: 0;
  list-style: none;
}

/* Compact brand pills — secondary nav, so light-weight and small. */
.platform {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3xs);
  min-block-size: var(--min-tap);
  padding: var(--space-3xs) var(--space-xs);
  font-size: var(--step--1);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  text-decoration: none;
}

.platform:hover {
  border-color: var(--muted);
  background: var(--bg);
  text-decoration: none;
}

/* Official glyph in the platform's brand colour. */
.platform-icon {
  inline-size: 1.05em;
  block-size: 1.05em;
  flex: none;
}

.platform-spotify .platform-icon {
  fill: #1db954;
}

.platform-youtube .platform-icon {
  fill: #f00;
}

.platform-rss .platform-icon {
  fill: #f26522;
}

/* ---- Cards: episode list + search results ------------------------------ */

/* Section label ("Episodes") — give it room so it doesn't sit lonely on top of
   the cards. */
.section-heading {
  margin-block: 0 var(--space-m);
  font-size: var(--step-1);
}

/* Fluid grid: one column on phones, filling into 2–3 columns as the viewport
   widens (no media queries — `auto-fill` + `minmax` do the responsive work). On
   the narrow episode-detail column it collapses back to a single column. */
/* Search results: a plain fluid grid (no size weighting). */
.results {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
  gap: var(--space-m);
  align-items: start;
}

/* Episode mosaic: a wrapping flex row where every card grows to fill its line,
   so each line is gap-free *by construction* (no fragmentation) — and bigger
   episodes carry a slightly larger basis, so they get a touch more room. The
   ramp is deliberately subtle (a long episode shouldn't dwarf a medium one);
   the size cue is reinforced by the fluid title scale and whether the card is
   wide enough to show its cover (see the container query below). Fully fluid
   (cards-per-line follow the width, down to one on a phone) and in source order
   (flex never reorders). */
.episodes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-m);
  /* Equal-height rows: every card stretches to the tallest one on its line. */
  align-items: stretch;
}

/* Each card is its own query container, so its inner layout (cover vs no-cover,
   title size) reacts to the card's own width rather than the viewport's. The li
   is a flex box only so its single <article> child fills the stretched height. */
.episodes > li {
  display: flex;
  min-inline-size: 0;
  container-type: inline-size;
}

.episodes > li > article {
  flex: 1;
}

/* `flex: <grow> 1 <basis>` — basis sets the preferred width (≈ how many fit per
   line); grow (kept ∝ basis) lets the line fill without re-ordering the tiers.
   The bases sit close together on purpose: the tier is a nudge, not a chasm. */
.episode--sm {
  flex: 16 1 18rem;
}

.episode--md {
  flex: 20 1 22rem;
}

.episode--lg {
  flex: 24 1 26rem;
}

.episodes article,
.results article {
  padding: var(--space-m);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* Compact card (narrow / small episode): a single stacked column with no cover.
   The cover + two-column layout switch in only once the card is wide enough to
   carry them (see the container query below), so small cards stay lean. The
   summary row takes the slack (`1fr`) so the card fills its stretched height and
   the bottom-pinned date lines up across a row. */
.episodes article {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "title"
    "summary";
  gap: var(--space-xs);
  /* Extra headroom for the eyebrow tab that straddles the top edge. */
  padding-block-start: calc(var(--space-m) + var(--space-2xs));
}

/* Tags eyebrow: a little raised "tab" that escapes over the card's top edge —
   the accent season/episode chip + muted duration in their own rounded
   container, lifted half above the border for a touch of depth (the cherry on
   top). Absolutely placed so it claims no grid row. */
.episode-tags {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--space-m);
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  margin: 0;
  padding: var(--space-3xs) var(--space-s);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: var(--shadow-soft);
}

.tag {
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
}

/* Season/episode: the most visible meta — accent, bold, slightly tracked. */
.tag-episode {
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--accent);
}

/* Duration: always the same muted colour (the number conveys length). Inside
   the eyebrow tab it reads as plain text with a middot separator from the
   episode chip — not its own nested pill. */
.tag-duration {
  color: var(--muted);
}

.episode-tags .tag-episode + .tag-duration::before {
  content: "·";
  margin-inline-end: var(--space-2xs);
  color: var(--muted);
}

/* Length gauge: five rising bars in the eyebrow, `is-on` ones lit in accent —
   an at-a-glance "short vs long" read beside the duration (a 12-min short
   lights one bar, a 90-min deep-dive all five). Decorative + `aria-hidden`;
   the duration text carries the value for assistive tech. Bars share a
   baseline (`flex-end`) and step up in height via `:nth-child`. */
.ep-gauge {
  display: inline-flex;
  align-items: flex-end;
  gap: 2px;
  margin-inline-start: var(--space-2xs);
  /* Optical lift so the little bars sit on the text baseline, not below it. */
  transform: translateY(-1px);
}

.ep-gauge-bar {
  inline-size: 3px;
  border-radius: 1px;
  /* Unlit bars: the track. Lit bars override below. */
  background: var(--border);
}

.ep-gauge-bar.is-on {
  background: var(--accent);
}

.ep-gauge-bar:nth-child(1) {
  block-size: 4px;
}
.ep-gauge-bar:nth-child(2) {
  block-size: 6px;
}
.ep-gauge-bar:nth-child(3) {
  block-size: 8px;
}
.ep-gauge-bar:nth-child(4) {
  block-size: 10px;
}
.ep-gauge-bar:nth-child(5) {
  block-size: 12px;
}

.episode-title {
  grid-area: title;
  margin: 0;
}

.episode-summary {
  grid-area: summary;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-inline-size: 0;
}

.episode-summary > * {
  margin: 0;
}

.episode-date {
  color: var(--muted);
}

/* Pin the date to the bottom of the (stretched) summary, so it lines up with
   the bottom of the cover beside it. */
.episode-summary .episode-date {
  margin-block-start: auto;
}

/* Only the excerpt is clamped (it's a teaser) — the title is never truncated. */
.episode-excerpt {
  color: var(--muted);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

/* Search result card: compact, with the matched "source" as an eyebrow so the
   metadata reads apart from the snippet content (no bottom-pinned gap). */
.results article {
  padding: var(--space-s);
}

.result-link {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  color: inherit;
  text-decoration: none;
}

.result-source {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin: 0;
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  /* `--text`, not `--muted`: the eyebrow is small (`step--1`) so it lives at
     AA's 4.5:1 threshold — `--muted` was on the edge under axe-core's check.
     The visual softness comes from the smaller, tracked, uppercase type. */
  color: var(--text);
}

.result-field {
  font-weight: 700;
}

.result-time {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.result-title {
  margin: 0;
  font-size: var(--step-0);
  color: var(--accent);
}

.result-link:hover .result-title {
  text-decoration: underline;
}

.result-snippet {
  margin: 0;
  color: var(--text);
}

.results-summary {
  color: var(--muted);
}

/* "← Back to <episode>" above episode-scoped results: leaves search for the
   full reading view. */
.results-back {
  margin-block-end: var(--space-2xs);
  font-size: var(--step--1);
}

.results-back a {
  display: inline-flex;
  min-block-size: var(--min-tap);
  align-items: center;
  color: var(--muted);
  text-decoration: none;
}

.results-back a:hover,
.results-back a:focus-visible {
  color: var(--accent);
  text-decoration: underline;
}

/* ---- 404 page ----------------------------------------------------------- */

.not-found {
  /* Centred within the wide `main`, like the episode-detail reading column, so
     error and content pages share the same optical centre line. */
  max-inline-size: var(--measure);
  margin-inline: auto;
}

/* The "404" eyebrow: a small tracked accent label above the heading — enough to
   say "error" without shouting it as the headline (the headline orients). */
.nf-code {
  margin: 0 0 var(--space-2xs);
  font-size: var(--step--1);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.nf-lead {
  font-size: var(--step-1);
}

.nf-context {
  max-inline-size: 60ch;
  color: var(--muted);
}

/* The show's name, leading its description so the blurb reads as orientation. */
.nf-show {
  font-weight: 700;
  color: var(--text);
}

.nf-suggestions {
  margin-block-start: var(--space-l);
}

.nf-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

.nf-list a {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs);
  padding-block: var(--space-2xs);
  min-block-size: var(--min-tap);
  color: inherit;
  text-decoration: none;
}

.nf-list a:hover .nf-ep-title {
  text-decoration: underline;
}

/* Cover art. In the list cards (flex) `float` is ignored and `flex: none`
   keeps it square; in the episode-detail header (flow-root) it floats so the
   prose wraps around it — the newspaper recipe. */
/* Cover art. The element is sized to the artwork's own ratio (`inline-size`
   fixed, `block-size: auto`) — never cropped, never letterboxed — so the border
   hugs the art exactly and is never bigger than it. */
.episode-cover {
  block-size: auto;
  /* A placeholder fill so the framed box isn't an empty flash before the image
     paints (covers are also cached hard — see the cover route). */
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* Episode-detail header: a small floated cover the prose wraps around. */
.episode-header .episode-cover {
  float: inline-start;
  inline-size: clamp(72px, 22%, 104px);
  margin-inline-end: var(--space-s);
  margin-block-end: var(--space-2xs);
}

/* List card: a framed cover with subtle depth, filling its grid column. The
   *frame* (border, radius, depth, clip) lives on the wrap so the image can
   gently zoom on hover without escaping its rounded corners. Hidden on compact
   cards; the wide container query re-shows it (`display: block`). */
.episode-cover-wrap {
  display: none;
  grid-area: cover;
  align-self: start;
  inline-size: 100%;
  overflow: clip;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-soft);
}

/* The wrapped img fills the frame; its own border/radius/bg are reset onto the
   wrap above (otherwise we'd double the frame). */
.episode-cover-wrap > .episode-cover {
  display: block;
  inline-size: 100%;
  block-size: auto;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: none;
}

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

/* The title always shows in full — never clamped — on its own line(s). It scales
   fluidly with the *card* width (cqi); the compact range (sm cards) is capped at
   `--step-1`, while md/lg cards start there and climb to `--step-2` (below), so a
   bigger episode always reads bolder — a type-driven "this one's bigger" cue. */
.episodes h3 {
  font-size: clamp(var(--step-0), 5cqi, var(--step-1));
  line-height: 1.2;
  color: var(--accent);
}

.episodes a:hover h3 {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* Two per-card width gates (the tier class is on the <li> query container, so
   each card reads its *own* width — same-tier cards on a line stay identical,
   no sub-pixel flicker):

     ≥ 22rem — md/lg titles step up a size. The title is full-width on its own
       row, so the prominence costs the text nothing.
     ≥ 30rem — md/lg gain the cover + the two-column layout. Held back to here
       deliberately: at the ~22rem of a cramped 3-up row the artwork (which adds
       little) squeezed the text to a sliver, so a narrow card now stays lean,
       full-width text — the cover only returns once the card has real room
       (roomier 2-up / 1-up rows). sm cards never carry a cover. */
@container (min-width: 22rem) {
  .episode--md h3,
  .episode--lg h3 {
    font-size: clamp(var(--step-1), 6cqi, var(--step-2));
  }
}

@container (min-width: 30rem) {
  .episode--md article,
  .episode--lg article {
    grid-template-columns: clamp(96px, 26%, 140px) 1fr;
    grid-template-rows: auto 1fr;
    grid-template-areas:
      "title   title"
      "cover   summary";
    column-gap: var(--space-m);
    row-gap: var(--space-xs);
  }

  .episode--md .episode-cover-wrap,
  .episode--lg .episode-cover-wrap {
    display: block;
  }
}

time {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ---- Card motion (gated on prefers-reduced-motion) ---------------------- */

/* Cohesive motion vocabulary, three repeating ideas (per the animations
   guideline: a few concepts, applied consistently):
     • LIFT   — a card rises on hover/focus (tangible "this is reachable")
     • PRESS  — and settles back down on :active (tactile "selected")
     • REVEAL — entrance rise+fade as it scrolls in; cover zoom on hover
   The border-color shift is colour-only, so it's ungated (safe on touch +
   keyboard, and it's the one feedback reduced-motion users still get). */
.episodes > li:hover > article,
.results > li:hover > article,
.episodes > li:focus-within > article,
.results > li:focus-within > article {
  border-color: var(--muted);
}

/* With the whole-card click helper active (card-link.js sets `js-card-link`),
   the episode card reads as clickable — but keep the excerpt on a text cursor,
   since the helper preserves selection. Gated on the flag so the cursor is
   never a misleading affordance with JS off (where only the title is a link). */
.js-card-link .episodes > li > .episode {
  cursor: pointer;
}
.js-card-link .episodes .episode-excerpt {
  cursor: text;
}

@media (prefers-reduced-motion: no-preference) {
  .episodes article,
  .results article {
    /* Base = the *exit* timing: a slightly slower settle feels considered.
       The enter is made snappier on the hover/focus rule itself (below). */
    transition:
      transform var(--dur-default) var(--ease-out),
      box-shadow var(--dur-default) var(--ease-out),
      border-color var(--dur-quick) var(--ease-out);
  }

  /* Cover zooms within its clip frame — only the wrapped img moves. */
  .episodes .episode-cover-wrap > .episode-cover {
    transition: transform var(--dur-default) var(--ease-out);
  }

  /* LIFT. Hover is gated to real pointers (a fine pointer that can hover) so
     it never sticks after a tap on touch; keyboard `:focus-within` lifts
     unconditionally (it's exactly the feedback a keyboard user wants). The
     enter transition is quick (snappy response), the exit slow (the base). */
  @media (hover: hover) and (pointer: fine) {
    .episodes > li:hover > article,
    .results > li:hover > article {
      transform: translateY(-3px);
      box-shadow: var(--shadow-card);
      transition:
        transform var(--dur-quick) var(--ease-out),
        box-shadow var(--dur-quick) var(--ease-out);
    }

    .episodes > li:hover .episode-cover-wrap > .episode-cover {
      transform: scale(1.045);
    }
  }

  .episodes > li:focus-within > article,
  .results > li:focus-within > article {
    transform: translateY(-3px);
    box-shadow: var(--shadow-card);
    transition:
      transform var(--dur-quick) var(--ease-out),
      box-shadow var(--dur-quick) var(--ease-out);
  }

  /* PRESS. On click/tap the lifted card settles back down and compresses a
     hair — a tactile "got it" the instant an episode is selected. `:active`
     wins over `:hover` (later + more specific via the same chain), snappier
     still. Works on touch (tap) and mouse alike, which is what we want. */
  .episodes > li:active > article,
  .results > li:active > article {
    transform: translateY(0) scale(0.985);
    box-shadow: var(--shadow-soft);
    transition:
      transform var(--dur-quick) var(--ease-out),
      box-shadow var(--dur-quick) var(--ease-out);
  }

  @supports (animation-timeline: view()) {
    .episodes > li,
    .results > li {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 55%;
    }
  }
}

@keyframes rise-in {
  from {
    opacity: 0;
    transform: translateY(1rem);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ---- Search results ----------------------------------------------------- */

/* `scroll-margin` keeps the stacked sticky banners from covering the top of
   results when they auto-scroll into view (datastar `data-init`). It's the
   header height PLUS a gap, so "Results" lands with breathing room below the
   nav rather than flush against it — matching the page's normal top padding. */
.search-results {
  scroll-margin-block-start: calc(var(--stack-h, 4rem) + var(--space-l));
  /* A little more air below the sticky banner. */
  margin-block-start: var(--space-2xs);
}
/* The heading + count read as a pair (tight), then a clear gap before the
   result grid so the section doesn't feel cramped (the global reset zeroes
   heading/`p` margins, so the section owns its own rhythm). */
.search-results > h2 {
  margin-block-end: var(--space-2xs);
}
.search-results > .results-summary {
  margin-block-end: var(--space-m);
}

mark {
  padding-inline: 0.1em;
  color: inherit;
  background: color-mix(in oklab, var(--accent) 28%, transparent);
  border-radius: 3px;
}

/* Flat fallback for engines without `color-mix`. */
@supports not (background: color-mix(in oklab, red, blue)) {
  mark {
    background: var(--border);
  }
}

.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  margin-block-start: var(--space-m);
}

.pagination a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--min-tap);
  padding-inline: var(--space-s);
  font-weight: 600;
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.pagination a:hover {
  border-color: var(--muted);
  text-decoration: none;
}

.pagination .page-status {
  color: var(--muted);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
}

/* ---- Episode detail ----------------------------------------------------- */

.episode-header {
  display: flow-root; /* contain the floated cover */
  /* Breathing room before the player / content below the hero. */
  margin-block-end: var(--space-m);
}

/* Vertical rhythm inside the hero (the floated cover rules out flex `gap`). */
.episode-header h1 {
  margin-block-end: var(--space-2xs);
}

.episode-header .episode-meta {
  margin-block-end: var(--space-2xs);
}

audio {
  inline-size: 100%;
  margin-block: var(--space-s);
}

/* ---- Sticky episode dock ------------------------------------------------ */

/* The player (and a compact info bar) pin below the site banner as you scroll,
   so the episode stays identified and playable anywhere on a long page. Pure
   CSS handles the pinning; episode-dock.js only reveals the info bar once the
   hero is gone and keeps `--banner-h`/`--stack-h` in sync. */
.episode-dock {
  position: sticky;
  inset-block-start: var(--banner-h, 3.5rem);
  /* Sits below the site banner (`--z-header`), above the floating
     section-nav (`--z-side-nav`), and above the page content. The
     dock forms its own stacking context, so the download popover
     anchored inside it inherits this z relative to the rest of the
     page — that's how the popover beats the section-nav by default
     and still loses to the banner. */
  z-index: var(--z-dock);
  /* Opaque so scrolled content doesn't show through the pinned bar. */
  background: var(--bg);
}

/* Roomy around the player while it sits in the hero (not pinned). */
.episode-dock audio {
  margin-block: var(--space-m);
}

/* When pinned, the dock is a compact stacked banner: trim the player's margin
   so the bar isn't huge, but keep a little padding so it still breathes. */
.episode-detail.is-docked .episode-dock {
  padding-block: var(--space-2xs);
}

.episode-detail.is-docked .episode-dock audio {
  margin-block: var(--space-2xs);
}

/* The compact title + meta bar: hidden until the hero scrolls away (the
   `is-docked` class is toggled by episode-dock.js), then revealed as the second
   stacked banner above the player. */
.dock-summary {
  display: none;
}

.episode-detail.is-docked .dock-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3xs) var(--space-s);
  padding-block: var(--space-xs);
}

.dock-title {
  font-weight: 700;
  /* Keep the bar to one tidy line; the full title lives in the hero above. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-inline-size: 100%;
}

.dock-meta {
  color: var(--muted);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A subtle reveal that respects reduced-motion (functional content is never
   gated *behind* motion — it just animates in when motion is allowed). */
@media (prefers-reduced-motion: no-preference) {
  .episode-detail.is-docked .dock-summary {
    animation: dock-reveal 180ms var(--ease-out);
  }
}

@keyframes dock-reveal {
  from {
    opacity: 0;
    transform: translateY(-0.25rem);
  }
}

/* ---- Player extra controls (below <audio>): skip + speed --------------- */

/* A single row sitting beneath the native scrubber. On a phone it wraps to
   two rows (skip-back + skip-forward stay together; the speed stepper drops
   below). When the dock is pinned (the `is-docked` body state), this row
   tightens so the pinned bar stays slim. */
.player-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs) var(--space-s);
  padding-block-end: var(--space-2xs);
}

.episode-detail.is-docked .player-controls {
  padding-block: 0 var(--space-3xs);
  gap: var(--space-3xs) var(--space-xs);
}

/* Skip 15 / 30 — chip-shaped, icon on one side, label on the other. The
   icon is a looped arrow with the number inside it (browser-rendered SVG
   text, so it scales with the chip + recolours with `currentColor`). */
.player-skip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  min-block-size: var(--min-tap);
  padding: var(--space-3xs) var(--space-xs);
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
}

.player-skip:hover {
  border-color: var(--muted);
  background: var(--bg);
}

.player-skip-icon {
  inline-size: 1.4em;
  block-size: 1.4em;
  flex: none;
}

.player-skip-label {
  font-variant-numeric: tabular-nums;
}

/* Speed stepper: − [value] + . The value is fixed-width tabular-nums so the
   row doesn't reflow as the user steps. */
.player-speed {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  padding: var(--space-3xs) var(--space-2xs);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
}

.player-speed-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--min-tap);
  min-block-size: var(--min-tap);
  padding: 0;
  color: var(--text);
  background: none;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
}

.player-speed-step:hover {
  background: var(--bg);
}

.player-speed-icon {
  inline-size: 1em;
  block-size: 1em;
  flex: none;
}

.player-speed-value {
  min-inline-size: 3.5ch;
  text-align: center;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-size: var(--step--1);
}

/* ---- Download chip (`<details>` in the platforms row) ------------------ */

/* `<details>` wraps a `<summary>` (the chip itself) and the popover list.
   `position: relative` anchors the absolutely-positioned list to the chip
   so it doesn't drift if other platforms wrap. The wrapper is the popover's
   *stacking context anchor*: the chip lives in the hero `<header>` (static
   page content, z:auto) while the sticky player dock below it is z:25 —
   so without an explicit z on the wrapper, the popover panel extending
   downward into the dock's vertical territory paints under the dock. Pin
   it to `--z-popover` (above the dock, below the banner). */
.platform-download-wrap {
  position: relative;
  z-index: var(--z-popover);
}

/* Strip the native `<details>` disclosure marker — the chip carries its own
   caret (`.platform-download-arrow`). */
.platform-download-wrap > summary {
  list-style: none;
  cursor: pointer;
}
.platform-download-wrap > summary::-webkit-details-marker {
  display: none;
}

/* The caret rotates 180° when open — matches the `<details>` toggle state. */
.platform-download-arrow {
  display: inline-flex;
  align-items: center;
  margin-inline-start: var(--space-3xs);
}
.platform-download-arrow svg {
  inline-size: 0.7em;
  block-size: 0.5em;
}
.platform-download-wrap[open] .platform-download-arrow {
  transform: rotate(180deg);
}
/* Animate the rotation only when the user is OK with motion. Reduced-motion
   users still get the static rotated-when-open state above — they see the
   correct caret orientation, just without the smooth tween. */
@media (prefers-reduced-motion: no-preference) {
  .platform-download-arrow {
    transition: transform var(--dur-quick) var(--ease-out);
  }
}

/* The popover panel: absolutely positioned beneath the chip. Closed by
   default (the `<details>` element hides it without `[open]`). Sized to
   the menu's contents (~14rem) rather than a fixed wide minimum, so it
   reads as a compact menu rather than a giant overlay competing with
   the page. */
.platform-download-list {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: calc(100% + var(--space-3xs));
  /* `.platform-download-wrap` is the stacking-context anchor at
     `--z-popover`; this inner z just orders the panel above sibling
     content INSIDE the wrap (the summary chip, the rotating arrow). */
  z-index: 1;
  min-inline-size: 14rem;
  margin: 0;
  /* Hug the items — the row padding does the breathing. */
  padding: var(--space-3xs);
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* A small drop-shadow so the panel reads as floating over the page. */
  box-shadow: 0 6px 18px -8px rgba(0, 0, 0, 0.25);
  /* One notch below the body font — menus read as secondary chrome. */
  font-size: var(--step--1);
}

/* Each row is a real `<a download>` — full-width tap target. A grid keeps
   the bitrate / size / tag columns aligned across rows so the menu reads
   as a tidy list, not a ragged collection of pills. */
.download-item {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--space-2xs);
  /* No `min-block-size: var(--min-tap)` — the row already clears the
     32 px desktop minimum via padding, and on touch the popover sits
     directly under the chip (which IS at min-tap), so finger reach is
     fine. Forcing 44 px per row made the menu feel oversized. */
  padding-block: 0.4em;
  padding-inline: var(--space-2xs);
  color: var(--text);
  text-decoration: none;
  border-radius: calc(var(--radius) - 2px);
}

/* A small download-arrow glyph anchors each row's leading edge — it
   replaces the "wall of text" with a visible affordance saying "this
   is an actionable download row, not a label". `currentColor` masked
   from an inline SVG so it inherits hover/focus color shifts. */
.download-item::before {
  content: "";
  inline-size: 0.85em;
  block-size: 0.95em;
  background: currentColor;
  opacity: 0.55;
  mask:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M5 20h14v-2H5v2zm7-18v12.17l-3.59-3.58L7 12l5 5 5-5-1.41-1.41L12 14.17V2z'/></svg>")
    no-repeat center / contain;
}
@media (prefers-reduced-motion: no-preference) {
  .download-item::before {
    transition: opacity var(--dur-quick) var(--ease-out);
  }
}

.download-item:hover,
.download-item:focus-visible {
  background: var(--bg);
  text-decoration: none;
}
/* The global focus ring (`outline: 3px solid; outline-offset: 2px`) bleeds
   past the popover's 1 px border on tight menu rows. Pull the ring INSIDE
   the row so it stays inside the popover, still visible, and aligned with
   the row's rounded corners. */
.download-item:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}
.download-item:hover::before,
.download-item:focus-visible::before {
  opacity: 1;
}

.download-bitrate {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.download-size {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.download-tag {
  padding: 0.05em 0.45em;
  font-size: 0.78em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--on-accent);
  background: var(--muted);
  border-radius: 999px;
}

.download-tag-default {
  background: var(--accent);
}

.transcript {
  margin-block-start: var(--space-l);
}

/* Untimed (plain-text) transcript: a comfortable reading pane, no cue chrome. */
.transcript-plain p {
  margin-block-end: var(--space-s);
}

/* Detected timestamps in show notes (chapter markers): seek the player. Reset
   the global button chrome so they read as inline links. */
.ts {
  min-block-size: 0;
  margin: 0;
  padding: 0;
  font: inherit;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  background: none;
  border: 0;
  border-radius: 0;
  cursor: pointer;
}

.ts:hover {
  text-decoration: underline;
}

/* Each synced transcript fragment has a compact metadata strip above the
   spoken text. That keeps narrow screens from burning the first text row on a
   timestamp gutter, while wide screens still get speaker-left / actions-right
   scanning. */
.cue-row {
  margin-block-end: var(--space-s);
}

.cue-row-turn {
  margin-block-start: var(--space-l);
  padding-block-start: var(--space-m);
  border-block-start: 1px solid var(--border);
}

.cue-row-turn:first-child {
  margin-block-start: 0;
  padding-block-start: 0;
  border-block-start: 0;
}

.cue-nav {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-2xs);
  margin-block-end: var(--space-3xs);
}

.cue-speaker-bar {
  min-inline-size: 0;
}

.cue-speaker-bar:empty {
  display: none;
}

.cue-speaker {
  display: inline-flex;
  align-items: center;
  min-block-size: 2rem;
  max-inline-size: 100%;
  padding: 0 var(--space-xs);
  overflow-wrap: anywhere;
  font-size: var(--step--1);
  font-weight: 800;
  color: var(--spk, var(--accent));
  background: var(--surface);
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--spk, var(--accent));
  border-radius: var(--radius);
}

.cue-actions {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  min-block-size: 2rem;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.cue-action-sep {
  color: var(--border);
}

.cue-time {
  min-block-size: 2rem;
  padding: 0 var(--space-xs);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}

.cue-time:hover,
.cue-time:focus-visible {
  color: var(--accent);
  background: var(--bg);
}

@media (max-width: 42rem) {
  .cue-nav {
    grid-template-columns: 1fr;
  }

  .cue-actions {
    justify-self: stretch;
    inline-size: 100%;
  }

  .cue-time {
    flex: 1 1 auto;
    text-align: start;
  }
}

/* Synced transcript cue — a real <button>, so reset the control chrome. A
   full-width spoken-text block. The timestamp lives in `.cue-nav` above so the
   text gets the whole row on phones. Still guarantees the WCAG 2.5.8 tap
   target. */
.cue {
  display: block;
  inline-size: 100%;
  min-block-size: var(--min-tap);
  padding: var(--space-2xs) var(--space-xs);
  font: inherit;
  line-height: 1.65;
  text-align: start;
  color: inherit;
  background: none;
  border: 0;
  /* A left rule in the speaker's colour (a non-hue positional cue for
     colour-blind readers); transparent when the cue has no speaker. */
  border-inline-start: 3px solid var(--spk, transparent);
  border-radius: var(--radius);
  cursor: pointer;
}

.cue-text {
  display: block;
  inline-size: 100%;
}

/* Active cue (driven by the `$t` signal). Soft accent tint, with a flat
   fallback for engines without `color-mix`. The tinted background eats some
   of the timestamp's contrast against `--muted`, so lift it to full text
   colour while active — keeps the active line legible (and clears WCAG AA
   contrast against the tinted bg). */
.cue.active {
  background: var(--border);
}

@supports (background: color-mix(in oklab, red, blue)) {
  .cue.active {
    background: color-mix(in oklab, var(--spk, var(--accent)) 16%, transparent);
  }
}

/* Transcript lines are search-result deep-link targets (`#cue-N`). Clear the
   stacked sticky banners when jumping, and mark the landed-on line so the eye
   finds it. */
.transcript-plain p,
.cue {
  scroll-margin-block-start: var(--stack-h, 5rem);
}

.transcript-plain p:target,
.cue:target {
  background: var(--border);
  border-radius: var(--radius);
}

@supports (background: color-mix(in oklab, red, blue)) {
  .transcript-plain p:target,
  .cue:target {
    background: color-mix(in oklab, var(--accent) 16%, transparent);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .transcript-plain p:target,
  .cue:target {
    /* Deliberately above the 100–500ms micro-interaction range: this is a
       "you are here" spotlight after a scroll-jump (from a search result or
       a `#cue-N` deep link), not UI feedback. The viewport has just moved,
       so the highlight needs to linger long enough for the eye to land on
       the cue and register it before it fades. 1.2s is the sweet spot —
       shorter reads as a flicker the user misses mid-scroll. */
    animation: cue-flash 1.2s var(--ease-out);
  }
}

@keyframes cue-flash {
  from {
    background: color-mix(in oklab, var(--accent) 40%, transparent);
  }
}

/* ---- Share affordances + dialog ---------------------------------------- */

/* The platform-row Share button — same pill chrome as the listen-on buttons
   (`.platform`), and the SVG icon takes the text colour so it sits next to
   Apple / Spotify / YouTube without standing out as a brand. The other
   icons set `fill: <brand>` directly; we use the foreground text colour so
   the icon flips cleanly between light + dark. */
.platform-share {
  /* Override the global accent <button> chrome — Share is a secondary button
     in the platform row, not a primary CTA. */
  color: var(--text);
  background: var(--surface);
  font-weight: 600;
}

.platform-share .platform-icon,
.platform-download .platform-icon {
  /* SVG `<path>`'s default fill is black, not currentColor — so set it
     explicitly to the chip's text colour. Otherwise the icon vanishes
     in dark mode (`<path>` paints black against a dark surface). */
  fill: var(--text);
}

.platform-share:hover,
.platform-share:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
}

.platform-share:hover .platform-icon,
.platform-share:focus-visible .platform-icon,
.platform-download-wrap:hover .platform-download .platform-icon,
.platform-download-wrap[open] .platform-download .platform-icon {
  fill: var(--accent);
}

/* Per-cue copy-link: a small chain-icon button in the cue action bar, kept
   low-key (muted) so the transcript still reads as text. Revealed on hover,
   focus, or when its cue row is focused/hovered. Opens the same share dialog
   as the platform-row Share button. */
.cue-share {
  min-block-size: 2rem;
  min-inline-size: 2rem;
  padding: 0 var(--space-2xs);
  font: inherit;
  font-size: var(--step--1);
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  opacity: 0.35;
}

@media (hover: hover) {
  .cue-share {
    opacity: 0;
  }
}

.cue-share:hover,
.cue-share:focus-visible,
.cue-row:hover .cue-share,
.cue-row:focus-within .cue-share {
  opacity: 1;
  color: var(--accent);
}

.cue-share > span {
  display: inline-block;
}

/* The share dialog. Centred via native `<dialog>` defaults; a small focused
   panel with the URL textbox + timestamp toggle + copy action. The dialog is
   hidden until JS opens it (no `open` attribute) — so it's a graceful no-op
   without script. */
.share-dialog {
  inline-size: min(28rem, calc(100vw - 2 * var(--space-m)));
  max-block-size: calc(100vh - 2 * var(--space-l));
  padding: 0;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}

.share-dialog::backdrop {
  background: rgb(0 0 0 / 50%);
}

.share-dialog-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  padding: var(--space-m);
  /* Default form margins/padding would fight the dialog layout — reset. */
  margin: 0;
}

.share-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
}

.share-dialog-head h2 {
  margin: 0;
  font-size: var(--step-1);
}

.share-close {
  /* Reset the global accent <button> chrome — close is an icon-only ghost. */
  min-block-size: var(--min-tap);
  min-inline-size: var(--min-tap);
  padding: 0;
  font-size: var(--step-1);
  color: var(--muted);
  background: none;
  border: 0;
  cursor: pointer;
}

.share-close:hover,
.share-close:focus-visible {
  color: var(--accent);
}

.share-dialog-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

.share-dialog-label {
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--muted);
}

.share-dialog-url {
  inline-size: 100%;
  padding: var(--space-2xs) var(--space-xs);
  font: inherit;
  font-size: var(--step--1);
  font-family:
    ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
    monospace;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.share-dialog-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  font-size: var(--step--1);
  cursor: pointer;
}

.share-dialog-toggle input[type="checkbox"] {
  inline-size: 1.1em;
  block-size: 1.1em;
  /* Tint to the show's accent on supporting browsers. */
  accent-color: var(--accent);
}

.share-dialog-actions {
  display: flex;
  justify-content: flex-end;
}

.share-dialog-copy {
  /* Primary action — inherits the global button accent styling, just sized
     to match the rest of the dialog. */
  min-inline-size: 7rem;
}

/* Three static labels (Copy link / Copied / Copy failed) on the copy button.
   episode-share.js toggles `is-copied` or `is-failed` for ~1.5s. */
.share-dialog-copy .share-status-ok,
.share-dialog-copy .share-status-err {
  display: none;
}

.share-dialog-copy.is-copied .share-label,
.share-dialog-copy.is-failed .share-label {
  display: none;
}

.share-dialog-copy.is-copied .share-status-ok,
.share-dialog-copy.is-failed .share-status-err {
  display: inline;
}

.share-dialog-copy.is-failed {
  color: var(--bg);
  background: hsl(0 70% 55%);
}

/* Speaker turns in plain transcripts (`Name:`-prefixed lines): show the speaker
   when it changes, with extra space before each new turn, so the back-and-forth
   reads clearly. A left rule in the speaker's colour groups each turn. */
.transcript-plain .turn-start {
  margin-block-start: var(--space-m);
}
.transcript-plain .turn {
  padding-inline-start: var(--space-s);
  border-inline-start: 3px solid var(--spk, transparent);
}

.transcript-plain .speaker {
  display: block;
  margin-block-end: var(--space-3xs);
  font-weight: 700;
  color: var(--spk, var(--accent));
}

/* Per-speaker palette. Colour is a *secondary* cue — the speaker NAME label
   (always shown when the speaker changes) is the primary, colour-blind-safe
   distinguisher, reinforced by a left rule. Hues are an AA-contrast,
   colour-blind-aware set (à la Okabe–Ito), tuned per scheme, and deliberately
   independent of the show's accent theme so they stay mutually distinct.
   `--spk` is the active slot; rules above fall back to `--accent` without it. */
.speaker-0 {
  --spk: var(--spk-0);
}
.speaker-1 {
  --spk: var(--spk-1);
}
.speaker-2 {
  --spk: var(--spk-2);
}
.speaker-3 {
  --spk: var(--spk-3);
}
.speaker-4 {
  --spk: var(--spk-4);
}
.speaker-5 {
  --spk: var(--spk-5);
}

/* ---- Floating "On this page" section nav -------------------------------- */

/* A native <details> disclosure pinned to the bottom-inline-end corner: a
   compact pill that expands a jump list of the page's sections. Fixed so it
   floats over the reading column at any width; `column-reverse` opens the panel
   upward (away from the screen edge). Zero JS — the browser owns the toggle,
   focus, and keyboard. It's rendered only in the reading view, so it's absent
   while episode search results are shown (their anchors don't exist). */
.section-nav {
  position: fixed;
  /* Top of the `--z-*` ladder: the pill stays tappable over the sticky player
     dock, and the panel — which opens UPWARD into the dock's territory — is
     never painted over by it. Dismisses on outside-click / Escape so it never
     lingers (episode-dock.js). */
  z-index: var(--z-side-nav);
  inset-block-end: var(--space-s);
  inset-inline-end: var(--space-s);
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-end;
  gap: var(--space-2xs);
  font-size: var(--step--1);
}

/* The toggle pill. */
.section-nav > summary {
  /* Drop the default disclosure triangle (both rendering engines). */
  list-style: none;
  display: inline-flex;
  align-items: center;
  min-block-size: var(--min-tap);
  padding-block: var(--space-2xs);
  padding-inline: var(--space-s);
  font-weight: 600;
  color: var(--on-accent);
  background: var(--accent);
  border-radius: 999px;
  box-shadow: var(--shadow-soft);
  cursor: pointer;
  user-select: none;
}

.section-nav > summary::-webkit-details-marker {
  display: none;
}

/* A borderless list glyph so the pill reads as a menu without shipping an icon
   asset; decorative (the label carries the meaning for assistive tech). */
.section-nav-label::before {
  content: "≡";
  margin-inline-end: var(--space-3xs);
  font-weight: 700;
}

/* The expanded panel: a defined, responsive width so long headings wrap, capped
   in height with its own scroll so a deep outline never runs off-screen. */
.section-nav-body {
  inline-size: min(18rem, calc(100vw - 2 * var(--space-s)));
  max-block-size: min(60vh, 28rem);
  overflow-y: auto;
  padding: var(--space-s);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}

.section-nav-body ul {
  /* The list has `role="list"` (no class), so the global `ul[class]` reset
     doesn't reach it — strip markers + indent here. Hierarchy is conveyed by
     the per-level indentation + muted sub-items, not bullets. */
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

.section-nav-body a {
  display: flex;
  align-items: center;
  min-block-size: var(--min-tap);
  color: var(--text);
  text-decoration: none;
}

.section-nav-body a:hover,
.section-nav-body a:focus-visible {
  color: var(--accent);
  text-decoration: underline;
}

/* Show-notes sub-headings read as muted, progressively-indented children of the
   "Show notes" entry, so the outline's shape is legible at a glance. */
.section-nav-body .nav-sub a {
  color: var(--muted);
}

.section-nav-body .nav-d0 a {
  padding-inline-start: var(--space-s);
}

.section-nav-body .nav-d1 a {
  padding-inline-start: var(--space-m);
}

.section-nav-body .nav-d2 a {
  padding-inline-start: var(--space-l);
}

.section-nav-body .nav-d3 a {
  padding-inline-start: var(--space-xl);
}

/* Narrow screens use the <summary> pill as the label; the in-panel heading is
   only for the wide-screen sidebar (where the summary is hidden). */
.section-nav-heading {
  display: none;
}

/* Wide screens have room for a permanent sidebar, so the tap-to-open widget made
   for narrow screens makes no sense: drop the pill and pin the panel open in the
   right gutter, beside the centred reading column. `::details-content` keeps the
   panel shown regardless of the (now-hidden) toggle's open state.

   The reading column is pinned to a rem width here (it's already ~52rem at this
   font) so the gutter math is exact in rem — `--measure`'s `ch` unit resolves
   against each element's own font, which would mis-size the panel. The panel is
   flush-right and sized to the *remaining* gutter (`50vw − half the column − the
   gaps`), so it can never overlap the prose: a narrower viewport just yields a
   narrower panel, capped at 18rem. */
@media (min-width: 90rem) {
  .episode-detail {
    max-inline-size: 52rem;
  }

  .section-nav {
    inset-block-start: calc(var(--banner-h, 3.5rem) + var(--space-m));
    inset-block-end: var(--space-m);
    inset-inline-end: var(--space-m);
    inset-inline-start: auto;
    flex-direction: column;
    align-items: flex-end;
  }

  .section-nav > summary {
    display: none;
  }

  .section-nav::details-content {
    content-visibility: visible;
    block-size: auto;
  }

  .section-nav-body {
    inline-size: min(18rem, calc(50vw - 26rem - var(--space-l) - var(--space-m)));
    /* Fill the gutter height; a long outline scrolls within it. */
    max-block-size: 100%;
    box-shadow: var(--shadow-soft);
  }

  .section-nav-heading {
    display: block;
    margin: 0 0 var(--space-2xs);
    font-size: var(--step--1);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
  }
}

/* ---- Forms (search) ----------------------------------------------------- */

form[role="search"] {
  display: flex;
  flex-wrap: wrap;
  /* stretch so the input + button are always the exact same height */
  align-items: stretch;
  gap: var(--space-2xs);
  margin-block: var(--space-m);
}

form[role="search"] label {
  flex-basis: 100%;
  font-weight: 600;
}

input[type="search"] {
  flex: 1 1 16rem;
  min-block-size: var(--min-tap);
  padding: var(--space-2xs) var(--space-xs);
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

button {
  min-block-size: var(--min-tap);
  padding-inline: var(--space-s);
  font: inherit;
  font-weight: 600;
  color: var(--on-accent);
  background: var(--accent);
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}

/* ---- Motion (opt-in only) ---------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  a,
  button,
  .site-header a {
    transition:
      color var(--dur-quick) var(--ease-out),
      background-color var(--dur-quick) var(--ease-out);
  }
}
