/* =============================================================================
   site.css — the whole design system, one file, no build step.
   Owned by UI. Served directly; cache-busted by ?v={{ build_id }} in base.html.

   The system is "Letters" (see web/DESIGN_SYSTEM.md, which is the spec and
   wins any disagreement with this file's comments): 99% monochrome, a white
   canvas, near-black pill actions, 18px cards on barely-visible blue-tinted
   shadows. One blue (--c-signal) marks icons, focus and micro-graphics and is
   never text. The sky gradient (--g-sky) belongs to the landing hero only.

   Organised with @layer so specificity never becomes an argument: a component
   rule always beats a base rule regardless of selector weight, and a utility
   always beats a component.

   Colour lives entirely in custom properties on :root. Light mode is the only
   theme that ships (UI_UX_BRIEF § Colour); dark mode is a second :root block,
   not a redesign — that is the whole reason no colour literal appears below
   the tokens layer.
   ============================================================================= */

@layer tokens, base, layout, components, utilities;

/* -----------------------------------------------------------------------------
   TOKENS
   -------------------------------------------------------------------------- */
@layer tokens {
  :root {
    /* --- neutral scale -------------------------------------------------- */
    --c-bg: #ffffff; /* Paper White */
    --c-surface: #f5f5f5; /* Cloud Gray: footer, ad zones, alternating bands */
    --c-surface-2: #efeff1; /* pressed / hover surface */
    --c-surface-3: #e8e8ec; /* meter track, skeletons, disabled fill */

    /* Text tones, measured on --c-bg (#fff) and on --c-surface (#f5f5f5):
       Obsidian  #070709 20.1:1 · 18.5:1
       text-2    #3f3f48 10.4:1 ·  9.6:1
       Charcoal  #60606c  6.2:1 ·  5.7:1   (supporting copy)
       Slate     #8b8b8b  3.4:1 ·  3.1:1   DISABLED AND DECORATION ONLY —
                                           it fails AA as text (deviation 3). */
    --c-text: #070709;
    --c-text-2: #3f3f48;
    --c-text-muted: #60606c;
    --c-text-disabled: #8b8b8b;
    --c-text-invert: #ffffff;

    /* Three border weights on purpose. Decorative edges may be faint
       (#ececf0, 1.2:1); the dropzone's dashed line and the ghost button's line
       are a step up (#bebecc, 1.8:1) and never the only boundary of a control
       whose label is not inside it; anything that outlines an input must
       clear WCAG 1.4.11's 3:1, which --c-border-strong (#8e8e99, 3.2:1 on
       white) does. */
    --c-border: #ececf0;
    --c-border-hairline: #bebecc;
    --c-border-strong: #8e8e99;

    /* --- the action colour ---------------------------------------------- */
    /* Obsidian. White on it is 20.1:1, on its hover (#2a2a30) 14.3:1, and it
       is also the link colour, where the underline carries the "this is a
       link" signal rather than the hue. Used for: primary button, selected
       state, links. */
    --c-accent: #070709;
    --c-accent-strong: #2a2a30;
    /* Sky Tint: selected highlights and ::selection. Obsidian on it 15.8:1. */
    --c-accent-weak: #d7e6f5;

    /* Surgical Blue. 3.3:1 on white: passes the 3:1 non-text bar, fails the
       4.5:1 text bar. Icons, focus rings and micro-graphics. NEVER text
       (deviation 4). */
    --c-signal: #2597d0;

    /* --- semantic -------------------------------------------------------- */
    /* Unchanged AA-checked pairs, except --c-info, which moves into the blue
       family: #176bc5 is 5.3:1 on white and 4.9:1 on --c-info-bg. */
    --c-success: #15803d;
    --c-success-bg: #f0fdf4;
    --c-warn: #b45309;
    --c-warn-bg: #fffbeb;
    --c-error: #b91c1c;
    --c-error-bg: #fef2f2;
    --c-info: #176bc5;
    --c-info-bg: #eff6ff;

    --c-focus: #2597d0;

    /* The hero band, and nowhere else. It ends in white so it fades into the
       page instead of stopping at a seam (deviation 8). Obsidian on its darkest
       stop is 6.9:1, which is why the hero headline is not white
       (deviation 1). */
    --g-sky: linear-gradient(180deg, #779bc1 0%, #9abfda 45%, #cbdcec 78%, #ffffff 100%);

    /* --- type ------------------------------------------------------------ */
    /* Open Runde, self-hosted (font-src 'self' forbids a CDN; deviation 7).
       Latin subset, 400/500/600. The system stack is the fallback, so a font
       that fails to load costs a flash and never breaks layout. */
    --font-sans:
      "Open Runde", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
      sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
    --font-mono:
      ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

    /* The scale. NOTHING is below 14px, and every input stays at 16px: iOS
       Safari zooms a focused field whose computed font-size is under 16px,
       so .input and .select pin themselves to max(1rem, …) and never read
       --fs-caption (deviation 2). */
    --fs-display: clamp(2.5rem, 5.2vw + 1rem, 5rem); /* 40→80px, landing H1 */
    --fs-title: clamp(2rem, 2.4vw + 1.25rem, 2.75rem); /* 32→44px, inner H1 */
    --fs-heading: 1.75rem; /* 28px, H2 */
    --fs-head: 1.25rem; /* 20px, H3 and the wordmark */
    --fs-body-lg: 1.125rem; /* 18px */
    --fs-body: 1rem; /* 16px */
    --fs-caption: 0.875rem; /* 14px — the floor */

    --lh-display: 0.95;
    --lh-title: 1.1;
    --lh-tight: 1.2;
    --lh-snug: 1.4;
    --lh-body: 1.5;

    /* Tracking: tight on anything 28px and up, a hair tight on body, normal at
       14px. */
    --ls-tight: -0.04em;
    --ls-body: -0.01em;

    --fw-normal: 400;
    --fw-medium: 500;
    /* 600, not 700: only 400/500/600 ship, and asking for 700 makes the
       browser synthesise a smeared faux-bold from the 600 face. */
    --fw-bold: 600;

    /* --- space ----------------------------------------------------------- */
    --sp-1: 0.25rem;
    --sp-2: 0.5rem;
    --sp-3: 0.75rem;
    --sp-4: 1rem;
    --sp-5: 1.5rem;
    --sp-6: 2rem;
    --sp-7: 3rem;
    --sp-8: 4rem;
    --sp-9: 6rem;

    /* --- shape ----------------------------------------------------------- */
    --r-sm: 8px; /* icons, small chips inside cards */
    --r-md: 12px; /* inputs, selects, dropzone, banners */
    --r-lg: 18px; /* cards */
    --r-xl: 32px; /* the landing transcribe card and other hero cards */
    --r-pill: 100px; /* buttons, tags, nav pills, badges */

    /* The primary button is a pill now (deviation 5). The old flat slab was an
       ad defence; the defence lives in colour and position instead — a
       monochrome near-black pill looks nothing like the green, orange and blue
       fake-download creatives, and ad slots stay labelled and outside the
       action path (both tested in web/test/pages.test.mjs). */
    --btn-radius: var(--r-pill);
    --btn-underline: 0;

    /* --- layout ---------------------------------------------------------- */
    --w-page: 1200px;
    --w-content: 760px;
    --w-prose: 65ch;
    --w-sidebar: 300px;
    /* Below this the desktop sidebar (ad zone 3) does not exist at all. */
    --bp-sidebar: 1200px;
    /* The sticky header's height: 44px tap target + 2 × 12px padding + 1px
       rule. In-page anchors and other sticky things offset by it. */
    --header-h: 4.3125rem;

    /* WCAG 2.2 SC 2.5.8 requires 24x24; the mobile-first rule in the UI brief
       requires 44. 44 wins because it is the stricter of the two. */
    --tap: 44px;

    /* --- elevation ------------------------------------------------------- */
    /* All tinted, none grey: the reference's shadows are blue-black and so
       faint they read as air rather than as a drop. */
    --shadow-hairline: rgb(228 229 231 / 24%) 0 1px 2px;
    /* The primary button's four-layer stack. */
    --shadow-1:
      rgb(36 36 40 / 10%) 0 1px 2px, rgb(36 36 40 / 9%) 0 3px 3px, rgb(36 36 40 / 5%) 0 6px 4px,
      rgb(36 36 40 / 1%) 0 11px 4px;
    /* Feature and hero cards: atmospheric, not a drop shadow. */
    --shadow-2:
      rgb(16 55 132 / 3%) 0 17px 37px, rgb(16 55 132 / 3%) 0 67px 67px,
      rgb(16 55 132 / 2%) 0 150px 90px;
    --shadow-signal: rgb(23 107 197 / 8%) 0 1px 1px, rgb(23 107 197 / 7%) 0 3px 3px;

    --dur-fast: 120ms;
    --dur: 200ms;
    --ease: cubic-bezier(0.2, 0, 0.2, 1);
  }
}

/* -----------------------------------------------------------------------------
   BASE
   -------------------------------------------------------------------------- */
@layer base {
  /* Open Runde, self-hosted. The URLs are relative to THIS file, so they
     resolve to /static/base/fonts/ whatever query string the stylesheet was
     loaded with. base.html preloads 400 and 600 by the same absolute path —
     a preload whose URL differs by so much as a query string is a second
     download, not a head start. */
  @font-face {
    font-family: "Open Runde";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/open-runde-latin-400-normal.woff2") format("woff2");
  }
  @font-face {
    font-family: "Open Runde";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("../fonts/open-runde-latin-500-normal.woff2") format("woff2");
  }
  @font-face {
    font-family: "Open Runde";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("../fonts/open-runde-latin-600-normal.woff2") format("woff2");
  }

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

  html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    /* The header is sticky, so an in-page jump (#main, an FAQ anchor) would
       land underneath it without this. */
    scroll-padding-top: calc(var(--header-h) + var(--sp-4));
    /* Keeps the page from jumping sideways when a modal removes the
       scrollbar. */
    scrollbar-gutter: stable;
  }

  /* Cross-document view transitions: two declarations, no JavaScript, and
     engines without them simply navigate. Only the header and the tool card
     are named — naming many elements is where this gets slow on a mid-range
     phone, and the site's value is the tool, not the animation. */
  @view-transition {
    navigation: auto;
  }
  @media (prefers-reduced-motion: reduce) {
    @view-transition {
      navigation: none;
    }
  }
  /* A view-transition-name must be unique in the document — two elements
     sharing one skips the transition entirely — so these are opt-in
     attributes a template sets on exactly one element, not classes. */
  [data-vt="header"] {
    view-transition-name: site-header;
  }
  [data-vt="card"] {
    view-transition-name: tool-card;
  }

  body {
    margin: 0;
    background: var(--c-bg);
    color: var(--c-text);
    font-family: var(--font-sans);
    font-size: var(--fs-body);
    line-height: var(--lh-body);
    letter-spacing: var(--ls-body);
    /* Only 400/500/600 ship. A stray `bold` must pick the 600 face, not
       smear a synthetic weight across it. */
    font-synthesis-weight: none;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Room for the mobile anchor ad + the sticky action bar, both of which are
       fixed to the bottom. Pages that use neither simply have 0 here. */
    padding-bottom: calc(var(--page-bottom-inset, 0px) + env(safe-area-inset-bottom, 0px));
  }

  h1,
  h2,
  h3 {
    color: var(--c-text);
    line-height: var(--lh-tight);
    margin: 0 0 var(--sp-3);
    font-weight: var(--fw-bold);
    text-wrap: balance;
  }
  h1 {
    font-size: var(--fs-title);
    line-height: var(--lh-title);
    letter-spacing: var(--ls-tight);
    margin-bottom: var(--sp-4);
  }
  h2 {
    font-size: var(--fs-heading);
    line-height: var(--lh-tight);
    letter-spacing: var(--ls-tight);
  }
  h3 {
    font-size: var(--fs-head);
    line-height: var(--lh-snug);
    letter-spacing: -0.02em;
  }

  strong,
  b {
    font-weight: var(--fw-bold);
  }

  p,
  ul,
  ol {
    margin: 0 0 var(--sp-4);
  }
  p {
    text-wrap: pretty;
  }

  /* Links are Obsidian with an underline: the underline is the signal, not
     the hue, because the one blue on the site fails AA as text. */
  a {
    color: var(--c-accent);
    text-decoration-line: underline;
    text-decoration-thickness: 1px;
    text-decoration-color: color-mix(in srgb, currentcolor 45%, transparent);
    text-underline-offset: 0.18em;
    transition:
      color var(--dur-fast) var(--ease),
      text-decoration-color var(--dur-fast) var(--ease);
  }
  a:hover {
    color: var(--c-accent-strong);
    text-decoration-color: currentcolor;
    text-decoration-thickness: 2px;
  }

  img,
  svg {
    max-width: 100%;
    height: auto;
    vertical-align: middle;
  }

  code,
  kbd,
  samp {
    font-family: var(--font-mono);
    /* Relative, with a floor: 0.9375em of 14px text would be 13.1px, and
       nothing on the site goes below 14px. */
    font-size: max(0.875rem, 0.9375em);
    letter-spacing: 0;
  }

  hr {
    border: 0;
    border-top: 1px solid var(--c-border);
    margin: var(--sp-6) 0;
  }

  /* One focus treatment, everywhere. Two rings so it stays visible against a
     white page AND against an Obsidian fill — WCAG 2.2 SC 2.4.13 wants a 2px
     perimeter at 3:1 against what is next to it. Surgical Blue is 3.3:1 on
     white; the inner white ring is what separates it from a dark pill. */
  :focus-visible {
    outline: 3px solid var(--c-focus);
    outline-offset: 2px;
    box-shadow: 0 0 0 2px var(--c-bg);
    border-radius: var(--r-sm);
  }
  /* A pill's ring follows the pill. */
  .btn:focus-visible,
  .chip:focus-visible,
  .badge:focus-visible,
  .navmenu__button:focus-visible,
  .site-nav__cta:focus-visible {
    border-radius: var(--r-pill);
  }
  /* Older engines that do not support :focus-visible still get :focus. */
  :focus:not(:focus-visible) {
    outline: none;
    box-shadow: none;
  }

  ::selection {
    background: var(--c-accent-weak);
    color: var(--c-text);
  }
}

