/* Tenant podcast-site components.
 *
 * Copyright 2026 Plabayo BV.
 *
 * 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:focus-visible {
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.15em;
}
@media (hover: hover) and (pointer: fine) {
  .heading-link:hover {
    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:focus-visible {
  color: var(--accent);
}
@media (hover: hover) and (pointer: fine) {
  .heading-extlink:hover {
    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-head > h2[id] {
  font-size: var(--step-2);
}

/* Don't let the first line of content hug the section header. */
.show-notes > h2,
.transcript-head {
  margin-block-end: var(--space-s);
}

/* The transcript's header row: its heading, and the download menu at the end
   of the line. The menu keeps to the end even when a narrow column wraps it
   onto a line of its own, so its end-anchored panel always opens inward. */
.transcript-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-s);
}

.transcript-head > .transcript-download {
  margin-inline-start: auto;
}

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

/* Show notes are the creator's own prose, sanitized out of the feed, and the
   one region on the site whose content we do not write. A bare URL in it is a
   single unbreakable word: paste
   `https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exemplars`
   into an episode description and it is 450px of one token in a 343px column,
   which pushed the whole document sideways on a phone. Nothing clips it on the
   way out, so the page — not the paragraph — scrolled.

   `anywhere` rather than `break-word`: it also lets the min-content size
   shrink, so the column can narrow past the longest word instead of setting a
   floor every ancestor has to honour. Ordinary prose is unaffected; a break
   only happens where a word cannot fit at all. */
.show-notes {
  overflow-wrap: anywhere;
}

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

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 {
  --episode-chrome-gutter: clamp(var(--space-s), 4vw, var(--space-xl));
  --episode-chrome-max: min(var(--measure-wide), calc(100vw - 2 * var(--episode-chrome-gutter)));
  --episode-chrome-inline: min(var(--episode-chrome-max), clamp(var(--measure), 62vw, 70rem));

  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-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-m);
  max-inline-size: var(--measure-wide);
  margin-inline: auto;
  padding-inline: clamp(var(--space-s), 4vw, var(--space-xl));
}

/* Content-based basis, deliberately not a fixed length. Flex decides wrapping
   from an item's *base* size rather than its rendered width, so the `32rem`
   (512px) basis this used to carry overflowed a phone's ~361px footer row all by
   itself and pushed the Glossary link onto a second, near-empty line — however
   short the copyright was. A white-labelled footer (copyright only, no
   "built with AudioPress" credit) hit that every time: metres of free space, yet
   Glossary alone on its own line, right-aligned by the `margin-inline-start:
   auto` below. With `auto` the row wraps only when the text genuinely does not
   fit, which is still the case for the longer credited footer on a phone.
   `min-inline-size: 0` + `overflow-wrap` keep shrinking safe. */
.site-footer-copy {
  flex: 1 1 auto;
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

.site-footer small {
  display: block;
}

.site-footer-glossary {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--min-tap);
  margin-inline-start: auto;
  padding-block: var(--space-3xs);
  color: var(--muted);
  font-size: var(--step--2);
}

.site-footer-copyright {
  margin-block-end: var(--space-3xs);
}

/* ---- 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: var(--radius-pill);
  cursor: pointer;
}

.lang-banner-dismiss:focus-visible {
  color: var(--accent);
  background: var(--on-accent);
}
@media (hover: hover) and (pointer: fine) {
  .lang-banner-dismiss:hover {
    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 {
  min-inline-size: 0;
  margin: 0;
  font-size: var(--step-0);
  line-height: 1.1;
}

.banner-brand a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-inline-size: 0;
  color: var(--text);
  font-weight: 700;
  text-decoration: none;
}

.banner-cover {
  inline-size: 2rem;
  block-size: 2rem;
  flex: none;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) - 4px);
}

.banner-brand-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (hover: hover) and (pointer: fine) {
  .banner-brand a:hover {
    color: var(--accent);
    text-decoration: none;
  }
}

@media (max-width: 36rem) {
  .banner-cover {
    inline-size: 1.75rem;
    block-size: 1.75rem;
  }

  .banner-brand-title {
    max-inline-size: 34vw;
  }
}

/* Compact, single-row search in the banner (keep the input + button inline). */
.site-banner form[role="search"] {
  flex: 1 1 12rem;
  flex-wrap: nowrap;
  min-inline-size: 0;
  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;
  min-inline-size: 0;
}

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

/* The compact episode/404 banner cannot afford a second visible line, but the
   input still references this description for assistive technology. The show
   home renders the same scope text visibly beneath its wider search row. */
.site-banner form[role="search"] .search-scope {
  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: var(--radius-pill);
  cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
  .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;
}

@media (max-width: 42rem) {
  body:has(.section-nav) .banner-inner {
    align-items: center;
    gap: var(--space-2xs);
    padding-inline: max(var(--space-s), env(safe-area-inset-left))
      max(var(--space-s), env(safe-area-inset-right));
  }

  body:has(.section-nav) .banner-brand {
    flex: 0 1 auto;
    min-inline-size: min-content;
    font-size: var(--step--1);
  }

  body:has(.section-nav) .banner-brand a {
    display: block;
    overflow-wrap: anywhere;
  }

  body:has(.section-nav) .site-banner form[role="search"]:not(:has(select)) {
    flex: 999 1 9rem;
    min-inline-size: 8.75rem;
    max-inline-size: none;
  }
}

@media (max-width: 22rem) {
  body:has(.section-nav) .banner-brand {
    flex-basis: 100%;
  }
}

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

.feed-header {
  min-block-size: min(82svh, 52rem);
  display: grid;
  align-items: center;
  margin-block: clamp(var(--space-xl), 7vw, 6rem) clamp(var(--space-xl), 10vw, 8rem);
}

.feed-meta {
  inline-size: 100%;
}

.feed-title-stack {
  --feed-title-size: clamp(1.71rem, 9.36vw, 7.02rem);
  --feed-tagline-ratio: 0.825;
  position: relative;
  display: grid;
  container-type: inline-size;
  justify-items: center;
  gap: var(--space-2xs);
  text-align: center;
}

.feed-title-stack::after {
  content: "";
  inline-size: min(16rem, 34vw);
  block-size: 1px;
  margin-block-start: var(--space-2xs);
  background: linear-gradient(90deg, transparent, var(--border), transparent);
}

.feed-title {
  margin: 0;
  inline-size: fit-content;
  max-inline-size: 100%;
  /* A show name is identity, not prose: wrap only at its real spaces. The
     banner-metrics helper scales `--feed-title-size` when one non-space run is
     wider than the stack, and the tagline derives from that same value. */
  overflow-wrap: normal;
  font-size: var(--feed-title-size);
  line-height: 0.95;
  text-wrap: balance;
  word-break: normal;
}

.feed-tagline {
  margin: 0;
  color: var(--accent);
  overflow-wrap: break-word;
  font-size: min(calc(var(--feed-title-size) * var(--feed-tagline-ratio)), var(--feed-title-size));
  font-weight: 700;
  line-height: 1.05;
  text-wrap: balance;
  word-break: normal;
}

.content-rating-badge {
  display: inline-flex;
  align-items: center;
  inline-size: max-content;
  padding: 0.32rem 0.72rem;
  color: var(--danger);
  font-size: var(--step--2);
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border));
  border-radius: var(--radius-pill);
}

.feed-title-stack > .content-rating-badge {
  margin-block-start: var(--space-2xs);
}

