/* Podcast-site stylesheet.
 *
 * Copyright 2026 Plabayo BV.
 *
 * Authored against guidelines/css_and_some_js.md + website.md:
 * mobile-first, custom-property design tokens, fluid `clamp()` type scale,
 * logical properties throughout, `:focus-visible` rings, light/dark via
 * `prefers-color-scheme`, and all motion gated behind
 * `prefers-reduced-motion: no-preference`. Served immutably + fingerprinted. */

/* ---- Design tokens ------------------------------------------------------ */

:root {
  color-scheme: light dark;

  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* For values that are data rather than prose -- stream keys, ids, transcript
     timings -- where a column that lines up is worth more than a pretty face. */
  --font-mono:
    ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* Comfortable reading measure for prose-heavy podcast pages, plus a wider
     measure for grid-y, scannable pages (the episode list) that should use the
     extra room on desktop instead of stranding it in the margins. */
  --measure: 70ch;
  --measure-wide: 78rem;
  --radius: 10px;
  /* For a control nested inside a rounded box: subtracting its padding keeps
     the two corners concentric instead of chunky (see guidelines/css). */
  --radius-small: calc(var(--radius) - 4px);
  /* Fully rounded ends (a pill, a chip, a track), whatever the height. */
  --radius-pill: 999px;

  /* Motion tokens (one easing, two durations) — see guidelines/animations.md.
     All motion is gated behind `prefers-reduced-motion: no-preference`. */
  --ease-out: cubic-bezier(0.33, 1, 0.68, 1);
  --dur-quick: 120ms;
  --dur-default: 280ms;
  --shadow-soft: 0 4px 12px -6px rgb(0 0 0 / 45%);
  --shadow-card: 0 16px 34px -20px rgb(0 0 0 / 60%);
  --shadow-menu: 0 0.5rem 1.5rem rgb(0 0 0 / 18%);

  /* Spacing scale (one knob per step). */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-s: 1rem;
  --space-m: 1.5rem;
  --space-l: 2.5rem;
  --space-xl: 4rem;
  --space-2xl: 6rem;
  --space-3xl: 9rem;

  /* Fluid type scale: each step interpolates with the viewport, floored at an
     accessible rem base so user font-size preferences are respected. */
  --step--2: clamp(0.73rem, 0.65rem + 0.16vw, 0.75rem);
  --step--1: clamp(0.83rem, 0.78rem + 0.24vw, 0.95rem);
  --step-0: clamp(1rem, 0.93rem + 0.34vw, 1.19rem);
  --step-1: clamp(1.2rem, 1.06rem + 0.68vw, 1.55rem);
  --step-2: clamp(1.44rem, 1.21rem + 1.14vw, 2.03rem);
  --step-3: clamp(1.73rem, 1.36rem + 1.85vw, 2.66rem);
  --step-4: clamp(2.07rem, 1.49rem + 2.9vw, 3.5rem);

  /* Palette: light defaults at `:root`. OS preference + the nav theme toggle
     each layer on top — see the `@media` + `[data-theme]` blocks below.
     (We *used* `light-dark()` here, but axe-core 4.10.2 in CI couldn't resolve
     its computed value, so color-contrast spuriously read `#aeafb2` for every
     text node. Explicit declarations + a `[data-theme]` toggle override work
     everywhere and keep the instant-flip toggle behaviour.) */
  --bg: hsl(40 36% 97%);
  --surface: hsl(40 40% 100%);
  /* A control resting on a `--surface` card: one step away from it, so the
     two read as different planes without a shadow. */
  --surface-raised: hsl(40 34% 95%);
  --text: hsl(222 24% 16%);
  --muted: hsl(222 12% 40%);
  --border: hsl(222 16% 87%);
  --accent: hsl(255 64% 50%);
  --on-accent: hsl(0 0% 100%);
  --focus: hsl(255 86% 58%);

  /* Colour-blind-aware per-speaker transcript palette (AA contrast on this
     scheme; à la Okabe–Ito). Independent of `--accent` so speakers stay
     mutually distinct under any show theme. Dark variants below. */
  --spk-0: #1565c0;
  --spk-1: #c62828;
  --spk-2: #2e7d32;
  --spk-3: #6a1b9a;
  --spk-4: #b35900;
  --spk-5: #ad1457;

  /* Form/validation error text (AA on this scheme). */
  --danger: #c62828;
  --success: #26701f;
  --warning: #975000;

  --min-tap: 32px;

  /* Stacking layers. The painting order, bottom → top:
     * page content (default `auto`, effectively 0)
     * `--z-dock`     — sticky episode-dock (the player bar)
     * `--z-popover`  — popovers anchored to page content (the download
       menu); MUST sit above `--z-dock` because the chip lives in the
       hero `.episode-header` (static, z:auto) while the player dock is
       a sticky sibling — the popover panel extends downward INTO the
       dock's territory and would be painted over by the dock otherwise
     * `--z-header`   — the site banner (always-visible top chrome)
     * `--z-side-nav` — the floating "On this page" jump-nav (pill + open
       panel). Always on top: the pill must stay tappable over the dock, and
       the panel opens UPWARD into the dock's territory, so anything lower lets
       the player bar paint over it. Above the banner too — a bottom-corner
       control never overlaps the top chrome in practice, and an actively-open
       menu should win if it ever does. Dismisses on outside-click / Escape
       (episode-dock.js), so it never lingers over the page. */
  --z-dock: 25;
  --z-popover: 26;
  --z-header: 30;
  --z-side-nav: 40;
}