/* -----------------------------------------------------------------------------
   LAYOUT
   -------------------------------------------------------------------------- */
@layer layout {
  .skip-link {
    position: absolute;
    left: var(--sp-2);
    top: -100%;
    z-index: 100;
    padding: var(--sp-3) var(--sp-5);
    background: var(--c-accent);
    color: var(--c-text-invert);
    border-radius: var(--r-pill);
    font-weight: var(--fw-medium);
    text-decoration: none;
  }
  .skip-link:focus {
    top: var(--sp-2);
  }

  /* The shell is page-wide (--w-page) and its one default column is the
     reading/tool column (--w-content), centred. A page that wants to use the
     full width (the landing's sections, a full-bleed hero) widens its own
     column; nothing else has to know. */
  .shell {
    display: grid;
    grid-template-columns: minmax(0, var(--w-content));
    justify-content: center;
    gap: var(--sp-6);
    max-width: var(--w-page);
    margin: 0 auto;
    padding: 0 var(--sp-4);
  }
  @media (min-width: 600px) {
    .shell {
      padding: 0 var(--sp-5);
    }
  }

  /* Ad zone 3 is a desktop-only column. It is added by a media query rather
     than hidden by one, so below 1200px the element is not merely invisible —
     it is not in the grid, and content never reflows when it loads. */
  @media (min-width: 1200px) {
    .shell--sidebar {
      grid-template-columns: minmax(0, var(--w-content)) var(--w-sidebar);
      gap: var(--sp-7);
      align-items: start;
    }
  }

  /* --- header ------------------------------------------------------------ */
  /* Sticky and translucent: white at 85% over a blur, so content passing
     underneath reads as frosted rather than as a collision. z-index 50 sits
     above the fixed anchor ad (40) and action bar (45) so the open Convert
     panel covers them, and below the toast region (60). */
  .site-header {
    position: sticky;
    top: 0;
    z-index: 50;
    border-bottom: 1px solid var(--c-border);
    background: rgb(255 255 255 / 85%);
    -webkit-backdrop-filter: saturate(180%) blur(12px);
    backdrop-filter: saturate(180%) blur(12px);
  }
  .site-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    max-width: var(--w-page);
    margin: 0 auto;
    padding: var(--sp-3) var(--sp-4);
    min-height: var(--tap);
  }
  @media (min-width: 600px) {
    .site-header__inner {
      padding-inline: var(--sp-5);
    }
  }
  .wordmark {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    font-size: var(--fs-head);
    font-weight: var(--fw-bold);
    letter-spacing: var(--ls-tight);
    color: var(--c-text);
    text-decoration: none;
    white-space: nowrap;
  }
  .wordmark:hover {
    color: var(--c-text);
  }
  .site-nav {
    display: flex;
    gap: var(--sp-2);
    align-items: center;
  }
  /* Plain nav links (the founder's `items` and the "Tools" fallback) are
     ghost pills without the outline: quiet until hovered. */
  .site-nav a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0 var(--sp-4);
    border-radius: var(--r-pill);
    color: var(--c-text);
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--dur-fast) var(--ease);
  }
  .site-nav a:hover {
    background: var(--c-surface-2);
    color: var(--c-text);
  }

  /* The filled "Transcribe a video" pill. NOT .btn--primary: the landing
     page is tested to carry exactly one primary button in its flow, and this
     one is hidden there anyway (it would point at the page you are on). Also
     hidden on phones, where the wordmark is the way home and the header has
     room for the wordmark and one pill, no more, at 360px. */
  .site-nav .site-nav__cta {
    display: none;
    background: var(--c-accent);
    color: var(--c-text-invert);
    box-shadow: var(--shadow-1);
    padding: 0 var(--sp-5);
  }
  .site-nav .site-nav__cta:hover {
    background: var(--c-accent-strong);
    color: var(--c-text-invert);
  }
  @media (min-width: 640px) {
    .site-nav .site-nav__cta {
      display: inline-flex;
    }
    body[data-page="landing"] .site-nav .site-nav__cta {
      display: none;
    }
  }

  /* --- the header's Convert menu ---------------------------------------- */
  /* A `<details>` and a positioned panel. It opens with no JavaScript, which
     is why it is this element and not a scripted button; `boot.ts` adds only
     Escape and click-away on top. The summary is a ghost nav pill; the panel
     is a card. */
  .navmenu {
    position: relative;
  }
  .navmenu__button {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: var(--tap);
    padding: 0 var(--sp-4);
    border: 1px solid var(--c-border-hairline);
    border-radius: var(--r-pill);
    color: var(--c-text);
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition:
      border-color var(--dur-fast) var(--ease),
      background var(--dur-fast) var(--ease);
    /* The default disclosure triangle is a browser glyph in a browser's idea of
       a size. Replaced by the caret below so the control reads as a menu. */
    list-style: none;
  }
  .navmenu__button::-webkit-details-marker {
    display: none;
  }
  .navmenu__button::after {
    content: "";
    width: 0.4em;
    height: 0.4em;
    border-right: 1.5px solid var(--c-signal);
    border-bottom: 1.5px solid var(--c-signal);
    transform: translateY(-0.15em) rotate(45deg);
    transition: transform var(--dur-fast) var(--ease);
  }
  .navmenu[open] .navmenu__button::after {
    transform: translateY(0.1em) rotate(-135deg);
  }
  .navmenu__button:hover,
  .navmenu[open] .navmenu__button {
    border-color: var(--c-text);
  }
  .navmenu[open] .navmenu__button {
    background: var(--c-surface);
  }

  .navmenu__panel {
    position: absolute;
    z-index: 40;
    top: calc(100% + var(--sp-2));
    right: 0;
    /* Anchored to the right edge of the button and clamped to the viewport, so
       the panel cannot hang off the side of a narrow screen — the button sits
       at the right end of the header, which is exactly where an unclamped
       left-aligned panel would overflow. */
    max-width: calc(100vw - var(--sp-4) * 2);
    width: max-content;
    display: grid;
    gap: var(--sp-4);
    padding: var(--sp-5);
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-2), var(--shadow-hairline);
    /* Twenty-three links will not fit on a phone in portrait. Scrolling the
       panel keeps every tool reachable instead of pushing the last group off
       the bottom of the screen with no way to get to it. */
    max-height: min(70vh, 32rem);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  @media (min-width: 700px) {
    .navmenu__panel {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: var(--sp-5) var(--sp-7);
      padding: var(--sp-6);
    }
  }
  .navmenu__heading {
    margin: 0 0 var(--sp-1);
    font-size: var(--fs-caption);
    font-weight: var(--fw-bold);
    letter-spacing: 0;
    color: var(--c-text-muted);
  }
  .navmenu__list {
    list-style: none;
    margin: 0;
    padding: 0;
  }
  /* Two selectors deep so it outranks `.site-nav a` inside the same layer:
     menu entries are rows, not pills. */
  .navmenu .navmenu__list a {
    display: flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0 var(--sp-3);
    margin-inline: calc(var(--sp-3) * -1);
    border-radius: var(--r-sm);
    font-size: var(--fs-body);
    font-weight: var(--fw-normal);
    letter-spacing: var(--ls-body);
    line-height: var(--lh-tight);
    white-space: normal;
  }
  .navmenu .navmenu__list a:hover {
    background: var(--c-surface);
  }
  .navmenu .navmenu__all {
    grid-column: 1 / -1;
    display: flex;
    min-height: var(--tap);
    padding: var(--sp-3) 0 0;
    border-top: 1px solid var(--c-border);
    border-radius: 0;
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, currentcolor 45%, transparent);
    text-underline-offset: 0.18em;
  }
  .navmenu .navmenu__all:hover {
    background: none;
    text-decoration-color: currentcolor;
  }

  main {
    display: block;
    padding: var(--sp-6) 0 var(--sp-8);
  }

  /* --- footer ------------------------------------------------------------ */
  /* Cloud Gray, a four-column tool index, and plenty of air. */
  .site-footer {
    margin-top: var(--sp-8);
    border-top: 1px solid var(--c-border);
    background: var(--c-surface);
    padding: var(--sp-8) var(--sp-4) var(--sp-6);
    color: var(--c-text-muted);
    font-size: var(--fs-caption);
    letter-spacing: 0;
  }
  @media (min-width: 600px) {
    .site-footer {
      padding-inline: var(--sp-5);
    }
  }
  .site-footer a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    color: var(--c-text-muted);
    text-decoration: none;
  }
  .site-footer a:hover {
    color: var(--c-text);
    text-decoration: underline;
    text-decoration-thickness: 1px;
  }
  .site-footer__inner {
    max-width: var(--w-page);
    margin: 0 auto;
    padding-top: var(--sp-5);
    border-top: 1px solid var(--c-border);
    display: flex;
    flex-wrap: wrap;
    gap: 0 var(--sp-5);
    align-items: center;
  }

  /* The same links, flat and open. Two spots: the header menu for the person,
     this for the crawler and for anyone who has read to the end of a page. */
  .site-footer__tools {
    max-width: var(--w-page);
    margin: 0 auto var(--sp-7);
    display: grid;
    gap: var(--sp-6) var(--sp-5);
    grid-template-columns: minmax(0, 1fr);
  }
  @media (min-width: 520px) {
    .site-footer__tools {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }
  @media (min-width: 960px) {
    .site-footer__tools {
      grid-template-columns: repeat(4, minmax(0, 1fr));
    }
  }
  .site-footer__heading {
    margin: 0 0 var(--sp-2);
    font-size: var(--fs-caption);
    font-weight: var(--fw-bold);
    color: var(--c-text);
  }
  .site-footer__tools ul {
    list-style: none;
    margin: 0;
    padding: 0;
  }
  /* 44px rows: these are the only links on a phone's last screen, and a
     thumb is not a cursor. */
  .site-footer__tools a {
    display: flex;
    min-height: var(--tap);
    line-height: var(--lh-tight);
  }

  .stack > * + * {
    margin-top: var(--sp-4);
  }
  .stack-lg > * + * {
    margin-top: var(--sp-6);
  }
  .prose {
    max-width: var(--w-prose);
  }
}