.feed-platforms {
  display: flex;
  justify-content: center;
  margin-block-start: var(--space-m);
}

.feed-platforms ul {
  justify-content: center;
  align-items: center;
  gap: var(--space-xs);
}

/* The hero "Listen on ▾" menu is the right-hand control of the centered pair, so
   anchor its popover to the trigger's trailing edge (open leftward/inward). A
   left-anchored panel would run off the right viewport edge on phones. (The
   sticky header right-anchors for the same reason; the episode row's menu is the
   leading control, so it keeps the default left anchor.) */
.feed-platforms .platform-menu-list {
  inset-inline: auto 0;
}

/* The RSS feed leads the row as the recommended, open way to follow the show —
   a subtle accent halo sets it apart from the app pills without shouting. Scoped
   to the full-button row so the icon-only sticky-header variant is untouched. */
.feed-platforms .platform-rss {
  font-weight: 650;
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  background: color-mix(in oklab, var(--accent) 10%, var(--surface));
}
@media (hover: hover) and (pointer: fine) {
  .feed-platforms .platform-rss:hover {
    border-color: var(--accent);
    background: color-mix(in oklab, var(--accent) 16%, var(--surface));
  }
}

.feed-story {
  display: grid;
  grid-template-columns: minmax(12rem, 0.78fr) minmax(0, 1.42fr);
  align-items: center;
  gap: clamp(var(--space-m), 5vw, var(--space-xl));
  max-inline-size: var(--measure-wide);
  margin-block-start: clamp(var(--space-l), 8vw, var(--space-xl));
  margin-inline: auto;
}

.feed-cover-panel {
  justify-self: center;
  inline-size: min(100%, 20rem);
  transform: rotate(-2deg);
}

.feed-cover {
  display: block;
  inline-size: 100%;
  block-size: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-soft);
}

.feed-description {
  max-inline-size: 62ch;
  font-size: clamp(var(--step-0), 2vw, var(--step-1));
  line-height: 1.65;
}

.feed-description > :first-child {
  margin-block-start: 0;
}

.feed-description > :last-child {
  margin-block-end: 0;
}

/* The intro navigation sticks first. Later, the search header takes over with
   its own copy of the same intrinsic row and the form directly below it.
   Neither stage uses a measured height or an independently positioned seam. */
main:has(> .home-title-bar) {
  padding-block-start: 0;
}

.home-title-bar,
.home-header {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-header);
  inline-size: 100vw;
  margin-inline: calc(50% - 50vw);
}

.home-title-bar {
  z-index: calc(var(--z-header) + 1);
  visibility: hidden;
}

.home-header {
  margin-block-start: clamp(var(--space-l), 6vw, var(--space-xl));
  background: var(--surface);
  border-block-end: 1px solid var(--border);
}

.home-header-row {
  display: grid;
  background: var(--bg);
}

.home-header-row > * {
  grid-area: 1 / 1;
  min-inline-size: 0;
  border-block-end: 1px solid var(--border);
}

.show-sticky-summary {
  display: grid;
  padding-inline: max(clamp(var(--space-s), 4vw, var(--space-xl)), env(safe-area-inset-left))
    max(clamp(var(--space-s), 4vw, var(--space-xl)), env(safe-area-inset-right));
  visibility: var(--home-nav-visibility, hidden);
}

.show-sticky-summary-inner {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  /* A grid item's `min-width` is `auto`, which is a floor at its own
     min-content — so this row could not compress below the widest it wanted to
     be, and on a 305px content box it sat 4px past the edge and took the page
     sideways with it. The children already opt out of the same floor; the row
     itself had not. */
  min-inline-size: 0;
  min-block-size: 3.8rem;
  padding-block-start: max(var(--space-2xs), env(safe-area-inset-top));
  padding-block-end: var(--space-2xs);
}

.show-sticky-home {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-block-size: var(--min-tap);
  min-inline-size: 0;
  color: var(--text);
  font-weight: 800;
  text-decoration: none;
  opacity: var(--home-header-stuck, 0);
}

.show-sticky-cover {
  inline-size: 2rem;
  block-size: 2rem;
  flex: none;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) - 4px);
}

@media (hover: hover) and (pointer: fine) {
  .show-sticky-home:hover {
    color: var(--accent);
    text-decoration: none;
  }
}

.show-sticky-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.show-sticky-section {
  position: absolute;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  color: var(--muted);
  font-size: var(--step--1);
  font-weight: 800;
  text-decoration: none;
  white-space: nowrap;
  opacity: var(--home-header-stuck, 0);
}

.show-sticky-section:focus-visible {
  color: var(--accent);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .show-sticky-section:hover {
    color: var(--accent);
    text-decoration: none;
  }
}

.show-sticky-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2xs);
  min-inline-size: 0;
}

/* The sticky "Listen on" cluster follows the compact title's reveal. */
.show-sticky-summary .show-sticky-platforms {
  display: flex;
  align-items: center;
  gap: var(--space-3xs);
  opacity: var(--home-header-stuck, 0);
}

.show-sticky-summary .platform-icon-only {
  color: var(--text);
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}

.show-sticky-summary .platform-icon-only:focus-visible {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 10%, transparent);
  border-color: transparent;
}
@media (hover: hover) and (pointer: fine) {
  .show-sticky-summary .platform-icon-only:hover {
    color: var(--accent);
    background: color-mix(in oklab, var(--accent) 10%, transparent);
    border-color: transparent;
  }
}

/* The sticky menu trigger sits flush with the icon buttons: transparent until
   hover, so the header reads as one calm row. */
.show-sticky-summary .platform-menu-trigger {
  color: var(--text);
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  padding-inline: var(--space-2xs);
  font-size: var(--step--1);
  white-space: nowrap;
}

.show-sticky-summary .platform-menu-trigger:focus-visible {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 10%, transparent);
  border-color: transparent;
}
@media (hover: hover) and (pointer: fine) {
  .show-sticky-summary .platform-menu-trigger:hover {
    color: var(--accent);
    background: color-mix(in oklab, var(--accent) 10%, transparent);
    border-color: transparent;
  }
}

/* Right-anchor the popover in the top-right sticky corner so it never spills
   past the viewport edge. */
.show-sticky-summary .platform-menu-list {
  inset-inline: auto 0;
}

.show-sticky-actions .theme-toggle {
  flex: none;
  background: var(--bg);
}

/* Theme switching requires scripting; the control always stays in its row. */
@media (scripting: none) {
  .home-title-bar {
    display: none;
  }

  .show-sticky-actions .theme-toggle {
    visibility: hidden;
  }
}

/* The first bar is present throughout the introduction. Its title and Listen
   on controls enter independently as their hero counterparts pass behind it. */
.home-title-bar .show-sticky-summary,
.home-title-bar .show-sticky-home,
.home-title-bar .show-sticky-section,
.home-title-bar .show-sticky-platforms,
.home-title-bar .home-section-heading {
  visibility: hidden;
}

.home-title-bar .show-sticky-summary-inner > h2 {
  display: none;
}

:root[data-home-early-active] .home-title-bar,
:root[data-home-early-active] .home-title-bar .show-sticky-summary {
  visibility: visible;
}

:root[data-home-early-active][data-show-home-summary]
  .home-title-bar
  :is(.show-sticky-home, .show-sticky-section),
