/* AudioPress — episode-page chrome.
 *
 * Split out of app.css so the pages that can never render a player (the show
 * home, the glossary, search results, the 404) don't parse it: it is ~39 KB of
 * the ~116 KB stylesheet, all of it render-blocking. Linked only when
 * `layout::PageMeta::episode_chrome` is set.
 *
 * Everything here is scoped to episode-page markup — the player + its extra
 * controls, the sticky dock, the per-variant download chip, the share
 * affordances, the floating section nav, and the transcript. That exclusivity is
 * load-bearing, and `service::tests::public_assets` guards it: a selector in
 * here that also matches home/glossary/404 markup would silently lose its styles
 * on those pages. Loaded *after* app.css; no class in this file is styled by
 * app.css's remaining tail, so the cascade is unchanged by the split.
 */

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

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

.episode-title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs) var(--space-m);
  margin-block-end: var(--space-2xs);
}

.episode-title-row h1 {
  flex: 1 1 24rem;
  min-inline-size: 0;
  margin: 0;
  overflow-wrap: anywhere;
}

@media (max-width: 42rem) {
  .episode-header h1 {
    font-size: var(--step-2);
    line-height: 1.12;
  }
}

.episode-header .episode-meta {
  margin: 0;
}

/* In the hero the meta is *info presented on top*, not the sticky nav, so it is
   never end-aligned: it drops onto its own line under the title and reads from
   the inline-start (left) edge. */
.episode-title-row .episode-meta-row {
  flex: 1 1 100%;
  max-inline-size: 100%;
  margin-inline-start: 0;
  text-align: start;
}

.episode-meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-2xs);
  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;
  /* Overlap the banner's measured edge by one CSS pixel so device-pixel
     rounding cannot expose a fractional seam once the dock becomes sticky. */
  inset-block-start: calc(var(--banner-h, 3.5rem) - 1px);
  inline-size: var(--episode-chrome-inline);
  max-inline-size: var(--episode-chrome-inline);
  margin-inline: calc((100% - var(--episode-chrome-inline)) / 2);
  /* Sits below the site banner (`--z-header`) and content popovers, but above
     ordinary page content. The dock forms its own stacking context; when its
     section nav opens we raise this ancestor as a whole below, because a
     descendant's larger z-index cannot escape its parent's stacking context. */
  z-index: var(--z-dock);
  /* The controls are content-sized, but the sticky band must still paint
     full-bleed so scrolled content can't peek between the banner and player. */
  background: transparent;
  isolation: isolate;
  padding-block: var(--space-m);
}

.episode-dock::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset-block: -1px 0;
  inset-inline-start: 50%;
  inline-size: 100vw;
  transform: translateX(-50%);
  background: var(--bg);
}

.episode-dock audio {
  margin-block: 0;
}

/* When pinned, the dock is a compact stacked banner: the dock owns spacing
   around the native player so sticky geometry stays exact. */