/* OS prefers dark — unless the user explicitly forced "light" via the toggle. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: hsl(230 18% 11%);
    --surface: hsl(230 16% 16%);
    --surface-raised: hsl(230 15% 22%);
    --text: hsl(220 24% 92%);
    --muted: hsl(220 14% 66%);
    --border: hsl(230 12% 27%);
    --accent: hsl(255 90% 80%);
    --on-accent: hsl(230 34% 12%);
    --focus: hsl(255 92% 80%);
    --spk-0: #64b5f6;
    --spk-1: #ef9a9a;
    --spk-2: #81c784;
    --spk-3: #ce93d8;
    --spk-4: #ffb74d;
    --spk-5: #f48fb1;
    --danger: #ef9a9a;
    --success: #81c784;
    --warning: #ffb74d;
  }
}

/* Toggle override (theme-init.js sets data-theme + color-scheme on <html>):
   user's explicit choice always wins over the OS preference. */
:root[data-theme="dark"] {
  --bg: hsl(230 18% 11%);
  --surface: hsl(230 16% 16%);
  --surface-raised: hsl(230 15% 22%);
  --text: hsl(220 24% 92%);
  --muted: hsl(220 14% 66%);
  --border: hsl(230 12% 27%);
  --accent: hsl(255 90% 80%);
  --on-accent: hsl(230 34% 12%);
  --focus: hsl(255 92% 80%);
  --spk-0: #64b5f6;
  --spk-1: #ef9a9a;
  --spk-2: #81c784;
  --spk-3: #ce93d8;
  --spk-4: #ffb74d;
  --spk-5: #f48fb1;
  --danger: #ef9a9a;
  --success: #81c784;
  --warning: #ffb74d;
}

/* Coarse pointers (touch) get larger hit targets — WCAG 2.2 target size. */
@media (pointer: coarse) {
  :root {
    --min-tap: 44px;
  }
}

/* ---- Reset (low-specificity) ------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* `hidden` means hidden. The attribute is only a user-agent `display: none`,
   so any component that sets its own `display` (a flex error box, a grid
   wait) silently shows what the markup hid -- which is how a placeholder once
   announced "this part didn't load" before it had even tried. One rule here
   instead of an `[hidden]` override per component. */
[hidden] {
  /* biome-ignore lint/complexity/noImportantStyles: the attribute has to beat any component's own display, whatever its specificity */
  display: none !important;
}

:where(body, h1, h2, h3, p, ul, ol, figure) {
  margin: 0;
}

:where(ul[class]) {
  padding: 0;
  list-style: none;
}

img,
audio {
  display: block;
  max-inline-size: 100%;
}

/* ---- Base --------------------------------------------------------------- */

/* Reserve the scrollbar gutter so content doesn't shift sideways when a page
   grows past one viewport and the scrollbar appears (no effect under overlay
   scrollbars; matters on classic Windows/Linux scrollbars). */
html {
  scrollbar-gutter: stable;
}

body {
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.6;
  color: var(--text);
  background: var(--bg);
  /* Sticky footer without layout-thrash: column flex, main grows. */
  display: flex;
  flex-direction: column;
  min-block-size: 100vh;
  min-block-size: 100dvh;
}

:where(h1, h2, h3) {
  line-height: 1.15;
  text-wrap: balance;
}

h1 {
  font-size: var(--step-3);
}