:root[data-home-early-active][data-show-home-platforms] .home-title-bar .show-sticky-platforms {
  visibility: visible;
  opacity: 1;
}

/* Keep a focused/open intro control in place during the handoff. Both rows
   retain their identical intrinsic size, while only one navigation is exposed. */
:root[data-home-early-active] .home-header .home-header-row {
  --home-nav-visibility: hidden;
}

/* The retained intro controls precede the hero h1. Keep the joined row's
   screen-reader heading before the episode h3s while its controls are hidden. */
:root[data-home-early-active][data-home-header-stuck] .home-header .show-sticky-summary-inner > h2 {
  visibility: visible;
}

/* Scroll-state queries also enhance the joined row without scripting. The JS
   handoff coordinates the earlier bar and supplies this state where needed;
   it never sets sizes or offsets. */
@supports (container-type: scroll-state) {
  .home-header {
    container-type: scroll-state;
    container-name: home-header;
  }

  @container home-header scroll-state(stuck: top) {
    .home-header-row {
      --home-header-stuck: 1;
      --home-nav-visibility: visible;
      --home-heading-visibility: hidden;
    }
  }
}

:root[data-home-header-stuck] .home-header-row,
.home-header-row:has(
  .show-sticky-home:focus,
  .show-sticky-section:focus,
  .show-sticky-platforms:focus-within,
  .theme-toggle:focus,
  .platform-menu[open]
) {
  --home-header-stuck: 1;
  --home-nav-visibility: visible;
  --home-heading-visibility: hidden;
}

/* Scrolling must never hide the topic link while the visitor is using it. */
.home-header-row:has(.home-section-heading:focus-within) {
  --home-header-stuck: 0;
  --home-nav-visibility: hidden;
  --home-heading-visibility: visible;
}

/* The title has already appeared in the first bar. The joined copy takes over
   at full opacity, so docking search never replays the title's entrance. */
.home-header :is(.show-sticky-home, .show-sticky-section, .show-sticky-platforms) {
  opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .home-section-heading,
  .home-title-bar .show-sticky-home,
  .home-title-bar .show-sticky-section,
  .home-title-bar .show-sticky-platforms {
    transition: opacity var(--dur-quick) var(--ease-out);
  }
}

.home-search-bar {
  padding-block: clamp(var(--space-2xs), 1.5vw, var(--space-s));
  padding-inline: max(clamp(var(--space-s), 4vw, var(--space-xl)), env(safe-area-inset-left))
    max(clamp(var(--space-s), 4vw, var(--space-xl)), env(safe-area-inset-right));
  background: var(--surface);
}

/* Live search can replace a tall episode list with only a handful of results
   (or none). Without some temporary depth below this sticky rail, its parent
   becomes too short to keep the focused controls at their current viewport
   position and the rail drops toward the bottom of the screen. Reserve one
   viewport while any search control has focus; once focus leaves, the results
   collapse back to their natural height. */
.home-header:focus-within ~ #content {
  min-block-size: 100vh;
  min-block-size: 100dvh;
}

.home-section-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3xs) var(--space-m);
  margin: 0;
  padding-block: var(--space-2xs);
  padding-inline: max(clamp(var(--space-s), 4vw, var(--space-xl)), env(safe-area-inset-left))
    max(clamp(var(--space-s), 4vw, var(--space-xl)), env(safe-area-inset-right));
  opacity: calc(1 - var(--home-header-stuck, 0));
  visibility: var(--home-heading-visibility, visible);
}

/* This heading shares a row with navigation; the card-section margin below
   must not add another vertical gap when those links wrap onto the next row. */
.home-section-heading .home-episodes-heading {
  margin: 0;
}

.home-directory-links > a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  min-block-size: var(--min-tap);
  color: var(--accent);
  font-size: var(--step--1);
  font-weight: 700;
  text-underline-offset: 0.22em;
}

.home-directory-links {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  min-inline-size: 0;
  gap: 0 var(--space-s);
}

/* Semantic references — a glossary term or a person the show knows — are
   ordinary links with a quiet explanatory treatment. They remain unmistakable
   without interrupting transcript flow.

   One treatment for both, because they are one thing to a reader: a word in the
   prose that will tell you more if you ask it, and the same card answers either.
   A person's name used to fall back to the plain anchor style for want of a rule
   of its own, which made the two look like different offers.

   Matched on the *exact* class, which is what separates a reference in a
   sentence from a component that merely wants the same hover card. The
   auto-linker emits one class and nothing else; a speaker chip above a cue and a
   person card in the episode's meta band both add their own on top, so they are
   excluded by construction rather than by a list of exceptions — which is how
   the cards came to wear a dotted underline in the first place. A test pins the
   linker to a single class so this cannot silently stop matching. */
[class="glossary-link"],
[class="person-link"] {
  color: inherit;
  background: color-mix(in oklab, var(--accent) 9%, transparent);
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 0.1em;
  text-underline-offset: 0.18em;
}

/* The emphasised states tint their own background *on top of* whatever the cue
   already paints (hover, `:target`, the active-cue highlight), so bare
   `--accent` text lands on a background two tints deep and measured only
   4.41:1 — under AA, and close enough to the line that it passed or failed on
   which Chrome resolved the nested `oklab` compositing. Pulling the text
   toward `--text` fixes it from the side that cannot compound: it darkens in
   light mode, lightens in dark mode, and gains contrast under every accent
   theme, instead of tuning one tint percentage against one backdrop. */
[class="glossary-link"]:focus-visible,
[class="person-link"]:focus-visible {
  color: color-mix(in oklab, var(--accent) 78%, var(--text));
  background: color-mix(in oklab, var(--accent) 16%, transparent);
  text-decoration-style: solid;
}

@media (hover: hover) and (pointer: fine) {
  [class="glossary-link"]:hover,
  [class="person-link"]:hover {
    color: color-mix(in oklab, var(--accent) 78%, var(--text));
    background: color-mix(in oklab, var(--accent) 16%, transparent);
    text-decoration-style: solid;
  }
}

.home-search-bar-inner {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xs);
  max-inline-size: var(--measure-wide);
  margin-inline: auto;
}

.home-search-bar form[role="search"] {
  display: grid;
  grid-template-columns: minmax(12rem, 1fr) minmax(9rem, 14rem) auto;
  gap: var(--space-2xs);
  align-items: center;
  margin: 0;
}