/* -----------------------------------------------------------------------------
   COMPONENTS
   -------------------------------------------------------------------------- */
@layer components {
  /* --- buttons --------------------------------------------------------- */
  /* Every button is a pill. A bare `.btn` is the secondary: a transparent
     ghost with a hairline that darkens to Obsidian on hover. */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    min-height: var(--tap);
    padding: var(--sp-3) var(--sp-5);
    border: 1px solid var(--c-border-hairline);
    border-radius: var(--btn-radius);
    background: transparent;
    color: var(--c-text);
    font: inherit;
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    line-height: var(--lh-tight);
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition:
      background var(--dur-fast) var(--ease),
      border-color var(--dur-fast) var(--ease),
      color var(--dur-fast) var(--ease),
      transform var(--dur-fast) var(--ease);
  }
  .btn:hover {
    border-color: var(--c-text);
    color: var(--c-text);
  }

  /* THE action. Obsidian pill, white 500 text, at least 48px tall, on the
     four-layer shadow. Hover lightens to --c-accent-strong; active drops 1px. */
  .btn--primary {
    min-height: 48px;
    padding: 14px var(--sp-5);
    background: var(--c-accent);
    color: var(--c-text-invert);
    border-color: var(--c-accent);
    box-shadow: var(--shadow-1);
    font-weight: var(--fw-medium);
  }
  .btn--primary:hover {
    background: var(--c-accent-strong);
    border-color: var(--c-accent-strong);
    color: var(--c-text-invert);
  }
  .btn--primary:active {
    transform: translateY(1px);
    box-shadow: var(--shadow-hairline);
  }

  /* The neutral variant. Named rather than implicit so a template never has to
     know that a bare `.btn` is the secondary style. */
  .btn--secondary {
    background: transparent;
    color: var(--c-text);
  }

  .btn--ghost {
    border-color: transparent;
    background: transparent;
    color: var(--c-text);
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, currentcolor 45%, transparent);
    text-underline-offset: 0.18em;
  }
  .btn--ghost:hover {
    border-color: transparent;
    background: var(--c-surface-2);
  }

  .btn--block {
    width: 100%;
  }

  .btn[disabled],
  .btn[aria-disabled="true"] {
    cursor: not-allowed;
    pointer-events: none;
    border-color: var(--c-border);
    color: var(--c-text-muted);
    box-shadow: none;
    transform: none;
  }
  /* Disabled primary: a Cloud-fill pill with Charcoal text (5.1:1 on
     --c-surface-3) and no shadow. Not opacity — a faded Obsidian pill reads
     as "loading", not as "not yet". */
  .btn--primary[disabled],
  .btn--primary[aria-disabled="true"] {
    background: var(--c-surface-3);
    border-color: var(--c-surface-3);
    color: var(--c-text-muted);
  }
  /* aria-disabled links stay focusable (a disabled control the keyboard cannot
     reach cannot explain why it is disabled), so the pointer-events kill above
     is deliberately reversed for them. */
  a.btn[aria-disabled="true"],
  .chip[aria-disabled="true"] {
    pointer-events: auto;
  }

  /* Busy state: the label is swapped by JS, never the geometry — a button that
     resizes when clicked moves whatever is under it. */
  .btn[data-busy="true"] {
    cursor: progress;
  }

  /* --- the card --------------------------------------------------------- */
  /* White, 18px, a hairline edge and the atmospheric blue-tinted shadow.
     Nothing may be inserted inside this element. The ad-slot structural test
     asserts it. Hero cards (the landing transcribe card) take --r-xl in their
     own page rules. */
  .card {
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-2);
    padding: var(--sp-5);
  }
  @media (min-width: 600px) {
    .card {
      padding: 2.5rem;
    }
  }

  /* --- fields ------------------------------------------------------------ */
  .field {
    display: block;
  }
  .field__label {
    display: block;
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    color: var(--c-text);
    margin-bottom: var(--sp-2);
  }
  .field__hint {
    font-size: var(--fs-caption);
    letter-spacing: 0;
    color: var(--c-text-muted);
    margin-top: var(--sp-2);
  }

  .input,
  .select {
    width: 100%;
    min-height: 48px;
    padding: var(--sp-3) var(--sp-4);
    /* 1rem, never less: below 16px iOS zooms the viewport on focus and the
       one-action layout is destroyed. */
    font: inherit;
    font-size: max(1rem, var(--fs-body));
    letter-spacing: var(--ls-body);
    color: var(--c-text);
    background: var(--c-bg);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--r-md);
    transition: border-color var(--dur-fast) var(--ease);
  }
  .input:hover,
  .select:hover {
    border-color: var(--c-text-2);
  }
  .input::placeholder {
    color: var(--c-text-muted);
    opacity: 1;
  }
  .input:user-invalid,
  .input[aria-invalid="true"] {
    border-color: var(--c-error);
  }
  .input:disabled,
  .select:disabled {
    background: var(--c-surface);
    border-color: var(--c-border-hairline);
    color: var(--c-text-muted);
    cursor: not-allowed;
  }

  /* URL field with the Paste button inside it. Implemented as a flex row that
     LOOKS like one control rather than an absolutely-positioned button over an
     input, so the button never overlaps text and the field never needs
     hard-coded right padding. The affix is a small ghost pill inset in the
     field, not a grey slab bolted onto its end. */
  .input-group {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    min-height: 48px;
    padding-right: var(--sp-1);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--r-md);
    background: var(--c-bg);
    overflow: hidden;
    transition: border-color var(--dur-fast) var(--ease);
  }
  .input-group:hover {
    border-color: var(--c-text-2);
  }
  .input-group:focus-within {
    outline: 3px solid var(--c-focus);
    outline-offset: 2px;
  }
  .input-group .input {
    align-self: stretch;
    border: 0;
    border-radius: 0;
    min-width: 0;
    flex: 1 1 auto;
  }
  .input-group .input:focus {
    outline: none;
  }
  .input-group__affix {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-1);
    flex: 0 0 auto;
    min-height: var(--tap);
    min-width: var(--tap);
    padding: 0 var(--sp-4);
    border: 1px solid var(--c-border);
    border-radius: var(--r-pill);
    background: var(--c-surface);
    color: var(--c-text);
    font: inherit;
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    cursor: pointer;
    transition:
      background var(--dur-fast) var(--ease),
      border-color var(--dur-fast) var(--ease);
  }
  .input-group__affix:hover {
    background: var(--c-surface-2);
    border-color: var(--c-border-hairline);
  }
  .input-group__affix svg {
    color: var(--c-signal);
  }
  .input-group__affix:focus-visible {
    border-radius: var(--r-pill);
    outline-offset: -3px;
  }

  /* --- badges, tags and chips ------------------------------------------- */
  /* One pill shape: 14px 500 text, a hairline edge. An icon inside one is the
     signal blue. Text is never the signal blue. */
  .badge {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: var(--sp-1) var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--r-pill);
    background: var(--c-bg);
    color: var(--c-text-2);
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    line-height: var(--lh-snug);
  }
  .badge svg,
  .chip svg {
    color: var(--c-signal);
  }
  /* Sky Tint with Obsidian text (15.8:1). It used to carry the info blue as
     text, which is 4.2:1 on this fill. */
  .badge--accent {
    background: var(--c-accent-weak);
    border-color: transparent;
    color: var(--c-text);
    box-shadow: var(--shadow-signal);
  }
  .badge--word {
    background: var(--c-success-bg);
    border-color: transparent;
    color: var(--c-success);
  }

  .chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
  }
  .chip {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: var(--tap);
    padding: var(--sp-2) var(--sp-4);
    border: 1px solid var(--c-border-hairline);
    border-radius: var(--r-pill);
    background: var(--c-bg);
    color: var(--c-text);
    font: inherit;
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    text-decoration: none;
    cursor: pointer;
    transition:
      background var(--dur-fast) var(--ease),
      border-color var(--dur-fast) var(--ease);
  }
  .chip:hover {
    border-color: var(--c-text);
    color: var(--c-text);
  }
  .chip[aria-disabled="true"] {
    cursor: not-allowed;
    border-color: var(--c-border);
    background: var(--c-surface);
    color: var(--c-text-muted);
  }
  /* Selected: Obsidian fill and white text. Not colour alone — the fill
     flips from outline to solid, which survives greyscale. */
  .chip[aria-pressed="true"],
  .chip[aria-current="page"] {
    border-color: var(--c-accent);
    background: var(--c-accent);
    color: var(--c-text-invert);
  }
  .chip[aria-pressed="true"] svg,
  .chip[aria-current="page"] svg {
    color: currentcolor;
  }

  .chip-group__label {
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    letter-spacing: 0;
    color: var(--c-text-muted);
    margin-bottom: var(--sp-2);
  }

  /* Horizontal scroll for the download row on narrow screens. The row keeps
     its own scroll container so the page body never scrolls sideways. */
  @media (max-width: 599px) {
    .chip-row--scroll {
      flex-wrap: nowrap;
      overflow-x: auto;
      scroll-snap-type: x proximity;
      padding-bottom: var(--sp-2);
      -webkit-overflow-scrolling: touch;
    }
    .chip-row--scroll > * {
      flex: 0 0 auto;
      scroll-snap-align: start;
    }
  }

  /* --- banners ----------------------------------------------------------- */
  /* One pattern for every E-state: bold first line (what happened), plain
     lines (why / quota status / next step), optional CTA. The shape is the
     system's (12px corners, generous padding); the semantic colours and the
     4px leading rule are unchanged, because the rule is the non-colour
     signal that a banner is a state and not decoration. */
  .banner {
    border: 1px solid var(--c-border);
    border-left: 4px solid var(--c-text-muted);
    border-radius: var(--r-md);
    background: var(--c-surface);
    padding: var(--sp-4) var(--sp-5);
    color: var(--c-text-2);
  }
  .banner__title {
    margin: 0 0 var(--sp-2);
    font-weight: var(--fw-bold);
    color: var(--c-text);
  }
  .banner p {
    margin: 0 0 var(--sp-2);
  }
  .banner > :last-child {
    margin-bottom: 0;
  }
  .banner__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-top: var(--sp-4);
  }
  .banner--error {
    border-color: color-mix(in srgb, var(--c-error) 18%, transparent);
    border-left-color: var(--c-error);
    background: var(--c-error-bg);
  }
  .banner--error .banner__title {
    color: var(--c-error);
  }
  .banner--warn {
    border-color: color-mix(in srgb, var(--c-warn) 18%, transparent);
    border-left-color: var(--c-warn);
    background: var(--c-warn-bg);
  }
  .banner--info {
    border-color: color-mix(in srgb, var(--c-info) 18%, transparent);
    border-left-color: var(--c-info);
    background: var(--c-info-bg);
  }
  .banner--success {
    border-color: color-mix(in srgb, var(--c-success) 18%, transparent);
    border-left-color: var(--c-success);
    background: var(--c-success-bg);
  }
  /* A notice is the quiet variant: E15/E16/E21 and the cache/caption lines
     are not failures and must not look like one. */
  .banner--notice {
    border-left-width: 4px;
    font-size: var(--fs-caption);
    letter-spacing: 0;
    padding: var(--sp-3) var(--sp-4);
  }

  /* --- quota meter ------------------------------------------------------- */
  .meter {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    width: 100%;
    min-height: var(--tap);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background: var(--c-surface);
    color: var(--c-text-2);
    font-size: var(--fs-caption);
    text-align: left;
    font-family: inherit;
    cursor: pointer;
  }
  .meter__bar {
    flex: 0 0 72px;
    height: 8px;
    border-radius: 999px;
    background: var(--c-surface-3);
    overflow: hidden;
  }
  .meter__fill {
    display: block;
    height: 100%;
    width: var(--meter-pct, 0%);
    background: var(--c-accent);
    transition: width var(--dur) var(--ease);
  }
  .meter__text {
    flex: 1 1 auto;
  }
  /* The pre-load state. It says nothing it cannot know yet — no "90 of 90". */
  .meter[data-state="loading"] {
    cursor: default;
  }
  .meter[data-state="loading"] .meter__fill {
    width: 100%;
    background: var(--c-surface-3);
  }
  .meter[data-state="low"] {
    border-color: var(--c-warn);
    background: var(--c-warn-bg);
    color: var(--c-warn);
  }
  .meter[data-state="low"] .meter__fill {
    background: var(--c-warn);
  }
  .meter[data-state="empty"] {
    border-color: var(--c-error);
    background: var(--c-error-bg);
    color: var(--c-error);
  }
  .meter[data-state="empty"] .meter__fill {
    background: var(--c-error);
  }
  /* Unavailable is not zero. If /api/quota never answered we say so. */
  .meter[data-state="unknown"] {
    cursor: default;
    color: var(--c-text-muted);
  }

  /* --- dialog ------------------------------------------------------------ */
  .dialog {
    width: min(28rem, calc(100vw - 2 * var(--sp-4)));
    padding: 0;
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    background: var(--c-bg);
    color: var(--c-text);
    box-shadow: var(--shadow-2);
  }
  .dialog::backdrop {
    background: rgb(17 24 39 / 55%);
    overflow: hidden;
    overscroll-behavior: contain;
  }
  /* An open modal does not scroll-lock the page by itself in any engine. This
     is the part that works everywhere; the touch-drag-behind-the-modal case on
     iOS is not solved here and is noted in the report rather than papered over
     with a position:fixed body swap that loses the scroll position. */
  body:has(dialog[open]) {
    overflow: hidden;
  }
  .dialog {
    overscroll-behavior: contain;
  }
  .dialog__body {
    padding: var(--sp-5);
  }
  .dialog__title {
    margin: 0 0 var(--sp-3);
    font-size: var(--fs-head);
    font-weight: var(--fw-bold);
  }
  .dialog__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-top: var(--sp-5);
  }
  .dialog__actions .btn {
    flex: 1 1 auto;
  }

  /* --- toast ------------------------------------------------------------- */
  .toast-region {
    position: fixed;
    left: 50%;
    bottom: calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
    transform: translateX(-50%);
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    width: max-content;
    max-width: calc(100vw - 2 * var(--sp-4));
    pointer-events: none;
  }
  .toast {
    padding: var(--sp-3) var(--sp-4);
    border-radius: var(--r-md);
    background: var(--c-text);
    color: var(--c-text-invert);
    box-shadow: var(--shadow-2);
    font-size: var(--fs-caption);
  }

  /* --- ad zones ---------------------------------------------------------- */
  /* Slots only. No ad code ships from here — the tag lands after account
     approval and launch does not wait for it.
     min-height is reserved so an ad that arrives late cannot shift layout
     (CLS), and max-height caps mobile ad density. */
  .adzone {
    display: block;
    margin: var(--sp-6) 0;
    text-align: center;
    color: var(--c-text-muted);
    font-size: var(--fs-caption);
  }
  .adzone__label {
    display: block;
    font-size: var(--fs-caption);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--c-text-muted);
    margin-bottom: var(--sp-1);
  }
  .adzone__slot {
    display: block;
    min-height: 100px;
    max-height: 280px;
    background: var(--c-surface);
    border: 1px dashed var(--c-border);
    border-radius: var(--r-md);
  }
  /* Zone 2 sits after the download row on the transcript page. AdSense's own
     guidance is to keep ads far away from download links, and "directly below
     the download row" is adjacency however the row is worded — so the zone
     carries a hard separation and a rule above it, and the label is not
     optional. */
  .adzone--after-actions {
    margin-top: var(--sp-8);
    padding-top: var(--sp-5);
    border-top: 1px solid var(--c-border);
  }
  .adzone--sidebar {
    position: sticky;
    top: var(--sp-5);
    margin: 0;
  }
  .adzone--sidebar .adzone__slot {
    min-height: 600px;
    max-height: 600px;
  }
  @media (max-width: 1199px) {
    .adzone--sidebar {
      display: none;
    }
  }
  /* The mobile anchor. Google manages the creative; we manage only the space
     it occupies and the fact that it disappears under a modal. */
  .adzone--anchor {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    margin: 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--c-bg);
    border-top: 1px solid var(--c-border);
  }
  .adzone--anchor .adzone__slot {
    min-height: 50px;
    max-height: 100px;
    border: 0;
  }
  @media (min-width: 1200px) {
    .adzone--anchor {
      display: none;
    }
  }
  /* Set on <body> while any modal is open. */
  body[data-modal-open="true"] .adzone--anchor {
    display: none;
  }

  /* --- how it works / platform badges / faq ------------------------------ */
  .steps {
    display: grid;
    gap: var(--sp-3);
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: step;
  }
  .steps li {
    display: flex;
    gap: var(--sp-3);
    align-items: baseline;
    margin: 0;
    color: var(--c-text-2);
  }
  .steps li::before {
    counter-increment: step;
    content: counter(step);
    flex: 0 0 1.75rem;
    height: 1.75rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    background: var(--c-accent-weak);
    color: var(--c-info);
    font-size: var(--fs-caption);
    font-weight: var(--fw-bold);
  }

  .platform-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    list-style: none;
    margin: 0;
    padding: 0;
  }

  .faq {
    border-top: 1px solid var(--c-border);
  }
  .faq details {
    border-bottom: 1px solid var(--c-border);
  }
  .faq summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    min-height: var(--tap);
    padding: var(--sp-3) 0;
    cursor: pointer;
    font-weight: var(--fw-medium);
    list-style: none;
  }
  .faq summary::-webkit-details-marker {
    display: none;
  }
  .faq summary::after {
    content: "+";
    font-size: var(--fs-head);
    color: var(--c-text-muted);
  }
  .faq details[open] summary::after {
    content: "\2212";
  }
  .faq__answer {
    padding-bottom: var(--sp-4);
    color: var(--c-text-2);
    max-width: var(--w-prose);
  }

  /* --- dropzone ---------------------------------------------------------- */
  .dropzone {
    display: block;
    width: 100%;
    padding: var(--sp-6) var(--sp-4);
    border: 2px dashed var(--c-border-strong);
    border-radius: var(--r-lg);
    background: var(--c-surface);
    color: var(--c-text-2);
    text-align: center;
    font: inherit;
    cursor: pointer;
  }
  .dropzone[data-dragover="true"] {
    border-color: var(--c-accent);
    background: var(--c-accent-weak);
  }
  .dropzone__title {
    font-weight: var(--fw-medium);
    color: var(--c-text);
    margin-bottom: var(--sp-1);
  }
  .dropzone__hint {
    font-size: var(--fs-caption);
    color: var(--c-text-muted);
  }
  /* On touch devices the drag affordance is a lie; it becomes a file picker. */
  @media (hover: none) {
    .dropzone {
      padding: var(--sp-5) var(--sp-4);
      border-style: solid;
    }
  }

  .trustline {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    color: var(--c-success);
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
  }

  /* --- parsed preview / summary rows ------------------------------------- */
  .datarow {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--sp-2) var(--sp-4);
    margin: 0;
    font-size: var(--fs-caption);
  }
  .datarow dt {
    color: var(--c-text-muted);
  }
  .datarow dd {
    margin: 0;
    color: var(--c-text);
    font-variant-numeric: tabular-nums;
  }

  /* --- cue table (virtualised) ------------------------------------------- */
  /* The scroller is the only scrolling element; the sizer holds the full
     scroll height so the native scrollbar stays honest at 40,000 rows, and
     only the visible window is in the DOM. */
  .cue-table {
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    overflow: hidden;
    background: var(--c-bg);
  }
  .cue-table__head,
  .cue-table__row {
    display: grid;
    grid-template-columns: 4.5rem 11rem minmax(0, 1fr);
    gap: var(--sp-3);
    align-items: start;
    padding: var(--sp-2) var(--sp-3);
  }
  .cue-table--metrics .cue-table__head,
  .cue-table--metrics .cue-table__row {
    grid-template-columns: 4.5rem 11rem minmax(0, 1fr) 5rem;
  }
  @media (max-width: 599px) {
    .cue-table__head,
    .cue-table__row,
    .cue-table--metrics .cue-table__head,
    .cue-table--metrics .cue-table__row {
      grid-template-columns: 3rem minmax(0, 1fr);
      grid-template-areas: "idx time" "idx text";
      row-gap: var(--sp-1);
    }
    .cue-table__cell--idx {
      grid-area: idx;
    }
    .cue-table__cell--time {
      grid-area: time;
    }
    .cue-table__cell--text {
      grid-area: text;
    }
    .cue-table__cell--metric {
      display: none;
    }
  }
  .cue-table__head {
    background: var(--c-surface-2);
    border-bottom: 1px solid var(--c-border);
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    color: var(--c-text-muted);
    position: sticky;
    top: 0;
    z-index: 1;
  }
  .cue-table__scroller {
    overflow-y: auto;
    overflow-x: hidden;
    max-height: var(--cue-table-height, 26rem);
    overscroll-behavior: contain;
    /* Scroll anchoring and a virtualised list are incompatible: the browser
       adjusts scrollTop when content above the viewport changes size, and in a
       windowed list content above the viewport changes size on every frame by
       design. Worse on this site than most — an ad frame reflowing above the
       table would silently move every computed row offset. */
    overflow-anchor: none;
  }
  .cue-table__sizer {
    position: relative;
    width: 100%;
  }
  .cue-table__window {
    position: absolute;
    inset-inline: 0;
    top: 0;
  }
  /* Each recycled row places itself. Positioning rows individually rather
     than translating one container is what lets a pooled element keep its
     content until its index actually changes — see lib/virtual.ts slotFor. */
  .cue-table__row--virtual {
    position: absolute;
    inset-inline: 0;
    top: 0;
    will-change: transform;
    contain: layout paint;
  }
  .cue-table__row {
    border-bottom: 1px solid var(--c-border);
    font-size: var(--fs-caption);
    line-height: 1.45;
  }
  .cue-table__row:nth-child(even) {
    background: var(--c-surface);
  }
  .cue-table__cell--idx,
  .cue-table__cell--time,
  .cue-table__cell--metric {
    font-variant-numeric: tabular-nums;
    color: var(--c-text-muted);
    white-space: nowrap;
  }
  .cue-table__cell--text {
    color: var(--c-text);
    overflow-wrap: anywhere;
    /* Two lines, which is the resegmenter's own maximum per cue, so this
       clips almost nothing. It is also what makes the row height uniform,
       which is what the windowing arithmetic is built on. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .cue-table__cell--metric[data-over="true"] {
    color: var(--c-error);
    font-weight: var(--fw-bold);
  }
  .cue-table__empty {
    padding: var(--sp-5);
    text-align: center;
    color: var(--c-text-muted);
  }

  /* --- diff view --------------------------------------------------------- */
  /* Stacked by default (360px first). Side-by-side is the enhancement. */
  .diff {
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    overflow: hidden;
  }
  .diff__legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--c-surface-2);
    border-bottom: 1px solid var(--c-border);
    font-size: var(--fs-caption);
    color: var(--c-text-muted);
  }
  .diff__row {
    border-bottom: 1px solid var(--c-border);
    padding: var(--sp-3);
  }
  .diff__meta {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--fs-caption);
    color: var(--c-text-muted);
    font-variant-numeric: tabular-nums;
    margin-bottom: var(--sp-2);
  }
  .diff__panes {
    display: grid;
    gap: var(--sp-2);
  }
  .diff__pane {
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--r-sm);
    background: var(--c-surface);
    overflow-wrap: anywhere;
  }
  .diff__pane-label {
    display: block;
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
    color: var(--c-text-muted);
    margin-bottom: var(--sp-1);
  }
  /* Clamped so every diff row is the same height, which is what lets the diff
     use the same windowing as the cue table. Two lines is the resegmenter's
     own maximum per cue, so it clips almost nothing. */
  .diff__pane > span:last-child {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .diff .cue-table__scroller {
    max-height: var(--diff-height, 30rem);
  }
  .diff__pane--before {
    background: var(--c-error-bg);
  }
  .diff__pane--after {
    background: var(--c-success-bg);
  }
  /* Per-cue change marker — the mobile substitute for two columns. Never
     colour alone: the marker carries a glyph and a text label too. */
  .diff__marker {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 0 var(--sp-2);
    border-radius: 999px;
    font-size: var(--fs-caption);
    font-weight: var(--fw-medium);
  }
  .diff__marker[data-change="edited"] {
    background: var(--c-warn-bg);
    color: var(--c-warn);
  }
  .diff__marker[data-change="removed"] {
    background: var(--c-error-bg);
    color: var(--c-error);
  }
  .diff__marker[data-change="added"] {
    background: var(--c-success-bg);
    color: var(--c-success);
  }
  .diff__marker[data-change="unchanged"] {
    background: var(--c-surface-2);
    color: var(--c-text-muted);
  }
  .diff ins,
  .diff del {
    text-decoration: none;
    border-radius: 2px;
    padding: 0 0.1em;
  }
  .diff ins {
    background: #d1fae5;
    text-decoration: underline;
    text-decoration-style: solid;
  }
  .diff del {
    background: #fee2e2;
    text-decoration: line-through;
  }
  @media (min-width: 700px) {
    .diff--sbs .diff__panes {
      grid-template-columns: 1fr 1fr;
    }
  }

  /* --- option panel ------------------------------------------------------ */
  .options {
    display: grid;
    gap: var(--sp-3);
  }
  .optgroup {
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background: var(--c-bg);
  }
  .optgroup > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    min-height: var(--tap);
    padding: var(--sp-3) var(--sp-4);
    cursor: pointer;
    font-weight: var(--fw-medium);
    list-style: none;
  }
  .optgroup > summary::-webkit-details-marker {
    display: none;
  }
  .optgroup > summary::after {
    content: "";
    width: 0.6rem;
    height: 0.6rem;
    border-right: 2px solid var(--c-text-muted);
    border-bottom: 2px solid var(--c-text-muted);
    transform: rotate(45deg);
    transition: transform var(--dur-fast) var(--ease);
  }
  .optgroup[open] > summary::after {
    transform: rotate(-135deg);
  }
  .optgroup__body {
    padding: 0 var(--sp-4) var(--sp-4);
    display: grid;
    gap: var(--sp-3);
  }

  /* The quiet variant, for a panel that is open most of the time.
     CORE's encoding detector reports low confidence on nearly every legacy
     file it nonetheless decodes correctly, so the override opens on most
     uploads. An opened panel that looks like a warning would teach users to
     distrust a correct answer, so this one reads as a label with a control
     under it: no border emphasis, caption-sized summary, muted. */
  .optgroup--quiet {
    border-color: transparent;
    background: var(--c-surface);
  }
  .optgroup--quiet > summary {
    font-size: var(--fs-caption);
    font-weight: var(--fw-normal);
    color: var(--c-text-muted);
    min-height: 2rem;
    padding: var(--sp-2) var(--sp-3);
  }
  .optgroup--quiet > .optgroup__body {
    padding: 0 var(--sp-3) var(--sp-3);
  }

  .opt {
    display: grid;
    gap: var(--sp-2);
  }
  .opt--check {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    column-gap: var(--sp-3);
    min-height: var(--tap);
    align-content: center;
  }
  .opt--check input[type="checkbox"],
  .opt--check input[type="radio"] {
    width: 1.25rem;
    height: 1.25rem;
    margin: 0.35rem 0 0;
    accent-color: var(--c-accent);
  }
  .opt__label {
    font-weight: var(--fw-medium);
  }
  .opt__help {
    grid-column: 1 / -1;
    font-size: var(--fs-caption);
    color: var(--c-text-muted);
  }
  .opt--check .opt__help {
    grid-column: 2;
  }
  /* Nesting. The rail is what makes "keep italic sits UNDER remove
     formatting" legible without reading the label — the competitor's cleaner
     is confusing precisely here. */
  .opt__children {
    grid-column: 1 / -1;
    margin-left: var(--sp-3);
    padding-left: var(--sp-4);
    border-left: 2px solid var(--c-border);
    display: grid;
    gap: var(--sp-2);
  }
  .opt--check > .opt__children {
    grid-column: 2;
    margin-left: 0;
  }
  .opt__children[hidden] {
    display: none;
  }
  .opt--disabled > .opt__label,
  .opt--disabled > .opt__help {
    opacity: 0.6;
  }
  .opt__row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    align-items: center;
  }
  .opt__row .input,
  .opt__row .select {
    width: auto;
    min-width: 8rem;
    flex: 0 1 auto;
  }

  /* --- the 9-cell position grid ------------------------------------------ */
  /* One choice from a fixed set, so the inputs are real radios and only the
     labels are painted. Alignment 1 is BOTTOM-left in ASS numbering, so the
     markup runs 7-8-9 / 4-5-6 / 1-2-3 and the grid reads the way it looks. */
  .grid9 {
    display: grid;
    grid-template-columns: repeat(3, var(--tap));
    grid-template-rows: repeat(3, var(--tap));
    gap: 2px;
    width: max-content;
    padding: var(--sp-1);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--r-md);
    background: var(--c-surface);
  }
  .grid9 input {
    position: absolute;
  }
  .grid9__cell {
    display: block;
    border-radius: var(--r-sm);
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    cursor: pointer;
  }
  .grid9__cell:hover {
    background: var(--c-surface-2);
  }
  .grid9 input:checked + .grid9__cell {
    background: var(--c-accent);
    border-color: var(--c-accent-strong);
  }
  /* The ring has to follow the checked cell, because the input itself is
     visually hidden and a keyboard user would otherwise see nothing move. */
  .grid9 input:focus-visible + .grid9__cell {
    outline: 3px solid var(--c-focus);
    outline-offset: 2px;
  }

  /* --- colour ------------------------------------------------------------- */
  .opt__color {
    width: var(--tap);
    height: var(--tap);
    padding: 2px;
    border: 1px solid var(--c-border-strong);
    border-radius: var(--r-md);
    background: var(--c-bg);
    cursor: pointer;
  }
  .opt__alpha {
    flex: 1 1 8rem;
    min-height: var(--tap);
    accent-color: var(--c-accent);
  }

  /* --- a tool's stated limitation -----------------------------------------
     DELIBERATELY NOT A BANNER. `.banner--warn` would put an alarm above a
     control that works, and the founder's ruling is that this is a stated
     limitation of a working feature, not an apology and not a warning.

     So: body text weight, a quiet left rule in the neutral border colour, and
     no icon. It reads as part of the page's explanation of itself, which is
     what it is. A tool that says what it does not do, before you use it, is
     the reason to believe what it says it does. ---------------------------- */
  .notice {
    margin: var(--sp-3) 0 0;
    padding: var(--sp-3) var(--sp-4);
    border-inline-start: 3px solid var(--c-border-strong);
    background: var(--c-surface);
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
    color: var(--c-text-2);
  }

  /* --- the download row's two lossy outcomes ------------------------------
     WARN and REFUSE must be distinguishable WITHOUT colour. Two red-ish
     banners a shade apart is colour-as-the-only-signal with extra steps, which
     TECHNICAL_REQUIREMENTS forbids and which a greyscale screenshot, a 200%
     zoom or any form of colour blindness collapses into one thing.

     So they differ on three axes at once — rule weight, leading glyph, and
     colour — and the difference that actually matters is neither: `refuse`
     has no download control in its subtree at all, which survives this
     stylesheet failing to load. ---------------------------------------------- */
  .dl-outcome {
    border-inline-start-style: solid;
  }

  .dl-outcome--warn {
    border-inline-start-width: 3px;
  }

  /* Twice the rule, and a dotted edge that reads as "stopped" rather than
     "careful" at a glance and in print. */
  .dl-outcome--refuse {
    border-inline-start-width: 6px;
    border-inline-start-style: double;
  }

  .dl-outcome__list {
    margin: var(--sp-2) 0 0;
    padding-inline-start: var(--sp-4);
    display: grid;
    gap: var(--sp-1);
  }

  /* The glyph is the greyscale carrier. `::marker` rather than a pseudo-element
     on the text so it never lands inside a copied selection. */
  .dl-outcome--warn .dl-outcome__list {
    list-style: "! ";
  }

  .dl-outcome--refuse .dl-outcome__list {
    list-style: "\00d7  ";
  }

  .dl-outcome__list::marker,
  .dl-outcome__list li::marker {
    font-variant-numeric: tabular-nums;
  }

  /* --- batch list -------------------------------------------------------- */
  .batch {
    list-style: none;
    margin: 0;
    padding: 0;
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    overflow: hidden;
  }
  .batch li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2) var(--sp-3);
    padding: var(--sp-3);
    border-bottom: 1px solid var(--c-border);
    font-size: var(--fs-caption);
  }
  .batch li:last-child {
    border-bottom: 0;
  }
  .batch__name {
    flex: 1 1 12rem;
    overflow-wrap: anywhere;
    color: var(--c-text);
  }
  .batch__status[data-status="done"] {
    color: var(--c-success);
  }
  .batch__status[data-status="skipped"] {
    color: var(--c-warn);
  }
  .batch__status[data-status="failed"] {
    color: var(--c-error);
  }
  .batch__reason {
    flex: 1 1 100%;
    color: var(--c-text-muted);
  }

  /* --- progress / queue -------------------------------------------------- */
  .status-line {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
  }
  .position-pill {
    display: inline-flex;
    align-items: center;
    padding: var(--sp-1) var(--sp-3);
    border-radius: 999px;
    background: var(--c-accent-weak);
    color: var(--c-info);
    font-weight: var(--fw-bold);
    font-variant-numeric: tabular-nums;
  }
  .progress {
    width: 100%;
    height: 10px;
    border-radius: 999px;
    background: var(--c-surface-3);
    overflow: hidden;
  }
  .progress__fill {
    display: block;
    height: 100%;
    width: var(--progress-pct, 0%);
    background: var(--c-accent);
    transition: width var(--dur) var(--ease);
  }
  .progress[data-indeterminate="true"] .progress__fill {
    width: 35%;
    animation: indeterminate 1.4s var(--ease) infinite;
  }
  @keyframes indeterminate {
    0% {
      transform: translateX(-100%);
    }
    100% {
      transform: translateX(320%);
    }
  }

  /* --- transcript -------------------------------------------------------- */
  .transcript {
    max-width: var(--w-prose);
    font-size: var(--fs-body);
  }
  .transcript p {
    margin: 0 0 var(--sp-4);
  }
  .transcript[data-mode="timestamps"] p {
    display: grid;
    grid-template-columns: 4.5rem minmax(0, 1fr);
    gap: var(--sp-3);
    margin-bottom: var(--sp-2);
  }
  .transcript__t {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    color: var(--c-text-muted);
    text-decoration: none;
  }
  .transcript[data-mode="paragraphs"] .transcript__t {
    display: none;
  }

  .skeleton {
    background: linear-gradient(
      90deg,
      var(--c-surface-2) 25%,
      var(--c-surface-3) 37%,
      var(--c-surface-2) 63%
    );
    background-size: 400% 100%;
    animation: shimmer 1.4s ease infinite;
    border-radius: var(--r-sm);
    height: 1rem;
    margin-bottom: var(--sp-2);
  }
  .skeleton--title {
    height: 1.75rem;
    width: 60%;
    margin-bottom: var(--sp-4);
  }
  .skeleton--short {
    width: 70%;
  }
  @keyframes shimmer {
    0% {
      background-position: 100% 50%;
    }
    100% {
      background-position: 0 50%;
    }
  }

  /* --- sticky action bar (transcript, mobile) ----------------------------- */
  .actionbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 45;
    display: flex;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    padding-bottom: calc(var(--sp-2) + env(safe-area-inset-bottom, 0px));
    background: var(--c-bg);
    border-top: 1px solid var(--c-border);
    box-shadow: 0 -1px 3px rgb(17 24 39 / 8%);
  }
  .actionbar .btn {
    flex: 1 1 0;
  }
  @media (min-width: 700px) {
    .actionbar {
      display: none;
    }
  }

  /* --- tooltip (tap-equivalent, never hover-only) ------------------------- */
  .tip {
    position: relative;
  }
  .tip__bubble {
    display: block;
    margin-top: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--r-sm);
    background: var(--c-surface-2);
    color: var(--c-text-2);
    font-size: var(--fs-caption);
  }
  .tip__bubble[hidden] {
    display: none;
  }
}