.episode-detail:is(.is-docked, :has(#content > .search-results)) .episode-dock {
  padding-block: var(--space-2xs);
}

/* Docked, the dock is the *second* sticky bar directly under the site banner, so
   it wears the banner's surface + divider rather than the page background. That
   removes the dark `--bg` band that otherwise read as an empty gap between the
   main nav and this secondary nav — the two now stack as one cohesive header. */
.episode-detail:is(.is-docked, :has(#content > .search-results)) .episode-dock::before {
  background: var(--surface);
  border-block-end: 1px solid var(--border);
}

.episode-detail:is(.is-docked, :has(#content > .search-results)) .episode-dock audio {
  margin-block: 0;
}

/* 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(.is-docked, :has(#content > .search-results)) .dock-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3xs) var(--space-s);
  padding-block: var(--space-xs);
}

.dock-title {
  flex: 1 1 24rem;
  min-inline-size: 0;
  max-inline-size: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  font-size: var(--step--1);
  font-weight: 700;
  /* Keep the bar to one tidy line without hiding the title. Long titles can be
     panned manually; scrollbars are hidden so the sticky dock stays clean. */
  scrollbar-width: none;
  white-space: nowrap;
}

.dock-title::-webkit-scrollbar {
  display: none;
}

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

/* Right-align the meta only while it shares the title's line: the title
   (`flex: 1 1 24rem`) grows to absorb the free space, pushing the meta to the
   inline-end edge. When the line is too narrow the meta wraps onto its own line,
   where — with no auto margin — it falls back to the inline-start (left) edge.
   So the meta is end-aligned in the sticky dock only when there's room, and
   start-aligned once it stands alone. */

@media (max-width: 42rem) {
  .dock-title {
    font-size: var(--step--2);
  }
}

/* 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(.is-docked, :has(#content > .search-results)) .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. Playback controls live in
   one transport group; secondary actions live in their own group and move into
   the floating page menu on narrow screens. When the dock is pinned (the
   `is-docked` body state), this row tightens so the pinned bar stays slim. */
.player-controls,
.player-control-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}

.player-controls {
  gap: var(--space-2xs) var(--space-s);
  padding-block-end: var(--space-2xs);
  inline-size: 100%;
}

.player-control-group {
  gap: var(--space-2xs);
}

.episode-detail:is(.is-docked, :has(#content > .search-results)) .player-controls {
  padding-block: 0 var(--space-3xs);
  gap: var(--space-3xs) var(--space-xs);
}

.episode-detail:is(.is-docked, :has(#content > .search-results)) .player-control-group {
  gap: var(--space-3xs);
}

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

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

.player-share,
.player-follow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  min-block-size: var(--min-tap);
  padding: var(--space-3xs) var(--space-xs);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: var(--step--1);
  font-weight: 600;
}

.player-share {
  cursor: pointer;
}

.player-share:hover,
.player-share:focus-visible,
.player-follow:focus-within {
  border-color: var(--muted);
  background: var(--bg);
}

.player-share-icon {
  inline-size: 1.15em;
  block-size: 1.15em;
  flex: none;
  fill: currentColor;
}

.player-follow {
  cursor: pointer;
}

.player-follow input {
  flex: none;
  margin: 0;
}

.player-follow span {
  overflow-wrap: anywhere;
}

@media (max-width: 89.999rem) {
  .episode-dock {
    padding-block: var(--space-s) var(--space-2xs);
  }

  .player-controls {
    display: grid;
    grid-template-columns: auto auto auto;
    gap: 0;
    align-items: stretch;
    justify-content: center;
    max-inline-size: 100%;
    padding-block-end: var(--space-3xs);
  }

  .episode-detail:is(.is-docked, :has(#content > .search-results)) .player-controls {
    gap: 0;
  }

  .player-transport,
  .player-actions {
    flex-wrap: nowrap;
    justify-content: space-evenly;
    max-inline-size: 100%;
    min-inline-size: 0;
    padding: 0.1rem;
    gap: 0.12rem;
    background: color-mix(in oklab, var(--surface) 76%, var(--bg));
    border: 1px solid color-mix(in oklab, var(--border) 72%, transparent);
    border-radius: calc(var(--radius) + 4px) 0 0 calc(var(--radius) + 4px);
  }

  .player-actions {
    border-inline-start: 0;
    border-radius: 0 calc(var(--radius) + 4px) calc(var(--radius) + 4px) 0;
  }

  .player-skip,
  .player-speed,
  .player-share,
  .player-follow {
    min-block-size: 1.9rem;
    font-size: var(--step--2);
    background: transparent;
    border-color: transparent;
  }

  .player-skip,
  .player-share,
  .player-follow {
    gap: 0.18rem;
    padding-inline: 0.4rem;
  }

  .player-skip:hover,
  .player-speed-step:hover,
  .player-share:hover,
  .player-share:focus-visible,
  .player-follow:focus-within {
    background: color-mix(in oklab, var(--text) 8%, transparent);
  }

  .player-speed {
    flex: 0 1 auto;
    justify-content: center;
    max-inline-size: 7.5rem;
    padding: 0;
    border-radius: 0;
    border-inline-color: color-mix(in oklab, var(--border) 72%, transparent);
  }

  .player-speed-step {
    min-inline-size: 1.9rem;
    min-block-size: 1.9rem;
  }

  .player-speed-value {
    min-inline-size: 3ch;
    font-size: var(--step--2);
  }

  .player-skip-icon {
    inline-size: 1.2em;
    block-size: 1.2em;
  }

  .player-speed-icon {
    inline-size: 0.9em;
    block-size: 0.9em;
  }

  .player-controls > .section-nav {
    position: relative;
    z-index: auto;
    inset: auto;
    display: block;
    inline-size: auto;
    max-inline-size: none;
    margin: 0;
    font-size: var(--step--2);
  }

  .player-controls > .section-nav > summary {
    block-size: 100%;
    min-block-size: 0;
    padding-block: 0;
    padding-inline: 0.8rem;
    min-inline-size: 3.25rem;
    justify-content: center;
    font-size: var(--step--2);
    color: var(--text);
    background: color-mix(in oklab, var(--surface) 76%, var(--bg));
    border-block: 1px solid color-mix(in oklab, var(--border) 72%, transparent);
    border-inline: 0;
    border-radius: 0;
    box-shadow: none;
    white-space: nowrap;
  }

  .player-controls > .section-nav > summary:hover,
  .player-controls > .section-nav > summary:focus-visible {
    background: color-mix(in oklab, var(--text) 8%, transparent);
  }

  .player-controls > .section-nav[open] {
    z-index: var(--z-side-nav);
  }

  .player-controls-nav-only {
    grid-template-columns: auto;
    justify-content: end;
  }
}

@media (min-width: 42.001rem) and (max-width: 89.999rem) {
  .player-controls > .section-nav .section-nav-label {
    font-size: 0;
  }

  .player-controls > .section-nav .section-nav-label::before {
    margin-inline-end: 0;
    font-size: var(--step-0);
  }
}

@media (max-width: 42rem) {
  body:has(.section-nav) .site-banner .theme-toggle,
  .player-actions {
    display: none;
  }

  .player-controls {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: stretch;
    justify-content: stretch;
  }

  .player-transport {
    inline-size: 100%;
  }

  .player-controls > .section-nav {
    position: static;
  }

  .player-controls > .section-nav > summary {
    color: var(--on-accent);
    background: var(--accent);
    border: 1px solid color-mix(in oklab, var(--accent) 72%, var(--border));
    border-radius: 0 calc(var(--radius) + 4px) calc(var(--radius) + 4px) 0;
  }

  .player-controls > .section-nav > summary:hover,
  .player-controls > .section-nav > summary:focus-visible {
    filter: brightness(1.04);
  }
}

@media (max-width: 30rem) {
  .player-controls > .section-nav > summary {
    padding-inline: 0.75rem;
  }

  .player-controls > .section-nav .section-nav-label {
    font-size: 0;
  }

  .player-controls > .section-nav .section-nav-label::before {
    margin-inline-end: 0;
    font-size: var(--step-0);
  }
}

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

.platform-download-wrap[open] {
  z-index: var(--z-side-nav);
}

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

/* The loading card must never wrap markup that also exists in the loaded state.
   `#transcript` is rendered by both, so padding/border here would inset the
   heading while loading and let it snap back when the transcript arrives — a
   visible jump for the reader, and the episode page's entire CLS (0.057 of
   0.058, the only metric that was not at ceiling). `.transcript-loading` is
   therefore a pure state hook, which is also how `episode-player.js` and
   `episode-cue-sync.js` use it; the card decorates the transient content only. */
.transcript-loading-card {
  padding: var(--space-m);
  background:
    linear-gradient(90deg, color-mix(in oklab, var(--accent) 16%, transparent), transparent 55%),
    var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.transcript-loading-message {
  margin-block: var(--space-xs) 0;
  color: var(--muted);
}

.transcript-loading-fallback {
  margin-block: var(--space-2xs) 0;
  font-size: var(--step--1);
}

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

/* A speaker chip that leads somewhere presses like a control, not like prose.
   An underline across a bordered box reads as two affordances for one thing,
   and the box already says "this is a unit" — so the chip itself takes the
   hover tint and the active nudge, and the text keeps its own colour. */
a.cue-speaker {
  text-decoration: none;
  cursor: pointer;
}

a.cue-speaker:hover,
a.cue-speaker:focus-visible {
  color: var(--spk, var(--accent));
  background: color-mix(in oklab, var(--spk, var(--accent)) 14%, var(--surface));
  border-color: var(--spk, var(--accent));
  text-decoration: none;
}

a.cue-speaker:active {
  background: color-mix(in oklab, var(--spk, var(--accent)) 22%, var(--surface));
}

/* In the plain (untimed) reading pane the speaker is a block label above the
   turn rather than a chip in a rail, so it gets the same press feedback
   without growing a box that would break the reading rhythm. */
a.speaker {
  text-decoration: none;
  cursor: pointer;
}

a.speaker:hover,
a.speaker:focus-visible {
  color: color-mix(in oklab, var(--spk, var(--accent)) 78%, var(--text));
  text-decoration: none;
}

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

  a.speaker {
    transition: color var(--dur-quick) var(--ease-out);
  }
}

.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-inline-size: 44px;
  min-block-size: 44px;
  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 text. The adjacent timestamp is the seek button; cue
   text remains non-interactive so it can contain real glossary links without
   nesting controls. */
.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: text;
}

.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.

   `:target` alone is not enough once JS is in play: the enhanced path streams
   the transcript in, so the cue the fragment names is created *after* the
   fragment was resolved, and is re-created whenever the window is re-fetched.
   A browser drops its target element when that happens and does not pick the
   new one up without another fragment navigation. `episode-player.js` marks
   the cue it jumped to with `data-cue-target`, so the highlight is the same
   either way — `:target` carries it with JS off, the attribute with JS on. */
.transcript-plain p,
.cue {
  scroll-margin-block-start: var(--stack-h, 5rem);
}

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

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

@media (prefers-reduced-motion: no-preference) {
  .transcript-plain p:target,
  .transcript-plain p[data-cue-target],
  .cue:target,
  .cue[data-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 ---------------------------------------- */

.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-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 parked at the bottom-inline-end of the reading
   flow: a compact pill that expands a jump list of the page's sections. Sticky
   keeps it reachable while reading, but because it remains in normal flow it
   stops before the footer instead of covering it. `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: sticky;
  /* 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);
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-end;
  gap: var(--space-2xs);
  inline-size: max-content;
  max-inline-size: calc(100vw - 2 * var(--space-s));
  /* Same zero-height flow trick as `.mobile-actions`: the pill can drop into
     the page's bottom padding while still stopping before the footer. */
  margin-block-start: var(--space-2xs);
  margin-block-end: calc(-1 * (var(--min-tap) + var(--space-2xs)));
  margin-inline-start: auto;
  margin-inline-end: calc(var(--space-s) - (100vw - 100%) / 2);
  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-actions {
  display: none;
}

.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;
}

@media (max-width: 42rem) {
  .section-nav-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2xs);
    padding-block-end: var(--space-s);
    margin-block-end: var(--space-s);
    border-block-end: 1px solid var(--border);
  }

  .section-nav-actions .theme-toggle {
    background: var(--bg);
    border-color: var(--border);
  }

  .section-nav-actions .player-share,
  .section-nav-actions .player-follow {
    min-inline-size: 0;
    padding-inline: var(--space-xs);
  }
}

/* When open, the floating menu should be anchored to the viewport, not to the
   zero-height sticky flow box below the episode content. Keep the closed state
   sticky so the pill still stops before the footer, but make the active popup
   reliably fit on screen even when opened near the top of the page. */
@media (max-width: 89.999rem) {
  .episode-dock:has(.section-nav[open]) {
    z-index: var(--z-side-nav);
  }

  .player-controls > .section-nav:not([open]) {
    position: relative;
    z-index: auto;
    inline-size: auto;
    max-inline-size: none;
    margin: 0;
  }

  .player-controls > .section-nav[open] {
    position: relative;
    z-index: var(--z-side-nav);
    inline-size: auto;
    max-inline-size: none;
    margin: 0;
  }

  .section-nav[open] {
    margin: 0;
  }
}

@media (max-width: 42rem) {
  .player-controls > .section-nav:not([open]),
  .player-controls > .section-nav[open] {
    position: static;
  }

  .player-controls > .section-nav[open] > .section-nav-body {
    position: fixed;
    z-index: var(--z-side-nav);
    inset-block-end: max(var(--space-s), env(safe-area-inset-bottom));
    inset-inline-end: max(var(--space-s), env(safe-area-inset-right));
  }
}

@media (min-width: 42.001rem) and (max-width: 89.999rem) {
  .player-controls > .section-nav[open] > .section-nav-body {
    position: absolute;
    z-index: var(--z-side-nav);
    inset-block-start: calc(100% + var(--space-s));
    inset-inline-end: 0;
  }
}

/* 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. The details stays closed in
   the markup so narrow screens still start collapsed; on wide screens this
   direct-child rule intentionally beats the UA details-closed display rule.

   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 {
    position: fixed;
    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;
    margin: 0;
  }

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

  .section-nav:not([open]) > .section-nav-body {
    display: block;
  }

  .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);
  }
}

/* ---- The episode meta band --------------------------------------------- */
/* Topics and people, between the show notes and the transcript.
   This is metadata about the episode, not the episode, so it is set apart by a
   hairline frame and a faint tint rather than by size or weight: unmistakably
   a different kind of thing from the prose above and the transcript below, and
   skippable in one glance by a reader who does not want it. */

.episode-meta-band {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  margin-block: var(--space-l);
  padding: var(--space-s);
  background: color-mix(in oklab, var(--surface) 55%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* One group per row, each starting with its own label.

   Side by side, the two groups shared a width neither wanted: two topics and
   six people split the band down the middle left one column crowded and the
   other mostly empty, and both were narrower than the page they sit on. Down
   the page each takes the room it actually needs.

   Scoped to the band so it beats the row layout `.episode-topics` carries for
   the compact variant on an episode card. */
.episode-meta-band .episode-meta-group {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs) var(--space-s);
  min-inline-size: 0;
  margin-block: 0;
}

/* Topics are chips a line high, so the label reads as the start of that line
   rather than a heading over it. It keeps its own column while the chips have
   room beside it, and gives way before they have to compress. */
.episode-meta-band .episode-topics--meta {
  flex-wrap: wrap;
}

.episode-meta-band .episode-topics--meta .topic-list {
  flex: 1 1 min(100%, 18rem);
  min-inline-size: 0;
}

/* People are cards with a face, a name and a role: too tall to sit beside a
   caption without leaving a ragged edge, so the label goes above them and the
   cards get the full width. */
.episode-meta-band .episode-people-strip {
  flex-direction: column;
  align-items: start;
  gap: var(--space-2xs);
}

/* ---- People in this episode -------------------------------------------- */

.episode-people-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
  min-inline-size: 0;
}

/* A real card, not a text link. An underline appearing across a name, a role
   and an avatar on hover reads as damage to a composite object; a card can
   simply lift and take the accent, which is what "clickable" looks like for
   something with internal structure.

   The corner matches the topic chip beside it (`0.45rem`) rather than being a
   pill: two shapes in one band read as two kinds of thing, and a pill wastes
   its own width on the round ends. The avatar stays a circle — it is the one
   element that says "this is a person", and a photo will be cropped to one. */
.episode-person-card {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-inline-size: 0;
  /* Cards sit on one row and wrap; the cap keeps a long name from taking the
     whole band while still letting a short one stay short. */
  max-inline-size: 100%;
  padding: var(--space-3xs) var(--space-xs) var(--space-3xs) var(--space-3xs);
  color: inherit;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0.45rem;
  text-decoration: none;
}

.episode-person-card:hover,
.episode-person-card:focus-visible {
  color: var(--text);
  background: color-mix(in oklab, var(--accent) 10%, var(--surface));
  border-color: var(--accent);
  text-decoration: none;
}

.episode-person-card:hover .episode-person-title,
.episode-person-card:focus-visible .episode-person-title {
  color: var(--accent);
}

/* A monogram placeholder until avatars exist, so the row has the same shape
   either way and adding photos later changes nothing structural. */
.episode-person-avatar {
  display: grid;
  place-items: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  flex: none;
  border-radius: 999px;
  background: color-mix(in oklab, var(--accent) 18%, var(--surface));
  color: var(--text);
  font-weight: 700;
  font-size: var(--step--1);
}

/* A real picture fills the same box the monogram holds, so adding one changes
   nothing structural — which is what the placeholder was for. */
.person-avatar-image {
  object-fit: cover;
}

.episode-person-meta {
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
}

.episode-person-title {
  overflow-wrap: anywhere;
  font-weight: 600;
}

.episode-person-role {
  color: var(--muted);
  font-size: var(--step--1);
}

/* On a phone the faces would eat the row before the names do, so the strip
   collapses to names and roles. The markup is unchanged — only the avatar is
   dropped, and the card reclaims the space it left. */
@media (max-width: 34rem) {
  .episode-person-avatar {
    display: none;
  }

  .episode-person-card {
    padding-inline-start: var(--space-xs);
  }

  .episode-people-list {
    gap: var(--space-3xs);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .episode-person-card {
    transition:
      color var(--dur-quick) var(--ease-out),
      background-color var(--dur-quick) var(--ease-out),
      border-color var(--dur-quick) var(--ease-out);
  }

  .episode-person-card .episode-person-title {
    transition: color var(--dur-quick) var(--ease-out);
  }
}