.home-search-bar form[role="search"] > label {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.home-search-bar input[type="search"] {
  box-sizing: border-box;
  block-size: var(--min-tap);
  min-block-size: var(--min-tap);
}

.home-search-bar select {
  box-sizing: border-box;
  inline-size: 100%;
  block-size: var(--min-tap);
  min-block-size: var(--min-tap);
  padding-block: 0;
  padding-inline: var(--space-s);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.home-search-bar button[type="submit"] {
  align-self: center;
  block-size: var(--min-tap);
  min-block-size: var(--min-tap);
  padding-inline: var(--space-s);
}

.home-search-bar .search-scope {
  grid-column: 1 / -1;
}

.search-submit-icon {
  inline-size: 1.05em;
  block-size: 1.05em;
  flex: none;
}

.episodes-peek {
  position: fixed;
  inset-inline-end: max(var(--space-s), env(safe-area-inset-right));
  inset-block-end: max(var(--space-s), env(safe-area-inset-bottom));
  z-index: var(--z-side-nav);
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  min-block-size: var(--min-tap);
  padding: var(--space-2xs) var(--space-xs);
  color: var(--accent);
  font-weight: 800;
  text-decoration: none;
  background: color-mix(in oklab, var(--surface) 90%, transparent);
  border: 1px solid color-mix(in oklab, var(--accent) 44%, var(--border));
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-soft);
  backdrop-filter: blur(14px);
  opacity: 1;
  transform: translateY(0);
}

.episodes-peek svg {
  inline-size: 1.15em;
  block-size: 1.15em;
}

:root[data-show-home-episodes] .episodes-peek {
  opacity: 0;
  pointer-events: none;
  transform: translateY(0.5rem);
}

@media (prefers-reduced-motion: no-preference) {
  .episodes-peek {
    transition:
      opacity var(--dur-quick) var(--ease-out),
      transform var(--dur-quick) var(--ease-out);
  }
}

@media (max-width: 54rem) {
  .feed-header {
    min-block-size: auto;
    margin-block-start: var(--space-xl);
  }

  .feed-story {
    grid-template-columns: 1fr;
    margin-block-start: var(--space-l);
  }

  .feed-cover-panel {
    inline-size: min(72vw, 16rem);
  }

  .home-search-bar form[role="search"] {
    grid-template-columns: minmax(0, 1fr) minmax(8rem, 38vw) auto;
  }
}

@media (max-width: 36rem) {
  .feed-title-stack {
    --feed-title-size: clamp(1.49rem, 13.5vw, 4.41rem);
  }

  .feed-platforms ul {
    gap: var(--space-3xs);
  }

  .feed-platforms .platform {
    min-block-size: 2.25rem;
    padding-inline: var(--space-2xs);
    font-size: var(--step--1);
  }

  .show-sticky-summary-inner {
    min-block-size: 3rem;
    gap: var(--space-2xs);
    padding-block-end: var(--space-3xs);
  }

  .show-sticky-cover {
    inline-size: 1.75rem;
    block-size: 1.75rem;
  }

  .show-sticky-title {
    max-inline-size: 32vw;
  }

  .show-sticky-section {
    display: none;
  }

  .show-sticky-summary .show-sticky-platforms {
    gap: 0.15rem;
  }

  .show-sticky-summary .platform-menu-trigger {
    padding-inline: var(--space-3xs);
  }

  .show-sticky-actions {
    gap: 0.2rem;
  }

  .home-search-bar {
    padding-block: var(--space-2xs);
  }

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

  .home-section-heading {
    column-gap: var(--space-xs);
  }

  .home-episodes-heading {
    font-size: var(--step-1);
  }

  .home-search-bar form[role="search"] {
    grid-template-columns: minmax(0, 1fr) minmax(7rem, 34vw) auto;
  }

  .home-search-bar select {
    min-inline-size: 0;
  }

  .home-search-bar button[type="submit"],
  .site-banner form[role="search"] button[type="submit"] {
    align-self: center;
    aspect-ratio: 1;
    inline-size: var(--min-tap);
    min-inline-size: var(--min-tap);
    block-size: var(--min-tap);
    padding-inline: 0;
  }

  .search-submit-label {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .episodes-peek-label {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* 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: var(--radius-pill);
  text-decoration: none;
}

@media (hover: hover) and (pointer: fine) {
  .platform:hover {
    border-color: var(--muted);
    background: var(--bg);
    text-decoration: none;
  }
}

.platform-icon-only {
  inline-size: var(--min-tap);
  min-inline-size: var(--min-tap);
  padding-inline: 0;
}

/* 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-deezer .platform-icon {
  fill: #a238ff;
}

/* Amazon Music is a two-tone badge with its colours baked into the SVG (like
   Apple Podcasts), so no brand fill here. */

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

/* ---- "Listen on ▾" menu -------------------------------------------------
   A native <details> (no JS): the `.platform` pill summary toggles a floating
   popover list of the streaming platforms. Same pattern + stacking rationale as
   the download chip — the wrapper is the stacking-context anchor pinned above
   the sticky player dock so the panel never paints under it. */
.platform-menu {
  position: relative;
  z-index: var(--z-popover);
}
.platform-menu[open] {
  z-index: var(--z-side-nav);
}
/* Strip the native disclosure marker — the trigger carries its own caret. */
.platform-menu > summary {
  list-style: none;
  cursor: pointer;
}
.platform-menu > summary::-webkit-details-marker {
  display: none;
}
.platform-menu-arrow {
  display: inline-flex;
  align-items: center;
  margin-inline-start: var(--space-3xs);
}
.platform-menu-arrow svg {
  inline-size: 0.7em;
  block-size: 0.5em;
}
.platform-menu[open] .platform-menu-arrow {
  transform: rotate(180deg);
}
@media (prefers-reduced-motion: no-preference) {
  .platform-menu-arrow {
    transition: transform var(--dur-quick) var(--ease-out);
  }
}

/* The popover panel: absolutely positioned beneath the trigger, hidden until
   [open] (the <details> hides it). Compact — hugs its rows. */
.platform-menu-list {
  position: absolute;
  inset-block-start: calc(100% + var(--space-3xs));
  inset-inline-start: 0;
  z-index: 1;
  min-inline-size: 12rem;
  margin: 0;
  padding: var(--space-3xs);
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 6px 18px -8px rgba(0, 0, 0, 0.25);
  font-size: var(--step--1);
}

/* Force a vertical, left-aligned, non-wrapping stack. Needs HIGHER specificity
   than the row-flex the list inherits as a `ul` inside `.platforms` — namely
   `.platforms ul` (display:flex + flex-wrap) and `.feed-platforms ul`
   (justify/align center), both (0,1,1). Without this the rows center and wrap
   like the pill rows. `.platform-menu .platform-menu-list` is (0,2,0). */
.platform-menu .platform-menu-list {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  flex-wrap: nowrap;
  gap: 0;
}

.platform-menu-list li {
  display: block;
}

/* Each row: brand glyph + label, a full-width tap target. */
.platform-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding-block: 0.45em;
  padding-inline: var(--space-2xs);
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
  border-radius: calc(var(--radius) - 2px);
}
.platform-menu-item:focus-visible {
  background: var(--bg);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .platform-menu-item:hover {
    background: var(--bg);
    text-decoration: none;
  }
}

.platform-menu-item .platform-icon {
  inline-size: 1.1em;
  block-size: 1.1em;
  flex: none;
}

/* ---- 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;
  margin-block: 0;
  padding: clamp(var(--space-s), 3vw, var(--space-m)) 0;
}

.episodes-section {
  padding-block-start: clamp(var(--space-m), 4vw, var(--space-l));
}

.episodes--static {
  display: grid;
  grid-template-columns: 1fr;
}

@media (min-width: 44rem) {
  .episodes--static.episodes--cols-2,
  .episodes--static.episodes--cols-3 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 70rem) {
  .episodes--static.episodes--cols-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

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

.season-break {
  position: relative;
  flex: 0 0 100%;
  inline-size: 100%;
  display: flex;
  align-items: center;
  margin-block: clamp(var(--space-l), 5vw, var(--space-xl)) var(--space-xs);
  margin-inline: 0;
  padding-block-start: var(--space-s);
  color: var(--muted);
  font-size: var(--step--1);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.season-break:first-child {
  margin-block-start: 0;
}

.season-break:not(:first-child)::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: calc(50% - 50vw);
  inline-size: 100vw;
  border-block-start: 1px solid var(--border);
}

.season-break-label {
  color: var(--accent);
}

.episodes--static .season-break {
  grid-column: 1 / -1;
}

.episodes--static > li {
  flex: none;
}

.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--static .episode--sm,
.episodes--static .episode--md,
.episodes--static .episode--lg {
  flex: none;
}

.episode-more {
  flex-basis: 100%;
  grid-column: 1 / -1;
  justify-content: center;
}

.episode-more-link {
  margin-inline: auto;
  min-inline-size: min(100%, 14rem);
}

.episode-more-dots {
  display: none;
  gap: 0.25rem;
  align-items: center;
}

.episode-more-link.is-loading {
  pointer-events: none;
}

.episode-more-link.is-loading .episode-more-label {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.episode-more-link.is-loading .episode-more-dots {
  display: inline-flex;
}

.episode-more-dots span {
  inline-size: 0.38rem;
  block-size: 0.38rem;
  border-radius: 50%;
  background: currentColor;
}

@media (prefers-reduced-motion: no-preference) {
  .episode-more-dots span {
    animation: episode-more-pulse 900ms ease-in-out infinite;
  }

  .episode-more-dots span:nth-child(2) {
    animation-delay: 120ms;
  }

  .episode-more-dots span:nth-child(3) {
    animation-delay: 240ms;
  }
}

@keyframes episode-more-pulse {
  0%,
  100% {
    opacity: 0.35;
    transform: translateY(0);
  }

  50% {
    opacity: 1;
    transform: translateY(-0.18rem);
  }
}

.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. A compact relationship cue
   lives inside that summary and trades one teaser line for its own, rather than
   growing a separate footer 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-xs));
}

.episodes article.episode-card--explicit {
  padding-block-end: 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. It normally stays on one
   line, but may wrap between the identity and duration rather than clipping. */
.episode-tags {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--space-m);
  transform: translateY(-50%);
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  inline-size: fit-content;
  max-inline-size: calc(100% - var(--space-m) * 2);
  margin: 0;
  padding: var(--space-3xs) var(--space-s);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-soft);
}

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