/* -----------------------------------------------------------------------------
   UTILITIES
   -------------------------------------------------------------------------- */

/* ==========================================================================
   PAGE-FAMILY SLOTS -- the Letters redesign, one slot per parallel agent.

   Ten agents restyle ten page families at the same time on ten branches, and
   all ten merge back into this one file. Git merges two insertions cleanly
   when unchanged lines separate them and conflicts when they touch, so each
   family writes ONLY between its own START and END lines, and the spacer
   comments between slots are what keep the merges trivial. Do not delete
   the spacers or the markers; they can be tidied in one pass once every
   branch has landed.
   ========================================================================== */
@layer components {
  /* ---- SLOT START: landing-hero -- landing hero band, H1, transcribe card, input group, quota meter ---- */
  /* ---- SLOT END: landing-hero ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: landing-sections -- landing below the fold: steps, platform pills, FAQ, section rhythm ---- */
  /* ---- SLOT END: landing-sections ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: platform -- the six /{slug}-transcript platform pages ---- */
  /* ---- SLOT END: platform ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: tool-input -- tool page head, trustline, breadcrumb, dropzone, parsed preview, encoding, FPS ---- */
  /* ---- SLOT END: tool-input ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: tool-options -- the options panel the TypeScript builds ---- */
  /* ---- SLOT END: tool-options ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: tool-results -- cue table, diff, progress, batch list, download row, loss list, related links ---- */
  /* ---- SLOT END: tool-results ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: hub-pair -- converter hub grid and pair pages ---- */
  /* ---- SLOT END: hub-pair ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: transcript -- transcript reader: meta header, segments, mode toggles, copy, notices, format chips ---- */
  /* ---- SLOT END: transcript ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: job-format -- job/queue stepper states and the format step page ---- */
  /* ---- SLOT END: job-format ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
  /* ---- SLOT START: states-chrome -- E-state banners, dialog, toasts, tooltips, ad zones, legal prose, nojs ---- */
  /* ---- SLOT END: states-chrome ---- */
  /* spacer */
  /* spacer */
  /* spacer */
  /* spacer */
}