h2 {
  font-size: var(--step-1);
}

p {
  text-wrap: pretty;
}

:where(main p, .show-notes) {
  margin-block-end: var(--space-s);
}

a {
  color: var(--accent);
}

@media (hover: hover) and (pointer: fine) {
  a:hover {
    text-decoration: underline;
  }
}

/* Controls keep their labels stable during repeated clicks and long presses.
   Reading content and editable fields retain native text selection. */
:where(button, summary, input[type="button"], input[type="submit"], input[type="reset"]) {
  -webkit-user-select: none;
  user-select: none;
}

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius);
}

/* ---- A11y utilities ----------------------------------------------------- */

/* Skip link: visually hidden until focused (no motion). */
.skip-link:not(:focus) {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link:focus {
  position: absolute;
  inset-block-start: var(--space-2xs);
  inset-inline-start: var(--space-2xs);
  z-index: 10;
  padding: var(--space-2xs) var(--space-s);
  color: var(--on-accent);
  background: var(--accent);
  border-radius: var(--radius);
}

/* Visually hidden, but available to assistive tech (labels, headings).
   `.visually-hidden` is the same utility under the name the deferred-section
   markup uses. */
.sr-only,
.visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- Show accent presets ------------------------------------------------ */

/* Every preset's accent pair, selected by `<html data-accent>`.
   These used to be a per-show `/theme.css` response, which was render-blocking
   *and* served `no-cache` — so every page view paid a revalidation round trip
   before first paint for ~250 bytes. `Theme` is a closed set of five presets and
   `Theme::css()` is a pure function of the variant, so shipping all five here
   (inside a stylesheet the page already fetches and caches) costs a few hundred
   compressed bytes and removes that request entirely.
   Violet is the `:root` default above, so an unset `data-accent` is already
   correct. `theme::tests` asserts these values stay in step with
   `Theme::palette()`, which remains the single source of truth.
   Specificity note: `:root[data-accent=…]` (0,1,1) outranks the bare `:root`
   defaults (0,1,0) regardless of source order, and mirrors the same
   light / prefers-dark / forced-dark layering used above. */
:root[data-accent="violet"] {
  --accent: hsl(255 64% 50%);
  --focus: hsl(255 86% 58%);
}
:root[data-accent="amber"] {
  --accent: hsl(28 90% 36%);
  --focus: hsl(28 92% 48%);
}
:root[data-accent="teal"] {
  --accent: hsl(184 80% 27%);
  --focus: hsl(184 84% 36%);
}
:root[data-accent="rose"] {
  --accent: hsl(344 78% 46%);
  --focus: hsl(344 84% 52%);
}
:root[data-accent="sky"] {
  --accent: hsl(208 84% 42%);
  --focus: hsl(208 88% 48%);
}

@media (prefers-color-scheme: dark) {
  :root[data-accent="violet"]:not([data-theme="light"]) {
    --accent: hsl(255 90% 80%);
    --focus: hsl(255 92% 80%);
  }
  :root[data-accent="amber"]:not([data-theme="light"]) {
    --accent: hsl(34 95% 62%);
    --focus: hsl(34 95% 66%);
  }
  :root[data-accent="teal"]:not([data-theme="light"]) {
    --accent: hsl(176 70% 60%);
    --focus: hsl(176 75% 64%);
  }
  :root[data-accent="rose"]:not([data-theme="light"]) {
    --accent: hsl(344 90% 75%);
    --focus: hsl(344 92% 78%);
  }
  :root[data-accent="sky"]:not([data-theme="light"]) {
    --accent: hsl(204 90% 70%);
    --focus: hsl(204 92% 74%);
  }
}

:root[data-accent="violet"][data-theme="dark"] {
  --accent: hsl(255 90% 80%);
  --focus: hsl(255 92% 80%);
}
:root[data-accent="amber"][data-theme="dark"] {
  --accent: hsl(34 95% 62%);
  --focus: hsl(34 95% 66%);
}
:root[data-accent="teal"][data-theme="dark"] {
  --accent: hsl(176 70% 60%);
  --focus: hsl(176 75% 64%);
}
:root[data-accent="rose"][data-theme="dark"] {
  --accent: hsl(344 90% 75%);
  --focus: hsl(344 92% 78%);
}
:root[data-accent="sky"][data-theme="dark"] {
  --accent: hsl(204 90% 70%);
  --focus: hsl(204 92% 74%);
}