/* Season/episode: the most visible meta — accent, bold, slightly tracked.
   The full wording is the baseline (and compatibility fallback); supporting
   browsers progressively swap to the localized short form on tight cards. */
.tag-episode {
  min-inline-size: 0;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--accent);
  overflow-wrap: anywhere;
  white-space: normal;
}

.tag-episode-long {
  display: inline;
}

.tag-episode-short {
  display: none;
}

/* 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-duration {
  display: inline-flex;
  flex: none;
  align-items: center;
  white-space: nowrap;
}

.tag-episode + .episode-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;
}

/* Card-relative progressive disclosure: wording matters more than the gauge.
   Hide the gauge through the largest 26rem tier plus a 2rem reserve for long
   localized labels and cross-platform font metrics. It returns once flex
   growth gives a card real surplus room. Wrapping remains the final fallback. */
@container (max-width: 28rem) {
  .ep-gauge {
    display: none;
  }
}

/* Keep this below the 21.3–22.4rem band a 390px viewport reaches depending on
   classic versus overlay scrollbars, so platform chrome does not flip wording. */
@container (max-width: 21rem) {
  .tag-episode-long {
    display: none;
  }

  .tag-episode-short {
    display: inline;
  }
}

.episode-explicit-badge {
  position: absolute;
  inset-inline-end: var(--space-xs);
  inset-block-end: var(--space-xs);
  max-inline-size: calc(100% - var(--space-xs) * 2);
  padding: 0.1rem var(--space-2xs);
  overflow: hidden;
  color: var(--danger);
  background: color-mix(in oklab, var(--danger) 11%, var(--surface));
  border: 1px solid color-mix(in oklab, var(--danger) 55%, var(--border));
  border-radius: var(--radius-pill);
  font-size: var(--step--2);
  font-weight: 800;
  line-height: 1.35;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.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 without creating another card row. */
.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;
}

/* A relationship carries more useful context than the last teaser line. For
   the usual single relation, swapping that line keeps related and unrelated
   cards at roughly the same height instead of stretching every card in a row. */