@layer utilities {
  .u-tnum {
    font-variant-numeric: tabular-nums;
  }
  .u-muted {
    color: var(--c-text-muted);
  }
  .u-caption {
    font-size: var(--fs-caption);
  }
  .u-center {
    text-align: center;
  }
  .u-nowrap {
    white-space: nowrap;
  }
  .u-mt-0 {
    margin-top: 0;
  }
  .u-mb-0 {
    margin-bottom: 0;
  }
  /* Reduced motion: no transitions, no smooth scroll, no animation. Not
     "shorter" — off.

     It lives in the utilities layer rather than the base layer, and that
     placement is the whole mechanism: a cascade layer beats every earlier
     layer regardless of selector weight, so `*` here overrides a component's
     transition without a single `!important` anywhere in the stylesheet. */
  @media (prefers-reduced-motion: reduce) {
    html {
      scroll-behavior: auto;
    }
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms;
      animation-iteration-count: 1;
      transition-duration: 0.01ms;
      scroll-behavior: auto;
    }
  }

  .u-hidden,
  [hidden] {
    display: none;
  }

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

  /* Pages that use the fixed mobile anchor set this so the last content is
     never trapped under it. */
  .u-anchor-inset {
    --page-bottom-inset: 60px;
  }
  .u-actionbar-inset {
    --page-bottom-inset: 128px;
  }

  /* THE JS-OFF FLOOR — and note which way round it is.

     `.u-js-only` is VISIBLE here. Hiding it and revealing it with a data-js
     flag is the obvious arrangement and it cannot work on this site: setting
     that flag needs an inline script, PLATFORM's CSP has no nonce and does not
     allow one, and the blocked script left the entire interactive surface at
     display:none with nothing in the console to say so. See nojs.css.

     The hiding therefore lives in nojs.css, which only a scripting-disabled
     browser ever fetches. What stays here is the other half: the plain-link
     fallbacks are hidden for everyone by default, because the readers who need
     them are exactly the ones who load nojs.css. */
  .u-nojs-only {
    display: none;
  }
}