.episode-summary:has(.episode-relations--compact) .episode-excerpt {
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

/* Typed episode relationships stay compact on cards and expand into their own
   semantic section on detail pages. The kind is visually secondary; the
   related episode remains a normal, keyboard-focusable link. */
.episode-relations {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.episode-relations--compact {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--space-s);
  font-size: var(--step--1);
  line-height: 1.35;
}

.episode-relations--compact > li {
  min-inline-size: 0;
}

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

.episode-relation-link {
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

.episode-relations--compact .episode-relation-link {
  display: inline-flex;
  flex: 0 1 auto;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.08em 0.28em;
  max-inline-size: 100%;
  padding-block: 0.1rem;
  color: var(--text);
  text-decoration: none;
}

.episode-relations--compact .episode-relation-link:focus-visible {
  color: var(--text);
  text-decoration: none;
}
.episode-relations--compact
  .episode-relation-link:focus-visible
  :where(.episode-relation-number, .episode-relation-title) {
  text-decoration: underline;
  text-underline-offset: 0.14em;
}
.episode-relations--compact .episode-relation-link:focus-visible .episode-relation-kind {
  color: var(--accent);
}
@media (hover: hover) and (pointer: fine) {
  .episode-relations--compact .episode-relation-link:hover {
    color: var(--text);
    text-decoration: none;
  }

  .episode-relations--compact .episode-relation-link:hover .episode-relation-kind {
    color: var(--accent);
  }

  .episode-relations--compact
    .episode-relation-link:hover
    :where(.episode-relation-number, .episode-relation-title) {
    text-decoration: underline;
    text-underline-offset: 0.14em;
  }
}

.episode-relation-number {
  flex: none;
  color: var(--accent);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.episode-relations--compact .episode-relation-number {
  flex: 0 1 auto;
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

.episode-relation-title {
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

.episode-relation-arrow {
  flex: none;
  color: var(--accent);
  font-weight: 800;
}

.episode-relations-section {
  inline-size: var(--episode-chrome-inline);
  max-inline-size: var(--episode-chrome-inline);
  margin-inline: calc((100% - var(--episode-chrome-inline)) / 2);
  margin-block: var(--space-l);
  padding: var(--space-m);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.episode-relations-section > h2 {
  margin-block-end: var(--space-s);
}

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

@media (hover: hover) and (pointer: fine) {
  .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:focus-visible {
  color: var(--accent);
  text-decoration: underline;
}
@media (hover: hover) and (pointer: fine) {
  .results-back a:hover {
    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;
}

@media (hover: hover) and (pointer: fine) {
  .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--static h3 {
  font-size: var(--step-1);
}

@media (hover: hover) and (pointer: fine) {
  .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));
  }
}

.episodes--static :where(.episode--sm, .episode--md, .episode--lg) h3 {
  font-size: var(--step-1);
}

@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 no motion gate applies to it --
   it is the one piece of this feedback a reduced-motion reader still gets.
   Its hover half is gated on the pointer all the same (a finger cannot
   hover); the `:focus-within` half is what reaches a touch or keyboard
   reader. */
.episodes > li:focus-within > article,
.results > li:focus-within > article {
  border-color: var(--muted);
}
@media (hover: hover) and (pointer: fine) {
  .episodes > li:hover > article,
  .results > li:hover > 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 {
    transform: translateY(1rem);
  }

  to {
    transform: translateY(0);
  }
}

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

.search-results {
  /* A little more air below the sticky banner. */
  margin-block-start: var(--space-2xs);
}

/* Episode search is a distinct, native document state: remove the reading hero
   and main's leading padding so the player follows the site banner directly,
   then let the result section follow the player in ordinary flow. Its
   `data-init` resets the document to this real start; there are no calculated
   offsets, preserved transcript heights, or hidden content to scroll into. */
body:has(.episode-detail #content > .search-results) main {
  padding-block-start: 0;
}

.episode-detail:has(#content > .search-results) .episode-header {
  display: none;
}

.episode-detail #content > .search-results {
  margin-block-start: 0;
}
/* 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);
}

@media (hover: hover) and (pointer: fine) {
  .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;
}

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

form[role="search"] .search-scope {
  flex-basis: 100%;
  color: var(--muted);
  font-size: var(--step--2);
  line-height: 1.35;
}

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

/* Keep icon and label centered with room between them on every search surface. */
form[role="search"] button[type="submit"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  gap: var(--space-2xs);
  min-inline-size: var(--min-tap);
  padding-block: var(--space-3xs);
  line-height: 1.2;
  white-space: nowrap;
}

/* ---- Topics and glossary ----------------------------------------------- */

.topic-list,
.topic-index-list,
.person-index-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.episode-topics {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-xs);
  margin-block: var(--space-xs);
}

.topic-list-label,
.topic-kicker,
.person-kicker {
  color: var(--muted);
  font-size: var(--step--2);
  font-weight: 800;
  letter-spacing: 0.08em;
  line-height: 1.35;
  text-transform: uppercase;
}

.topic-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.topic-list a {
  display: inline-flex;
  align-items: center;
  gap: 0.08em;
  min-block-size: 2rem;
  padding-inline: var(--space-2xs);
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 9%, transparent);
  border: 1px solid color-mix(in oklab, var(--accent) 34%, var(--border));
  border-radius: 0.45rem;
  font-size: var(--step--1);
  font-weight: 700;
  text-decoration: none;
}

.topic-list a:focus-visible {
  background: color-mix(in oklab, var(--accent) 16%, transparent);
  border-color: var(--accent);
  text-decoration: underline;
}
@media (hover: hover) and (pointer: fine) {
  .topic-list a:hover {
    background: color-mix(in oklab, var(--accent) 16%, transparent);
    border-color: var(--accent);
    text-decoration: underline;
  }
}

/* Inside the meta band the separation is the band's own; an accent bar per
   group would be two frames around one aside. */
.episode-topics--meta {
  margin-block: 0;
}

.episode-topics--compact {
  margin-block: var(--space-3xs) 0;
}

.episode-topics--compact .topic-list-label {
  font-size: 0.72rem;
}

.episode-topics--compact .topic-list a {
  min-block-size: 1.75rem;
  padding-inline: 0.42rem;
  font-size: var(--step--2);
}

.topic-directory,
.topic-archive,
.person-directory,
.person-detail {
  inline-size: min(100% - 2 * clamp(var(--space-s), 4vw, var(--space-xl)), 68rem);
  margin-inline: auto;
  padding-block: clamp(var(--space-xl), 8vw, var(--space-2xl));
}

.topic-directory-header,
.topic-archive-header,
.person-directory-header,
.person-detail-header {
  max-inline-size: 48rem;
  margin-block-end: clamp(var(--space-l), 5vw, var(--space-xl));
}

.topic-directory-header h1,
.topic-archive-header h1,
.person-directory-header h1,
.person-detail-header h1 {
  margin-block: var(--space-3xs) var(--space-xs);
  text-wrap: balance;
}

.topic-archive-header h1 {
  color: var(--accent);
}

.topic-directory-intro,
.topic-result-count,
.topic-empty-state,
.person-directory-intro,
.person-empty-state,
.person-detail-bio {
  max-inline-size: 52ch;
  margin: 0;
  color: var(--muted);
  font-size: var(--step-0);
  text-wrap: pretty;
}

/* The bio is authored in the same rich-text editor as show notes, so it can be
   several paragraphs with emphasis and links. Collapse only the outer margins,
   so the block still starts and ends flush with the heading above it. */
.person-detail-bio > :first-child {
  margin-block-start: 0;
}

.person-detail-bio > :last-child {
  margin-block-end: 0;
}

.person-detail-bio :is(ul, ol) {
  padding-inline-start: 1.25em;
}

.person-detail-bio a {
  color: var(--accent);
}

.topic-back-link {
  display: inline-flex;
  color: var(--muted);
  font-size: var(--step--1);
  font-weight: 700;
}

/* The ways out of a person or topic page: back to the episode that sent you,
   when one did, beside the way to the rest of the directory. Same shape as the
   glossary entry's, because it answers the same question. */
.topic-archive-back,
.person-detail-back-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-m);
  margin-block-end: var(--space-l);
}

.topic-archive-back a,
.person-detail-back-links a {
  color: var(--muted);
  font-size: var(--step--1);
  font-weight: 700;
}

.topic-archive-back a:focus-visible,
.person-detail-back-links a:focus-visible {
  color: var(--accent);
}
@media (hover: hover) and (pointer: fine) {
  .topic-archive-back a:hover,
  .person-detail-back-links a:hover {
    color: var(--accent);
  }
}

.topic-index-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: var(--space-s);
}

.topic-index-card,
.person-index-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "name arrow"
    "count arrow";
  align-items: center;
  gap: var(--space-3xs) var(--space-s);
  min-block-size: 7rem;
  padding: var(--space-m);
  color: var(--text);
  background: color-mix(in oklab, var(--surface) 90%, var(--bg));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-soft);
  text-decoration: none;
}

.topic-index-card:focus-visible {
  border-color: var(--accent);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .topic-index-card:hover {
    border-color: var(--accent);
    text-decoration: none;
  }
}

.topic-index-name,
.person-index-name {
  grid-area: name;
  min-inline-size: 0;
  overflow-wrap: anywhere;
  color: var(--accent);
  font-size: var(--step-1);
  font-weight: 800;
}

.topic-index-count,
.person-index-count {
  grid-area: count;
  color: var(--muted);
  font-size: var(--step--1);
}

.topic-index-arrow,
.person-index-arrow {
  grid-area: arrow;
  color: var(--accent);
  font-size: var(--step-1);
}

.topic-archive .episodes-section {
  padding-block-start: 0;
}

.topic-archive .episodes {
  padding-block-start: 0;
}

.glossary-directory,
.glossary-entry {
  inline-size: min(100% - 2 * clamp(var(--space-s), 4vw, var(--space-xl)), 68rem);
  margin-inline: auto;
  padding-block: clamp(var(--space-xl), 8vw, var(--space-2xl));
  overflow-wrap: anywhere;
}

.glossary-directory-header,
.glossary-entry > header {
  max-inline-size: 48rem;
  margin-block-end: clamp(var(--space-l), 5vw, var(--space-xl));
}

.glossary-directory-header h1,
.glossary-entry h1,
.glossary-card h2 {
  margin-block: 0 var(--space-xs);
  text-wrap: balance;
}

.glossary-kicker,
.glossary-directory-intro,
.glossary-empty-state,
.glossary-card-aliases,
.glossary-entry-aliases {
  color: var(--muted);
}

.glossary-kicker {
  margin-block-end: var(--space-3xs);
  font-size: var(--step--2);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.glossary-index-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
  gap: var(--space-s);
}

.glossary-card {
  min-inline-size: 0;
  padding: var(--space-m);
  background: color-mix(in oklab, var(--surface) 90%, var(--bg));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-soft);
  overflow-wrap: anywhere;
}

.glossary-card h2 a {
  color: var(--accent);
}

.glossary-description {
  max-inline-size: var(--measure);
  line-height: 1.7;
}

.glossary-entry-back {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-m);
  align-items: center;
  margin-block-end: var(--space-l);
}

/* One lazy definition card serves every semantic link on the page. It uses the
   native top layer; measured `left`/`top` values from reference-popover.js keep
   all four edges inside the visual viewport even under zoom, rotation, or an
   unusually long definition. Dismissal is coordinated by the enhancement so a
   click on its trigger cannot race the browser's automatic light-dismiss. */
.reference-popover[popover] {
  position: fixed;
  inset: unset;
  display: none;
  grid-template-rows: minmax(0, 1fr) auto;
  min-inline-size: 0;
  max-inline-size: calc(100vw - 0.5rem);
  max-block-size: calc(100vh - 0.5rem);
  margin: 0;
  padding: 0;
  color: var(--text);
  background:
    linear-gradient(135deg, color-mix(in oklab, var(--accent) 10%, transparent), transparent 38%),
    var(--surface);
  border: 1px solid color-mix(in oklab, var(--accent) 42%, var(--border));
  border-radius: calc(var(--radius) + 0.4rem);
  box-shadow: 0 1.5rem 4rem -1.4rem rgb(0 0 0 / 60%);
  overflow: hidden;
  overscroll-behavior: contain;
}

.reference-popover:popover-open {
  display: grid;
}

/* A narrow/coarse definition card behaves as a contained bottom sheet. Keep
   both the open sheet and an outside dismissal gesture from chaining scroll
   into the episode beneath it. The sheet body remains its own scrollport. */
html:has(.reference-popover[data-layout="sheet"]:popover-open),
html[data-reference-gesture-lock="true"] {
  overflow: hidden;
  overscroll-behavior: none;
}

.reference-popover-close {
  position: absolute;
  inset-block-start: var(--space-xs);
  inset-inline-end: var(--space-xs);
  z-index: 1;
  display: grid;
  place-items: center;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  min-block-size: 2.5rem;
  padding: 0;
  color: var(--muted);
  background: color-mix(in oklab, var(--surface) 88%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-soft);
  font-size: 1.5rem;
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
}

.reference-popover-close:focus-visible {
  color: var(--text);
  background: color-mix(in oklab, var(--accent) 12%, var(--surface));
  border-color: var(--accent);
}

@media (hover: hover) and (pointer: fine) {
  .reference-popover-close:hover {
    color: var(--text);
    background: color-mix(in oklab, var(--accent) 12%, var(--surface));
    border-color: var(--accent);
  }
}

.reference-popover-body {
  min-block-size: 0;
  padding: clamp(var(--space-m), 4vw, var(--space-l));
  padding-inline-end: clamp(4.25rem, 9vw, 5rem);
  overflow: auto;
  scrollbar-gutter: stable;
}

.reference-popover-body:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: -3px;
}

.reference-popover-content {
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

.reference-popover-content > header {
  margin-block-end: var(--space-m);
}

/* Nothing below it to separate from: a person with no bio gets a card that
   ends where the name does, not one with a held-open gap under it. */
.reference-popover-content > header:last-child {
  margin-block-end: 0;
}

.reference-popover-kicker {
  margin: 0 0 var(--space-3xs);
  color: var(--accent);
  font-size: var(--step--2);
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

/* The term this card is about. Sized in steps, not in `vw`: this heading used
   to read `clamp(var(--step-2), 5vw, var(--step-4))`, which is a full-page
   hero's rule, and a card is not the viewport -- on a desktop it resolved to
   the 3.5rem cap inside a box a few hundred pixels wide. (It never rendered
   that way, because `--step-4` was undefined and the whole declaration was
   dropped, leaving the title *smaller* than an ordinary `h2`. Defining the
   token is what made the mistake visible.) */
.reference-popover-content h2 {
  max-inline-size: 20ch;
  margin: 0;
  color: var(--text);
  font-size: var(--step-2);
  line-height: 1.04;
  text-wrap: balance;
}

.reference-popover-aliases {
  margin: var(--space-xs) 0 0;
  color: var(--muted);
  font-size: var(--step--1);
  font-style: italic;
  line-height: 1.45;
}

.reference-popover-description {
  min-inline-size: 0;
  max-inline-size: var(--measure);
  line-height: 1.65;
}

.reference-popover-description > :first-child {
  margin-block-start: 0;
}

.reference-popover-description > :last-child {
  margin-block-end: 0;
}

.reference-popover-description :is(pre, table) {
  display: block;
  max-inline-size: 100%;
  overflow-x: auto;
}

.reference-popover-description :is(code, pre, a) {
  overflow-wrap: anywhere;
}

.reference-popover-status {
  min-block-size: 7rem;
  margin: 0;
  color: var(--muted);
  font-size: var(--step-0);
}

/* While the skeleton draws the card, the loading words are for assistive
   tech only; without this their reserved height would still take space. */
.reference-popover-status.sr-only {
  min-block-size: 0;
}

/* The loading card (reference-popover.js `loading`): the shape of a definition
   at about its usual height. Text-tinted fills read on either palette; the
   shimmer is motion and only runs when it is welcome. */
.reference-popover-skeleton {
  --skeleton-fill: color-mix(in oklab, var(--text) 7%, transparent);
  --skeleton-shine: color-mix(in oklab, var(--text) 13%, transparent);

  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  min-block-size: 10rem;
}
.reference-popover-skeleton-line {
  display: block;
  inline-size: 92%;
  max-inline-size: var(--measure);
  block-size: 0.9rem;
  background-color: var(--skeleton-fill);
  border-radius: var(--radius-small);
}
.reference-popover-skeleton-line.is-kicker {
  inline-size: 5rem;
  block-size: 0.6rem;
  background-color: color-mix(in oklab, var(--accent) 22%, transparent);
}
.reference-popover-skeleton-line.is-title {
  inline-size: min(14rem, 70%);
  block-size: 1.9rem;
  margin-block-end: var(--space-s);
}
.reference-popover-skeleton-line.is-short {
  inline-size: 55%;
}
@media (prefers-reduced-motion: no-preference) {
  .reference-popover-skeleton-line {
    background-image: linear-gradient(
      90deg,
      transparent 0%,
      var(--skeleton-shine) 50%,
      transparent 100%
    );
    background-size: 200% 100%;
    background-repeat: no-repeat;
    animation: reference-skeleton-shimmer 1.4s ease-in-out infinite;
  }
  /* What replaces it arrives rather than blinks on. */
  .reference-popover-content {
    animation: reference-content-arrive var(--dur-default) var(--ease-out);
  }
  @keyframes reference-skeleton-shimmer {
    from {
      background-position: 150% 0;
    }
    to {
      background-position: -50% 0;
    }
  }
  @keyframes reference-content-arrive {
    from {
      opacity: 0;
    }
  }
}

.reference-popover-footer {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-m);
  align-items: center;
  justify-content: space-between;
  padding: var(--space-xs) clamp(var(--space-m), 4vw, var(--space-l));
  background: color-mix(in oklab, var(--surface) 92%, var(--bg));
  border-block-start: 1px solid var(--border);
}

.reference-popover-history {
  display: inline-flex;
  gap: var(--space-2xs);
}

.reference-popover-history-button {
  display: grid;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  min-block-size: 2.75rem;
  padding: 0;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: none;
  font-size: var(--step-1);
  line-height: 1;
  cursor: pointer;
}

.reference-popover-history-button:focus-visible {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 10%, var(--surface));
  border-color: var(--accent);
}

.reference-popover-history-button:disabled {
  color: color-mix(in oklab, var(--muted) 58%, transparent);
  background: transparent;
  border-color: color-mix(in oklab, var(--border) 72%, transparent);
  cursor: default;
}

@media (hover: hover) and (pointer: fine) {
  .reference-popover-history-button:not(:disabled):hover {
    color: var(--accent);
    background: color-mix(in oklab, var(--accent) 10%, var(--surface));
    border-color: var(--accent);
  }
}

.reference-popover-full-link {
  display: inline-flex;
  align-items: center;
  min-block-size: 2.75rem;
  color: var(--accent);
  font-size: var(--step--1);
  font-weight: 800;
  text-align: end;
  text-underline-offset: 0.2em;
}

.reference-popover[data-layout="sheet"] {
  box-shadow: 0 -1.25rem 4rem -1.5rem rgb(0 0 0 / 68%);
}

.reference-popover[data-layout="sheet"] .reference-popover-body {
  overscroll-behavior: contain;
  /* Own vertical panning at the gesture source on mobile Safari while keeping
     pinch zoom available for low-vision visitors. */
  touch-action: pan-y pinch-zoom;
}

.reference-popover[data-layout="sheet"]::before {
  position: absolute;
  inset-block-start: var(--space-xs);
  inset-inline-start: 50%;
  inline-size: 2.5rem;
  block-size: 0.22rem;
  background: color-mix(in oklab, var(--muted) 42%, transparent);
  border-radius: var(--radius-pill);
  translate: -50% 0;
  content: "";
}

.reference-popover[data-layout="sheet"]::backdrop {
  background: rgb(0 0 0 / 22%);
}

@media (max-width: 44rem), (pointer: coarse) {
  .reference-popover-close {
    inline-size: 2.75rem;
    block-size: 2.75rem;
    min-block-size: 2.75rem;
  }
}

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

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

  /* The card arrives (a short rise and fade in, `--dur-default`) and leaves
     faster than it came (`--dur-quick`), so a dismissal gets out of the way.
     Transitions rather than a keyframe entrance, because only a transition can
     run on the way *out*: `allow-discrete` keeps `display` and the top-layer
     `overlay` in place until the fade has finished. Opacity and translate
     only -- a scale would also skew the size reference-popover.js measures. */
  .reference-popover[popover] {
    opacity: 0;
    translate: 0 0.35rem;
    transition:
      opacity var(--dur-quick) var(--ease-out),
      translate var(--dur-quick) var(--ease-out),
      display var(--dur-quick) allow-discrete,
      overlay var(--dur-quick) allow-discrete;
  }

  .reference-popover[data-layout="sheet"] {
    translate: 0 1rem;
  }

  .reference-popover:popover-open {
    opacity: 1;
    translate: 0 0;
    transition-duration: var(--dur-default);

    @starting-style {
      opacity: 0;
      translate: 0 0.35rem;
    }
  }

  .reference-popover[data-layout="sheet"]:popover-open {
    @starting-style {
      translate: 0 1rem;
    }
  }
}

/* A card on its way out is already dismissed: it must not catch the click or
   the tap that follows. */
.reference-popover:not(:popover-open) {
  pointer-events: none;
}

/* Ordinary links remain usable while a transcript passage is streaming. */
.transcript-pages {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  margin-block: 1.5rem;
}

/* ---- Person pages ------------------------------------------------------- */
/* Directory cards and headers reuse the topic-directory rules above; only the
   parts a person page has and a topic archive does not live here. */

/* The picture leads the name, and drops out of the flow on a narrow screen
   rather than pushing the heading into a column of one word per line. */
.person-detail-identity {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-s);
  min-inline-size: 0;
}

.person-detail-identity > div {
  flex: 1 1 18rem;
  min-inline-size: 0;
}

.person-detail-identity .episode-person-avatar {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: 5rem;
  block-size: 5rem;
  border-radius: var(--radius-pill);
  background: color-mix(in oklab, var(--accent) 18%, var(--surface));
  color: var(--text);
  font-size: var(--step-2);
  font-weight: 700;
  object-fit: cover;
}

/* A short name in bare quotes reads as trivia. The label says what it is, so
   the pair is set as one small line: a muted caption and the name it names. */
.person-detail-short {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3xs) var(--space-2xs);
  margin-block: 0 var(--space-xs);
  font-size: var(--step--1);
}

.person-detail-short-label {
  color: var(--muted);
  font-weight: 800;
  letter-spacing: 0.08em;
  line-height: 1.35;
  text-transform: uppercase;
  font-size: var(--step--2);
}

.person-detail-short-name {
  color: var(--text);
  font-size: var(--step-0);
  font-weight: 650;
  overflow-wrap: anywhere;
}

.person-detail-links,
.person-detail-episodes {
  margin-block-start: clamp(var(--space-l), 5vw, var(--space-xl));
}

.person-detail-links h2,
.person-detail-episodes h2 {
  margin-block: 0 var(--space-s);
}

/* Cards, not a run of underlined words. Each one names the destination and
   shows the host under it, which is the question a reader actually has before
   leaving the site — and gives the row enough air to be read as a list. */
/* Cards of a readable width, as many per row as fit. The upper bound matters:
   a single link stretched across a 68rem column reads as a banner, not as one
   item in a list that may grow. */
.person-link-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 20rem));
  justify-content: start;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.person-link-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "label arrow"
    "host  arrow";
  align-items: center;
  gap: 0 var(--space-2xs);
  block-size: 100%;
  padding: var(--space-2xs) var(--space-s);
  color: var(--text);
  background: color-mix(in oklab, var(--surface) 90%, var(--bg));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
}

.person-link-card:focus-visible {
  border-color: var(--accent);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .person-link-card:hover {
    border-color: var(--accent);
    text-decoration: none;
  }
}

.person-link-label {
  grid-area: label;
  min-inline-size: 0;
  overflow-wrap: anywhere;
  color: var(--accent);
  font-weight: 700;
}

.person-link-host {
  grid-area: host;
  min-inline-size: 0;
  overflow-wrap: anywhere;
  color: var(--muted);
  font-size: var(--step--1);
}

/* A URL we could not parse a host out of leaves the line blank rather than a
   stray gap under the label. */
.person-link-host:empty {
  display: none;
}

.person-link-arrow {
  grid-area: arrow;
  color: var(--accent);
}

/* The episode cards bring their own vertical rhythm (they are the same cards
   the home page and topic archives use). Without this the heading floats a
   full section's worth of space above its own list. */
.person-detail-episodes .episodes-section,
.person-detail-episodes .episodes {
  padding-block-start: 0;
}

.person-episode-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Title and role sit on one line while there is room and stack before the
   title has to truncate — an episode title is the thing worth reading. */
.person-episode {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3xs) var(--space-xs);
  min-inline-size: 0;
  padding-block: var(--space-2xs);
  border-block-end: 1px solid var(--border);
}

.person-episode a {
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

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