/* =============================================================================
   ASTS Podcast Tracker — design system
   =============================================================================
   Codified guidelines live in DESIGN.md. Structure (in order):

     1. TOKENS      — all color/type/space/radius decisions. Themes are token
                      swaps only: light on :root, dark under the media query,
                      then [data-theme] overrides both directions.
     2. BASE        — reset, typography, links, focus.
     3. LAYOUT      — header, container, footer.
     4. COMPONENTS  — filter bar, episode cards, analysis blocks, badges,
                      ledger list, stat strip, transcript.
     5. RESPONSIVE  — small-screen adjustments.

   Rule: components consume semantic tokens only. Never hardcode a color in a
   component rule — add or extend a token instead.
   ========================================================================== */

/* 1. TOKENS ================================================================ */

:root {
  color-scheme: light;

  /* Grounds & ink — blue-biased neutrals, never pure grey */
  --ground: #f2f5f9;
  --surface: #ffffff;
  --surface-inset: #eaeff6;
  --line: #dde4ee;
  --line-strong: #c2cddc;
  --ink: #17212f;
  --ink-muted: #4f5f76;
  --ink-faint: #656f7e;

  /* Accent — "uplink cyan". Separate from all status hues. */
  --accent: #0b7ca4;
  --accent-strong: #085d7c;
  --accent-soft: #e0f1f8;
  --accent-contrast: #ffffff;
  --focus-ring: #0b7ca4;

  /* Dropdown caret for <select> — the fold chevron pointed down, stroked in the
     theme's --ink-faint. A token (not a component value) because the color is
     baked into the SVG: an inline data URI can't read a custom property, so the
     glyph has to swap with the theme like every other color decision. */
  --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M3.5 5.5L7 9l3.5-3.5' stroke='%23656f7e' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  /* Status paints — the shared hue set behind tags, categories, confidence.
     Six pairs; everything semantic aliases one of these. */
  --paint-green-bg: #e0f4e7;   --paint-green-fg: #177a3e;
  --paint-blue-bg: #e3ecfb;    --paint-blue-fg: #1d5fbf;
  --paint-amber-bg: #fcf0da;   --paint-amber-fg: #8f5f0e;
  --paint-red-bg: #fbe3e3;     --paint-red-fg: #b3261e;
  --paint-violet-bg: #eee5f9;  --paint-violet-fg: #6a3aa8;
  --paint-copper-bg: #f4e9de;  --paint-copper-fg: #8a5524;
  --paint-neutral-bg: #e9edf4; --paint-neutral-fg: #55627a;

  /* Elevation */
  --shadow-1: 0 1px 2px rgba(23, 33, 47, 0.05);
  --shadow-2: 0 4px 16px rgba(23, 33, 47, 0.07);

  /* Type */
  --font-display: "Barlow Condensed", "Arial Narrow", Arial, sans-serif;
  --font-body: "Barlow", -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;

  --text-xs: 0.72rem;    /* eyebrows, badges, table headers */
  --text-sm: 0.86rem;    /* meta, captions, chips */
  --text-md: 1rem;       /* body */
  --text-lg: 1.14rem;    /* ledes, topic titles */
  --text-xl: 1.5rem;     /* h2 / episode titles on cards */
  --text-2xl: 2.1rem;    /* page h1 (display face) */

  /* Space (4px base) & radii */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 24px; --sp-7: 32px; --sp-8: 48px;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-pill: 999px;

  /* Measure */
  --w-content: 1020px;
  --w-prose: 72ch;
  --ep-rail: 92px;   /* episode-card date rail width — one source of truth */
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --ground: #0b1017;
    --surface: #131b26;
    --surface-inset: #1a2431;
    --line: #232f42;
    --line-strong: #35465f;
    --ink: #e3eaf4;
    --ink-muted: #9fadc3;
    --ink-faint: #7d8da5;

    --accent: #56c3ea;
    --accent-strong: #8cd7f4;
    --accent-soft: #11303f;
    --accent-contrast: #0b1017;
    --focus-ring: #56c3ea;

    --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M3.5 5.5L7 9l3.5-3.5' stroke='%237d8da5' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

    --paint-green-bg: #123020;   --paint-green-fg: #63d197;
    --paint-blue-bg: #16283f;    --paint-blue-fg: #85b3f2;
    --paint-amber-bg: #322610;   --paint-amber-fg: #e2b463;
    --paint-red-bg: #371718;     --paint-red-fg: #f08d88;
    --paint-violet-bg: #261b39;  --paint-violet-fg: #c4a4ee;
    --paint-copper-bg: #2e2115;  --paint-copper-fg: #d4a370;
    --paint-neutral-bg: #202a38; --paint-neutral-fg: #9fadc3;

    --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-2: 0 4px 18px rgba(0, 0, 0, 0.35);
  }
}

/* Explicit theme choice (toggle) beats the OS preference, both directions. */
:root[data-theme="light"] {
  color-scheme: light;
  --ground: #f2f5f9; --surface: #ffffff; --surface-inset: #eaeff6;
  --line: #dde4ee; --line-strong: #c2cddc;
  --ink: #17212f; --ink-muted: #4f5f76; --ink-faint: #656f7e;
  --accent: #0b7ca4; --accent-strong: #085d7c; --accent-soft: #e0f1f8;
  --accent-contrast: #ffffff; --focus-ring: #0b7ca4;
  --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M3.5 5.5L7 9l3.5-3.5' stroke='%23656f7e' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --paint-green-bg: #e0f4e7;   --paint-green-fg: #177a3e;
  --paint-blue-bg: #e3ecfb;    --paint-blue-fg: #1d5fbf;
  --paint-amber-bg: #fcf0da;   --paint-amber-fg: #8f5f0e;
  --paint-red-bg: #fbe3e3;     --paint-red-fg: #b3261e;
  --paint-violet-bg: #eee5f9;  --paint-violet-fg: #6a3aa8;
  --paint-copper-bg: #f4e9de;  --paint-copper-fg: #8a5524;
  --paint-neutral-bg: #e9edf4; --paint-neutral-fg: #55627a;
  --shadow-1: 0 1px 2px rgba(23, 33, 47, 0.05);
  --shadow-2: 0 4px 16px rgba(23, 33, 47, 0.07);
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --ground: #0b1017; --surface: #131b26; --surface-inset: #1a2431;
  --line: #232f42; --line-strong: #35465f;
  --ink: #e3eaf4; --ink-muted: #9fadc3; --ink-faint: #7d8da5;
  --accent: #56c3ea; --accent-strong: #8cd7f4; --accent-soft: #11303f;
  --accent-contrast: #0b1017; --focus-ring: #56c3ea;
  --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M3.5 5.5L7 9l3.5-3.5' stroke='%237d8da5' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --paint-green-bg: #123020;   --paint-green-fg: #63d197;
  --paint-blue-bg: #16283f;    --paint-blue-fg: #85b3f2;
  --paint-amber-bg: #322610;   --paint-amber-fg: #e2b463;
  --paint-red-bg: #371718;     --paint-red-fg: #f08d88;
  --paint-violet-bg: #261b39;  --paint-violet-fg: #c4a4ee;
  --paint-copper-bg: #2e2115;  --paint-copper-fg: #d4a370;
  --paint-neutral-bg: #202a38; --paint-neutral-fg: #9fadc3;
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-2: 0 4px 18px rgba(0, 0, 0, 0.35);
}

/* Semantic aliases — map meanings onto the paint set. Extend here. */
:root {
  /* Epistemic tags */
  --tag-confirmed-bg: var(--paint-green-bg);     --tag-confirmed-fg: var(--paint-green-fg);
  --tag-guidance-bg: var(--paint-blue-bg);       --tag-guidance-fg: var(--paint-blue-fg);
  --tag-speculation-bg: var(--paint-amber-bg);   --tag-speculation-fg: var(--paint-amber-fg);
  --tag-rumor-bg: var(--paint-red-bg);           --tag-rumor-fg: var(--paint-red-fg);
  --tag-disagreement-bg: var(--paint-violet-bg); --tag-disagreement-fg: var(--paint-violet-fg);
  --tag-none-bg: var(--paint-neutral-bg);        --tag-none-fg: var(--paint-neutral-fg);

  /* Ledger categories */
  --cat-financing-bg: var(--paint-blue-bg);        --cat-financing-fg: var(--paint-blue-fg);
  --cat-launch-bg: var(--paint-green-bg);          --cat-launch-fg: var(--paint-green-fg);
  --cat-regulatory-bg: var(--paint-violet-bg);     --cat-regulatory-fg: var(--paint-violet-fg);
  --cat-commercial-bg: var(--paint-amber-bg);      --cat-commercial-fg: var(--paint-amber-fg);
  --cat-manufacturing-bg: var(--paint-copper-bg);  --cat-manufacturing-fg: var(--paint-copper-fg);

  /* Ledger reconciliation — a pending prediction retired by a confirmed
     outcome (event_ledger.resolved_by). Aliased onto green, the confirmed
     hue: the mark makes the same claim the Confirmed tag does, that a
     recorded fact now backs this row. */
  --resolved-bg: var(--paint-green-bg);          --resolved-fg: var(--paint-green-fg);

  /* Pending state mark — only used where a section does NOT already imply the
     confidence (the "Last 30 days" spotlight mixes confirmed and pending rows).
     Amber, the pending hue the past-target rail already uses: grouping, not
     alarm. */
  --pending-bg: var(--paint-amber-bg);           --pending-fg: var(--paint-amber-fg);

  /* Evidence tiers (event_sources.tier) — how good the evidence behind a ledger
     row is, an axis orthogonal to what KIND of claim it is. Deliberately
     NEUTRAL: the category chip already spends hue on that row, and a second hue
     family would turn every line into a traffic light. Authority is carried by
     fill density instead, so the ramp survives greyscale — solid ink at the top,
     bare faint text at the bottom, dashed for unverifiable. Aliases onto the
     existing neutrals; no new paint. Order matches db.SOURCE_TIERS. */
  --src-filing-bg: var(--ink);          --src-filing-fg: var(--surface);      --src-filing-line: var(--ink);
  --src-company-bg: var(--ink-muted);   --src-company-fg: var(--surface);     --src-company-line: var(--ink-muted);
  --src-exec-bg: var(--surface-inset);  --src-exec-fg: var(--ink);            --src-exec-line: var(--line-strong);
  --src-tracker-bg: transparent;        --src-tracker-fg: var(--ink-muted);   --src-tracker-line: var(--line-strong);
  --src-news-bg: transparent;           --src-news-fg: var(--ink-muted);      --src-news-line: var(--line);
  --src-podcast-bg: transparent;        --src-podcast-fg: var(--ink-faint);   --src-podcast-line: transparent;
  --src-unknown-bg: transparent;        --src-unknown-fg: var(--ink-faint);   --src-unknown-line: var(--line);

  /* Satellite production chart. Two series, aliased onto the paints their
     subject already owns elsewhere on the site: a shipped satellite is one
     that launches (green, the `launch` category), one still in the queue is
     being built (copper, `manufacturing`). Total is the top edge of the stack
     rather than a third series, so it takes no paint at all — plain ink, which
     is what separates it from both bands without spending a hue. No new hex.

     The band fills are mixed DOWN from each series' line color rather than
     aliased onto the matching `--paint-*-bg`: those are badge backgrounds, sized
     to sit behind ~10 characters, and at chart scale in dark they land within
     1.2:1 of the surface — the two bands, and the boundary between them,
     effectively vanish, leaving the stack's whole argument to the 2px edges.
     Mixing into `--surface` keeps one definition working in both themes.

     The two mix at different strengths on purpose. At equal strength the bands
     differ in hue but land within 1.01:1 of each other in luminance, so the
     boundary survives greyscale and color-blind vision only through the edge
     stroke. Density carries it instead, the way the source ramp does: shipped is
     the settled quantity and takes the heavier fill, in-production the lighter.

     The orbit split originally kept ONE hue for the whole shipped family and
     separated within it by density — solid green on orbit, pale green still
     waiting at the launch site — on the theory that a reader should see one green
     region meaning "left the factory", subdivided by how far along it is. That
     works for the FILLS and fails for the EDGES, which is what shipped and got
     reported: two 2px strokes cannot encode a density difference, so `in_orbit`'s
     top and the shipped total above it drew in literally the same hex and read as
     one repeated line.

     Density cannot be pushed far enough to rescue it. Measured in OKLab against
     the >= 15 legibility floor, the widest defensible two-green split reaches
     dE 12.9 in light and only 8.7 in dark, where both greens are light to begin
     with. So `awaiting_launch` leaves the family for blue: worst adjacent pair
     across the whole band order becomes 23.7 light / 18.1 dark, comfortably clear
     in both themes.

     What that costs, stated plainly: blue owns `financing` in the tag palette and
     owns nothing about a satellite waiting on a pad, so this is the one series
     color on the chart that is chosen for separation rather than inherited from
     its subject. The green family still reads on the fills — `in_orbit` green,
     `shipped` green in the two-band fallback — it just no longer carries the
     at-launch-site stage. `lost` aliases red, the only band that is not a stage
     of progress.

     KNOWN, PRE-DATES THIS: green vs red (in orbit vs lost) is dE 4.5 under
     deuteranopia, i.e. those two bands are not separable by hue for a red/green
     colour-blind reader. It is mitigated, not solved, by the legend, the direct
     labels, the Data fold, and Lost being a one-satellite sliver present only
     from 2026-04-20. Fixing it means moving `lost` off red, which spends the
     palette's one unambiguous "this went wrong" signal.

     Exactly one `--sat-<band>-line` / `--sat-<band>-fill` pair per band key in
     app.SATELLITE_BANDS, plus `shipped` for the two-band fallback shape. No
     aliases and no spares: a token that names a band but is not what that band's
     rules actually read is a trap, and `--sat-in_production-line` was one — it
     said copper while the band's edge and dots both (correctly) drew the total
     in ink. The in-production EDGE is the total edge, so it takes
     `--sat-total-line`; only the band's fill and its label take copper. */
  --sat-lost-line: var(--paint-red-fg);
  --sat-in_orbit-line: var(--paint-green-fg);
  --sat-awaiting_launch-line: var(--paint-blue-fg);
  --sat-shipped-line: var(--paint-green-fg);
  --sat-in_production-line: var(--paint-copper-fg);
  --sat-total-line: var(--ink-muted);
  --sat-lost-fill: color-mix(in srgb, var(--sat-lost-line) 45%, var(--surface));
  --sat-in_orbit-fill: color-mix(in srgb, var(--sat-in_orbit-line) 42%, var(--surface));
  /* 28%, not the 22% the pale-green version used: blue mixes toward the surface
     faster than green does, and at 22% the band came within dE 6.0 of in-production's
     copper wash. 28% maximises the worst adjacent fill pair (8.9 light / 9.5 dark);
     going further to 34% only moves the worst pair onto blue-vs-green, which is the
     one this change exists to open up. */
  --sat-awaiting_launch-fill: color-mix(in srgb, var(--sat-awaiting_launch-line) 28%, var(--surface));
  --sat-shipped-fill: color-mix(in srgb, var(--sat-shipped-line) 30%, var(--surface));
  --sat-in_production-fill: color-mix(in srgb, var(--sat-in_production-line) 15%, var(--surface));

  /* Projection strokes: one `--sat-<band>-target` per band key, mixed down from
     the SAME source that band's own edge draws in, so a dotted line always reads
     as the muted continuation of the solid line it leaves. That means
     `--sat-in_production-target` mixes from `--sat-total-line`, not from the
     copper `--sat-in_production-line`, for exactly the reason the note above
     gives: the in-production band's EDGE is the total edge. A token that named
     the band but did not match what the band's rules read would be the same trap
     twice.

     60%, between the fills (15-45%) and the full-strength lines. Lower and a
     dotted line — already broken up by its own gaps — dissolves into the fill it
     crosses; higher and "promised" starts reading as "disclosed", which is the
     one distinction this whole layer exists to draw. */
  --sat-lost-target: color-mix(in srgb, var(--sat-lost-line) 60%, var(--surface));
  --sat-in_orbit-target: color-mix(in srgb, var(--sat-in_orbit-line) 60%, var(--surface));
  --sat-awaiting_launch-target: color-mix(in srgb, var(--sat-awaiting_launch-line) 60%, var(--surface));
  --sat-shipped-target: color-mix(in srgb, var(--sat-shipped-line) 60%, var(--surface));
  --sat-in_production-target: color-mix(in srgb, var(--sat-total-line) 60%, var(--surface));

  /* Chart text and strokes are in viewBox units, so they scale with the SVG —
     which is exactly the problem on a phone, where the 760-unit chart paints
     into ~334px and an 11-unit label lands under 5px. They are tokens so the
     narrow breakpoint can scale them back up; a rem value from the type scale
     would not help, since inside a viewBox it resolves to user units and is
     scaled by the same transform as everything else. */
  --sat-axis-text: 11px;
  --sat-stroke: 2px;
}

/* 2. BASE ================================================================== */

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: 1.65;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-strong); text-decoration: underline; }

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

code, pre, .mono, .ts, .ledger-date {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

h1, h2, h3, h4 { text-wrap: balance; }

h1 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-2xl);
  line-height: 1.15;
  letter-spacing: 0.005em;
  margin: 0 0 var(--sp-2);
}

h2 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-xl);
  line-height: 1.2;
  margin: var(--sp-7) 0 var(--sp-3);
}

/* h3 is a scannable section label, research-report style */
h3 {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
  margin: var(--sp-6) 0 var(--sp-3);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
h3::after { content: ""; flex: 1; height: 1px; background: var(--line); }

h4 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-lg);
  margin: 0 0 var(--sp-2);
}

/* Eyebrow — page-level kicker above every h1 */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--accent);
  margin: 0 0 var(--sp-1);
}

.subhead {
  color: var(--ink-muted);
  margin: 0 0 var(--sp-6);
  max-width: var(--w-prose);
}

.empty-note { color: var(--ink-faint); font-style: italic; }

.parse-note {
  color: var(--tag-rumor-fg);
  background: var(--tag-rumor-bg);
  border-radius: var(--radius-sm);
  padding: var(--sp-2) var(--sp-3);
  display: inline-block;
  font-size: var(--text-sm);
}

.empty-state {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--sp-8);
  text-align: center;
  box-shadow: var(--shadow-1);
}

/* 3. LAYOUT ================================================================ */

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.header-inner {
  max-width: var(--w-content);
  margin: 0 auto;
  padding: var(--sp-3) var(--sp-5);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--ink);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.25rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.brand:hover { text-decoration: none; color: var(--ink); }
.brand-mark { color: var(--accent); flex: none; }

.main-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
}

.main-nav a {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.05rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  padding: 2px 0;
  border-bottom: 2px solid transparent;
}
.main-nav a:hover { color: var(--ink); text-decoration: none; }
.main-nav a.active { color: var(--ink); border-bottom-color: var(--accent); }

.theme-toggle {
  background: none;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  color: var(--ink-muted);
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--ink-faint); }
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }
/* When dark is active (by OS or toggle), offer the sun */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-sun { display: block; }
  :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: none; }
}
:root[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
:root[data-theme="light"] .theme-toggle .icon-sun { display: none; }
:root[data-theme="light"] .theme-toggle .icon-moon { display: block; }

.content {
  max-width: var(--w-content);
  margin: 0 auto;
  padding: var(--sp-7) var(--sp-5) var(--sp-8);
}

.site-footer {
  max-width: var(--w-content);
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-5) var(--sp-8);
  color: var(--ink-faint);
  font-size: var(--text-sm);
  border-top: 1px solid var(--line);
}

/* 4. COMPONENTS ============================================================ */

/* --- Filter bar ----------------------------------------------------------- */

.filter-bar {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--sp-4);
  margin-bottom: var(--sp-4);
  box-shadow: var(--shadow-1);
}

/* The controls are a disclosure, closed by default. Six fields ahead of the
   first episode card is a screenful of chrome on a phone; collapsed, the bar is
   a single row — funnel glyph, "Filters", and a count of what's applied. Nothing
   is hidden by folding: the .active-filters chip row below always names the
   filters in effect, and Clear rides that row so it stays reachable while closed.
   Fields inside a closed <details> still submit, so state survives the fold. */
.filter-summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  list-style: none;
}
.filter-summary::-webkit-details-marker { display: none; }
.filter-summary:hover .fold-title { color: var(--ink); }
.filter-summary .fold-title { gap: var(--sp-3); }
.filter-icon { flex: none; color: var(--accent); }
.filter-fold[open] > .filter-summary { margin-bottom: var(--sp-4); }
.filter-fold[open] > .filter-summary .fold-chevron { transform: rotate(90deg); }

/* Three equal columns, and both pages carry exactly six items — /watch is
   From, To, Section / Category, Source, Apply; the episode list is Search,
   From, To / Type, Speaker, Apply — so each lands as a 2x3 block of
   same-width cells. Equal columns rather than intrinsic widths: flex sized
   every control to its own content, so From and To (identical fields) came out
   different widths from Type and Speaker and the row read as a ragged strip.
   `align-items: end` bottom-aligns every cell, which is what puts the Apply
   button (no label above it) on the same baseline as the input boxes beside it
   rather than floating at the top of its cell. */
.filter-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-4);
  align-items: end;
}

.filter-field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0; /* grid cells must be allowed to shrink below content width */
}

.filter-field span {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
}

/* Selects share the text/date input box exactly — same token set, so the filter
   row reads as one control strip. Width comes from the grid column, not from
   the control's own content, so every box in the row is identical. */
.filter-field input,
.filter-field select {
  font-size: var(--text-sm);
  font-family: var(--font-body);
  color: var(--ink);
  background: var(--ground);
  padding: 8px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  width: 100%;
  min-width: 0;
}

/* One height for every control in the row, inputs and button alike. Without it
   the button sizes from its own font and padding and lands a pixel or two off
   the boxes beside it, which is visible once they share a bottom edge. */
.filter-field input,
.filter-field select,
.filter-actions button { min-height: 40px; }
/* The native dropdown arrow is pinned to the border edge — padding-right moves
   the option text but not the arrow, so the glyph crowds the box side however
   much room the field has. Dropping the native appearance and painting --caret
   ourselves puts the arrow on the same spacing scale as everything else, and
   makes it the fold chevron rather than a UA glyph. */
.filter-field select {
  -webkit-appearance: none;
  appearance: none;
  padding-right: 34px;
  background-image: var(--caret);
  background-repeat: no-repeat;
  background-position: right var(--sp-3) center;
}

.filter-field input:focus,
.filter-field select:focus {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
  border-color: var(--focus-ring);
}
.filter-field input::placeholder { color: var(--ink-faint); }

/* Mobile Safari/Chrome render <input type="date"> at a fixed intrinsic width and
   center the value, ignoring width:100%/min-width:0. Stripping the native
   appearance makes the field a plain box that fills its column (so it can't spill
   past the card) and left-aligns the value. Scoped to touch; desktop keeps its
   native calendar picker. */
@media (hover: none) {
  /* (The 40px floor that keeps an EMPTY date input from collapsing to a sliver
     once the native chrome is stripped is now unconditional, above — the grid
     needs every cell the same height on desktop too.) */
  .filter-field input[type="date"] {
    -webkit-appearance: none;
    appearance: none;
  }
  .filter-field input[type="date"]::-webkit-date-and-time-value {
    text-align: left;
    margin: 0;
  }
  /* Comfortable touch targets for the pager and the filter disclosure. */
  .page-btn { min-height: 40px; }
  .filter-summary { min-height: 40px; }
}

/* Apply is a grid cell like any other, so the button fills it rather than
   sizing to its own label — that is what makes it the same size as To and
   Category rather than a small tab parked at the end of the row. */
.filter-actions { display: flex; gap: var(--sp-3); align-items: center; }

.filter-actions button {
  background: var(--accent);
  color: var(--accent-contrast);
  border: none;
  padding: 8px 18px;
  border-radius: var(--radius-sm);
  width: 100%;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  cursor: pointer;
}
.filter-actions button:hover { background: var(--accent-strong); }

.clear-link { font-size: var(--text-sm); font-weight: 600; color: var(--ink-muted); }

.active-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-5);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.filter-chip {
  display: inline-block;
  background: var(--accent-soft);
  color: var(--accent-strong);
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-family: var(--font-mono);
}

/* --- Pagination ----------------------------------------------------------- */

.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  margin: var(--sp-7) 0 var(--sp-4);
}

.page-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  text-decoration: none;
  white-space: nowrap;
}
a.page-btn:hover { border-color: var(--accent); color: var(--accent-strong); }

.page-btn.is-disabled {
  color: var(--ink-faint);
  border-color: var(--line);
  opacity: 0.55;
  cursor: default;
}

.page-status {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--ink-muted);
  white-space: nowrap;
}

/* --- Episode cards -------------------------------------------------------- */

.episode-list { display: flex; flex-direction: column; gap: var(--sp-3); }

.episode-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.episode-card:hover { border-color: var(--line-strong); box-shadow: var(--shadow-2); }

.episode-card summary {
  cursor: pointer;
  list-style: none;
  padding: var(--sp-4) var(--sp-5);
}
.episode-card summary::-webkit-details-marker { display: none; }

.ep-row {
  display: grid;
  grid-template-columns: var(--ep-rail) 1fr auto;
  gap: var(--sp-4);
  align-items: start;
}

.ep-when {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-top: 3px;
}
.ep-date {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
}
.ep-duration {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xs);
  color: var(--ink-faint);
  white-space: nowrap;
}

.ep-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-xl);
  line-height: 1.2;
  margin: 0;
}

/* Named participants (Anpanman, Kook, guests) — who-is-in-it metadata. */
.ep-people {
  margin: 2px 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}

/* Summaries are multi-paragraph, so the clamp lives on the lead paragraph, not
   on the wrapper (-webkit-box clamping only works on a single text block). */
.ep-blurb {
  margin: var(--sp-2) 0 0;
  color: var(--ink-muted);
  max-width: var(--w-prose);
}
.ep-blurb > p { margin: 0; }
/* Collapsed card: lead paragraph only, clamped to 3 lines, so card height is
   independent of how many paragraphs the summary has. */
.ep-blurb > p ~ p { display: none; }
.ep-blurb > p:first-child {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}
/* Expanded card shows the full lede above the analysis. */
.episode-card details[open] .ep-blurb > p {
  display: block;
  -webkit-line-clamp: unset;
  line-clamp: unset;
  overflow: visible;
}
.episode-card details[open] .ep-blurb > p ~ p { margin-top: var(--sp-2); }

.ep-chevron {
  color: var(--ink-faint);
  padding-top: 6px;
  transition: transform 0.18s ease;
}
.episode-card details[open] .ep-chevron { transform: rotate(90deg); }

.episode-full {
  border-top: 1px solid var(--line);
  /* Left pad = card padding + rail + row gap, so the analysis starts under the
     title column above it (the date rail sits to its left), not the card edge. */
  padding: 0 var(--sp-5) var(--sp-5) calc(var(--ep-rail) + var(--sp-4) + var(--sp-5));
}

.permalink { margin: var(--sp-5) 0 0; font-size: var(--text-sm); font-weight: 600; }

/* "Listen to full episode" CTA — sits right after the summary (top of the
   expanded block on a list card; below the lede on the detail page). */
.listen-link { margin: var(--sp-4) 0; }
.listen-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--accent);
  text-decoration: none;
}
.listen-cta:hover { color: var(--accent-strong); text-decoration: underline; }
.listen-arrow { flex: none; transition: transform 0.15s ease; }
.listen-cta:hover .listen-arrow { transform: translate(1px, -1px); }

/* --- Copy-as-Markdown ----------------------------------------------------- */

/* The control rides the participants row on both pages, right-aligned, so it
   never takes its own row and is visible without expanding. Detail page: the
   meta row. List card: the participants row inside the (always-visible)
   <summary>, so it shows on collapsed cards too. */
.md-copy { position: relative; }

.episode-page .episode-meta {
  flex-wrap: wrap;
  align-items: center;
}
.episode-page .episode-meta .md-copy { margin-left: auto; }

/* List-card participants row: name(s) on the left, copy control pinned right. */
.ep-people-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  margin: 2px 0 0;
}
.ep-people-row .ep-people { margin: 0; }
.ep-people-row .md-copy { margin-left: auto; }

/* Ghost button: accent glyph + inline label on surface, not an accent fill — the
   primary interactive weight stays on links and the filter's Apply button. The
   label ("Copy as Markdown") rides inline on every device so the action is legible
   without hover; the fuller purpose ("paste into an LLM") stays in the aria-label. */
.md-copy-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  justify-content: center;
  background: var(--surface);
  color: var(--accent);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--sp-2) var(--sp-3);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.md-copy-btn:hover {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
}
.md-copy-btn.is-copied {
  background: var(--paint-green-bg);
  border-color: var(--paint-green-fg);
  color: var(--paint-green-fg);
}

/* Icon swaps to a check on success; the label swaps to "Copied!" (also announced
   via aria-live for assistive tech). */
.md-copy-icon,
.md-copy-check { flex: none; }
.md-copy-check { display: none; }
.md-copy-btn.is-copied .md-copy-icon { display: none; }
.md-copy-btn.is-copied .md-copy-check { display: inline-flex; }

/* Inline CTA label — the button reads "Copy as Markdown" / "Copied!"; display
   face, uppercase, matching the other buttons. Shown on every device (no tooltip). */
.md-copy-label {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-xs);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* Touch devices get a larger tap target. */
@media (hover: none) {
  .md-copy-btn { min-height: 40px; }
}

/* Kept in the DOM (not display:none) and merely hidden so the execCommand
   fallback can select it where the async Clipboard API is absent. A 1px clipped
   box (not left:-9999px) so it can never add to horizontal scroll; the copy JS
   reads .value and calls .select(), which grab the full text regardless of the
   rendered size, so copying still works. */
.md-copy-source {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
}

/* --- Analysis blocks ------------------------------------------------------ */

.analysis-section { margin-top: var(--sp-5); }

.takeaways-list {
  margin: 0;
  padding: 0;
  list-style: none;
  max-width: var(--w-prose);
}
.takeaways-list li {
  position: relative;
  padding-left: var(--sp-5);
  margin-bottom: var(--sp-2);
}
.takeaways-list li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 0.62em;
  width: 8px;
  height: 2px;
  background: var(--accent);
}

/* --- Collapsible analysis sub-sections (nested accordion) ----------------- */
/* Detailed Discussion, Watch Items, Open Questions fold shut by default on the
   list so an expanded episode leads with Summary + Key Takeaways; the permalink
   page renders them open. Native <details>; chevron mirrors the card's. */
.analysis-fold { margin-top: var(--sp-5); }

.fold-summary {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  cursor: pointer;
  list-style: none;
  padding: var(--sp-2) 0;
}
.fold-summary::-webkit-details-marker { display: none; }
.fold-summary:hover .fold-title { color: var(--ink); }

/* Same research-report label as h3, but the trailing hairline is an explicit
   flex spacer (.fold-rule) so the chevron sits at the row's right edge. The
   global h3::after hairline is cancelled so it doesn't double up. */
.fold-title {
  margin: 0;
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
}
.fold-title::after { content: none; }
.fold-count {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-faint);
}
.fold-rule { flex: 1; height: 1px; background: var(--line); }
.fold-chevron { flex: none; color: var(--ink-faint); transition: transform 0.18s ease; }
.analysis-fold[open] .fold-chevron { transform: rotate(90deg); }

/* Revealed content is indented under a quiet rule so it reads as one level down
   from the card, not a second top-level section. */
.fold-body {
  margin-top: var(--sp-2);
  padding: var(--sp-3) 0 var(--sp-1) var(--sp-4);
  border-left: 2px solid var(--line);
}

.topic-block {
  margin-bottom: var(--sp-4);
  padding: var(--sp-4);
  background: var(--surface-inset);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* Topic-level accordion inside the Detailed Discussion fold: each topic card
   collapses so an opened section reads as a scannable topic index
   (Section -> Topic -> Points). The card look is inherited from .topic-block;
   the h4 title is the disclosure, distinct from the uppercase section label. */
.topic-summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  list-style: none;
}
.topic-summary::-webkit-details-marker { display: none; }
.topic-summary .topic-title { margin: 0; }
.topic-summary:hover .topic-title { color: var(--accent); }
.topic-count {
  flex: none;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  color: var(--ink-faint);
}
.topic-chevron {
  flex: none;
  margin-left: auto;
  color: var(--ink-faint);
  transition: transform 0.18s ease;
}
.topic-fold[open] .topic-chevron { transform: rotate(90deg); }
.topic-body { padding-top: var(--sp-3); }

.points-list { margin: 0; padding: 0; list-style: none; }

/* Each point carries its epistemic status as a colored stripe + badge */
.point {
  margin-bottom: var(--sp-3);
  padding: 2px 0 2px var(--sp-3);
  border-left: 3px solid var(--line-strong);
}
.point:last-child { margin-bottom: 0; }
.point-tag-confirmed { border-left-color: var(--tag-confirmed-fg); }
.point-tag-company-guidance { border-left-color: var(--tag-guidance-fg); }
.point-tag-speculation { border-left-color: var(--tag-speculation-fg); }
.point-tag-rumor { border-left-color: var(--tag-rumor-fg); }
.point-tag-disagreement { border-left-color: var(--tag-disagreement-fg); }

.point-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
}
.point .speaker { font-weight: 700; font-size: var(--text-sm); }
.point-meta .ts { margin-left: auto; }
.point-text { margin: 2px 0 0; max-width: var(--w-prose); }

.watch-list, .questions-list { margin: 0; padding: 0; list-style: none; }

.watch-item, .question-item {
  margin-bottom: var(--sp-3);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--line);
}
.watch-item:last-child, .question-item:last-child {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: none;
}

.item-text { margin: 0 0 var(--sp-1); max-width: var(--w-prose); }

.item-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  font-size: var(--text-sm);
}
.item-meta .speaker { font-weight: 600; color: var(--ink-muted); }
.item-meta .ts { margin-left: auto; }

.timing-chip {
  display: inline-block;
  background: var(--accent-soft);
  color: var(--accent-strong);
  /* 1px sufficed for a one-line pill; a wrapped multi-line box needs real
     breathing room above and below the text. */
  padding: var(--sp-1) 10px;
  /* Named a chip, but it is an inner block (DESIGN.md): the value wraps to
     several lines, and a pill radius on a 4-line box bulges into a stadium —
     worst on mobile, where nearly every value wraps. */
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  /* Values are often full sentences (~130 chars); wrap them instead of forcing
     the card past the viewport. line-height gives multi-line values room. */
  white-space: normal;
  overflow-wrap: anywhere;
  line-height: 1.45;
  max-width: 100%;
}

.ts {
  color: var(--ink-faint);
  font-size: var(--text-xs);
  white-space: nowrap;
}

/* --- Badges & chips ------------------------------------------------------- */

.tag-badge, .cat-chip {
  display: inline-block;
  padding: 1px 9px;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.tag-confirmed { background: var(--tag-confirmed-bg); color: var(--tag-confirmed-fg); }
.tag-company-guidance { background: var(--tag-guidance-bg); color: var(--tag-guidance-fg); }
.tag-speculation { background: var(--tag-speculation-bg); color: var(--tag-speculation-fg); }
.tag-rumor { background: var(--tag-rumor-bg); color: var(--tag-rumor-fg); }
.tag-disagreement { background: var(--tag-disagreement-bg); color: var(--tag-disagreement-fg); }
.tag-none { background: var(--tag-none-bg); color: var(--tag-none-fg); }

.cat-chip.cat-financing { background: var(--cat-financing-bg); color: var(--cat-financing-fg); }
.cat-chip.cat-launch { background: var(--cat-launch-bg); color: var(--cat-launch-fg); }
.cat-chip.cat-regulatory { background: var(--cat-regulatory-bg); color: var(--cat-regulatory-fg); }
.cat-chip.cat-commercial { background: var(--cat-commercial-bg); color: var(--cat-commercial-fg); }
.cat-chip.cat-manufacturing { background: var(--cat-manufacturing-bg); color: var(--cat-manufacturing-fg); }

/* --- Episode permalink page ----------------------------------------------- */

.back-link { margin: 0 0 var(--sp-4); font-size: var(--text-sm); font-weight: 600; }

.episode-page {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--sp-6) var(--sp-7) var(--sp-7);
  box-shadow: var(--shadow-1);
}

.episode-page .episode-meta {
  display: flex;
  gap: var(--sp-4);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  color: var(--ink-muted);
  margin-bottom: var(--sp-4);
}

.episode-lede {
  font-size: var(--text-lg);
  line-height: 1.6;
  color: var(--ink);
  max-width: var(--w-prose);
  margin: 0 0 var(--sp-2);
}

.transcript-details summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--accent);
  padding: var(--sp-2) 0;
}

.transcript-scroll {
  max-height: 480px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-inset);
  padding: var(--sp-4);
  margin-top: var(--sp-2);
}

.transcript-text {
  white-space: pre-wrap;
  word-wrap: break-word;
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.6;
}

.episode-footnote {
  margin-top: var(--sp-7);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
}
.episode-footnote code { font-size: inherit; }

/* --- Watch page ----------------------------------------------------------- */

/* Satellite production chart (masthead of /watch).
   A stacked step area, not three lines: Total is Shipped + In production, so
   stacking states the arithmetic instead of drawing a sum over its own addends,
   and the top edge of the stack IS the total. It steps rather than slopes
   because the underlying data is disclosures, not measurements — a number holds
   until AST restates it, and a diagonal would draw values nobody ever gave. */
.sat-figure {
  margin: 0 0 var(--sp-6);
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}

.sat-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
}

.sat-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

/* Identity is never color-alone: each key wears its own swatch AND its name. */
.sat-key { display: inline-flex; align-items: center; gap: var(--sp-1); }
.sat-key::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 2px;
  border: 1.5px solid currentColor;
}
.sat-key-lost::before { background: var(--sat-lost-fill); color: var(--sat-lost-line); }
.sat-key-in_orbit::before { background: var(--sat-in_orbit-fill); color: var(--sat-in_orbit-line); }
.sat-key-awaiting_launch::before {
  background: var(--sat-awaiting_launch-fill);
  color: var(--sat-awaiting_launch-line);
}
.sat-key-shipped::before { background: var(--sat-shipped-fill); color: var(--sat-shipped-line); }
.sat-key-in_production::before {
  background: var(--sat-in_production-fill);
  color: var(--sat-in_production-line);
}
.sat-key-total::before {
  border: none;
  height: 2px;
  border-radius: 0;
  background: var(--sat-total-line);
}
/* The Target key is a rotated square in ink-faint — the only legend swatch with
   no band colour, because a target is not a series: any band can carry one, and
   giving the key one band's hue would say otherwise. Shape carries it instead,
   which is the same distinction the diamonds make against the dots. */
.sat-key-target::before {
  width: 8px;
  height: 8px;
  border-radius: 1px;
  transform: rotate(45deg);
  background: var(--surface);
  color: var(--ink-faint);
}

/* The tier-coincidence note, inside the Assumptions fold. (There is no on-page
   legend for the tier dashes any more — 2px strokes at 60% opacity over
   hatching cannot be told apart, so a key row asked readers to decode a channel
   that does not decode; the tooltip, the forward table and the fold's prose are
   where a tier reaches the reader now. See DESIGN.md.) Its swatch is a real
   .sat-fwd-edge polyline rather than a picture of one, in neutral ink because a
   tier is not a series — any band can be drawn at any tier, exactly the reason
   the Target key has no band colour either. */
.sat-tier-swatch {
  width: 24px;
  height: 9px;
  flex: none;
  overflow: visible;
}
/* Pinned to the FIRST line of the note, not centred on it: the note runs to
   four or five lines, and `align-items: center` left the swatch floating in the
   middle of a paragraph with nothing next to it. The margin is the offset onto
   the first line's optical centre. */
.sat-tier-note .sat-tier-swatch {
  align-self: flex-start;
  margin-top: 0.42em;
}
.sat-tier-swatch .sat-fwd-edge, .sat-tier-swatch .sat-floor-tick {
  stroke: var(--ink-muted);
}

/* Two tiers on one line, said in words because it cannot be said by drawing:
   the fold's one paragraph that carries a swatch, the two real strokes stacked
   exactly as the chart stacks them. Mono at the counts' own size, so the
   geometry claim in it reads as measurement rather than commentary. */
.sat-tier-note {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  /* --sp-3 like the .sat-note paragraphs around it in the fold, not the --sp-2
     it wore under the plot, so the fold keeps one vertical rhythm. */
  margin: var(--sp-3) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 1.5;
  color: var(--ink-muted);
  max-width: var(--w-prose);
}
.sat-tier-note strong { color: var(--ink); font-weight: 600; }

/* The hover layer (static/chart.js). Everything here is inert until the script
   adds .is-hovering, so the no-JS render is byte-identical to what it was: the
   crosshair sits at x=0 at zero opacity and the tooltip carries `hidden`.
   The wrapper is the positioning context for the tooltip, which is HTML rather
   than SVG <text> because it needs real type, wrapping and a shadow. */
.sat-chart-viewport {
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-inline: contain;
  scrollbar-width: thin;
}
.sat-chart-viewport:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The server renders genuinely wider geometry at the SAME height (1140x250
   wide, 760x300 narrow). That is the x-axis zoom: CSS must not enlarge a
   smaller SVG here, because width:auto + height:auto would zoom the y-axis too.
   Fixed canvas widths preserve the server-rendered coordinates and keep the
   tooltip payload in exact agreement with the SVG. */
.sat-plot { position: relative; }
.sat-plot-wide { width: 1140px; }
.sat-plot-narrow { width: 760px; }

.sat-pan-hint {
  margin: var(--sp-1) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-faint);
  text-align: right;
}

/* Two geometries, one shown per breakpoint. NOT two stylesheets over one SVG:
   a Q1-2027 horizon roughly doubles the x-span, and on a 390px viewport that
   leaves eleven months of real data in about half of an already-short plot. No
   type scaling fixes a shape problem. app.satellite_chart is pure and
   parameterised on width/height, so /watch calls it twice — 1140x250 and
   760x300 — and each geometry picks its own margins, its own tick density and
   its own aspect. The cost is roughly double the chart markup, which is small
   beside the ~300 ledger rows already on this page.

   Both canvases retain their established height, so scrolling changes only the
   horizontal scale and never makes the y-axis taller. */
.sat-plot-narrow { display: none; }

.sat-chart {
  display: block;
  width: 100%;
  height: auto;
  margin-top: var(--sp-2);
}

/* Recessive by construction: it is a pointer, not data, so it takes the grid's
   own weight and never more ink than the series it crosses. */
.sat-crosshair {
  stroke: var(--line-strong);
  stroke-width: 1;
  opacity: 0;
  pointer-events: none;
}
.sat-plot.is-hovering .sat-crosshair { opacity: 1; }

/* The markers on the hovered date grow rather than change colour — colour is
   spoken for (it is the band), and size is the one channel still free. */
.sat-dot.is-active { r: 5.5px; }

.sat-tip {
  position: absolute;
  top: var(--sp-3);
  /* chart.js sets --sat-tip-x to the hovered date's position as a percentage of
     the plot width — the SVG has no letterboxing (viewBox ratio + height:auto),
     so a fraction of the viewBox is the same fraction of the rendered box. A
     property rather than `left` inline, because the overlay breakpoint below
     positions this box a different way and an inline `left` would beat it. The
     offset keeps the box off the crosshair; .is-flipped puts it on the other
     side near the right edge. */
  left: var(--sat-tip-x, 0);
  transform: translateX(var(--sp-2));
  z-index: 2;
  /* 148px held four two-part rows (name, number). A projected row is three
     parts — name, tier, number — and at 148 every one of them wrapped: "In
     production" over two lines, "run-rate" split across a hyphen, "≥ 0" with
     the 0 on its own line, and a box tall enough to run off the bottom of the
     figure. Nothing here wraps now (see the nowrap rules below); this is the
     width that lets it not. */
  min-width: 216px;
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-2);
  /* It follows the pointer around; catching the pointer would make it chase
     itself. */
  pointer-events: none;
}
.sat-tip[hidden] { display: none; }
.sat-tip.is-flipped { transform: translateX(calc(-100% - var(--sp-2))); }
/* Dashed on a target date, the same distinction the dotted projections draw:
   this box is describing something promised, not something disclosed. */
.sat-tip.is-target { border-style: dashed; }

.sat-tip-date {
  margin: 0 0 var(--sp-2);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

.sat-tip-rows { display: grid; gap: 3px; margin: 0; }

/* Nothing in a row wraps. A wrapped band name or a hyphenated "run-rate" turns
   five rows into nine and pushes the box past the bottom of the figure, and a
   number that wraps away from its own label is unreadable besides. */
.sat-tip-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  font-size: var(--text-xs);
  white-space: nowrap;
}
/* Total is what the bands sum to, so it reads as their footing rather than as a
   sixth row. */
.sat-tip-row-total {
  margin-top: var(--sp-1);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
}
.sat-tip-row dt { color: var(--ink-muted); }
/* Two cells, written separately by the script: the tier and the number. One
   element holding "≥ 0 floor" would mean the script composed a string, and it
   states nothing it was not handed. The tier sits BEFORE the value so the
   numbers still right-align down the column. */
.sat-tip-row dd {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin: 0;
}
.sat-tip-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--ink);
}
/* Carried forward, not restated on this date — the same distinction the hollow
   markers and the greyed table cells make, in the same ink. */
.sat-tip-value.is-carried { color: var(--ink-faint); font-weight: 400; }

/* The evidence tier behind a projected number. Quiet: it qualifies the figure
   beside it rather than competing with it, and on a disclosed date it is empty. */
.sat-tip-tier {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-faint);
}

.sat-tip-date {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  white-space: nowrap;
}
/* "projected" on the tooltip's own heading line, so the first thing read about a
   forward date says what kind of date it is. */
.sat-tip-tag {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-faint);
}
.sat-tip-tag[hidden] { display: none; }

.sat-tip-note {
  margin: var(--sp-3) 0 0;
  font-size: var(--text-xs);
  line-height: 1.35;
  color: var(--ink-faint);
}
.sat-tip-note[hidden] { display: none; }
/* The floor caveat is the one line in this box that changes what a number MEANS
   rather than annotating it, so it is the only note that carries body ink. */
.sat-tip-floor { color: var(--ink-muted); max-width: 34ch; }
.sat-tip-projected + .sat-tip-floor { margin-top: var(--sp-1); }

/* The tooltip's other body, shown on a target date instead of the band rows.
   Every block is server-rendered and hidden; chart.js unhides the ones the
   hovered date names (see the comment in templates/watch.html), so nothing here
   is composed in the browser. */
.sat-tip-rows[hidden],
.sat-tip-targets[hidden],
.sat-tip-target[hidden] { display: none; }
.sat-tip-targets { display: grid; gap: var(--sp-3); }
.sat-tip-target-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin: 0;
  font-size: var(--text-xs);
}
.sat-tip-target-head strong {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
/* The stated period in the source's own units ("Q1 2027"), which is the thing a
   reader must not mistake for a deadline. */
.sat-tip-period {
  font-family: var(--font-mono);
  color: var(--ink-muted);
}
.sat-tip-target-note {
  margin: var(--sp-1) 0 0;
  max-width: 34ch;
  font-size: var(--text-xs);
  line-height: 1.35;
  color: var(--ink-muted);
}
.sat-tip-target-src {
  margin: var(--sp-1) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-faint);
}

.sat-grid { stroke: var(--line); stroke-width: 1; }

/* Band fills. Class names are the band keys straight from app.SATELLITE_BANDS,
   so a new band means: the two tokens above, plus .sat-band-*, .sat-key-*::before,
   .sat-edge-*, .sat-dot-*, .sat-dot-*.is-carried and .sat-value-* here, plus a
   band_labels entry in templates/watch.html (a missing one renders a blank
   legend chip and a blank table header, silently). */
.sat-band-lost { fill: var(--sat-lost-fill); }
.sat-band-in_orbit { fill: var(--sat-in_orbit-fill); }
.sat-band-awaiting_launch { fill: var(--sat-awaiting_launch-fill); }
.sat-band-shipped { fill: var(--sat-shipped-fill); }
.sat-band-in_production { fill: var(--sat-in_production-fill); }

/* A band's top edge is stroked unless app.satellite_chart suppressed it for
   being mostly zero (see there) — the topmost is always stroked, because its top
   edge IS the total, which is why it takes the total's ink and not its own
   band's hue. The interior boundary inside the green family is a hairline: it
   separates two shades of one thing rather than two series, and at full weight
   the green region reads as competing lines. That is `in_orbit`'s top; the
   shipped edge above it stays full weight because it is the boundary with a
   different colour family. */
.sat-edge {
  fill: none;
  stroke-width: var(--sat-stroke);
  stroke-linejoin: round;
  stroke-linecap: round;
}
.sat-edge-lost { stroke: var(--sat-lost-line); }
/* Full weight, not the half-weight hairline it carried while `awaiting_launch` was
   the second green: a hairline was right for an interior subdivision of one hue,
   and this boundary now separates two colour families. It is also the on-orbit
   count, a headline number with its own direct-labelled markers. */
.sat-edge-in_orbit { stroke: var(--sat-in_orbit-line); }
.sat-edge-awaiting_launch { stroke: var(--sat-awaiting_launch-line); }
.sat-edge-shipped { stroke: var(--sat-shipped-line); }
.sat-edge-in_production { stroke: var(--sat-total-line); }

/* A filled dot is a number AST restated on that date; hollow is one carried
   forward from an earlier disclosure. The surface-colored stroke doubles as the
   ring that keeps a dot readable where it sits on a band. */
.sat-dot { stroke: var(--surface); stroke-width: var(--sat-stroke); }
.sat-dot-lost { fill: var(--sat-lost-line); }
.sat-dot-in_orbit { fill: var(--sat-in_orbit-line); }
.sat-dot-awaiting_launch { fill: var(--sat-awaiting_launch-line); }
.sat-dot-shipped { fill: var(--sat-shipped-line); }
.sat-dot-in_production { fill: var(--sat-total-line); }
.sat-dot.is-carried { fill: var(--surface); }
.sat-dot-lost.is-carried { stroke: var(--sat-lost-line); }
.sat-dot-in_orbit.is-carried { stroke: var(--sat-in_orbit-line); }
.sat-dot-awaiting_launch.is-carried { stroke: var(--sat-awaiting_launch-line); }
.sat-dot-shipped.is-carried { stroke: var(--sat-shipped-line); }
/* Projections: everything past the last disclosure. DOTTED and SLOPED against
   the solid stepped edges above, because either signal alone is ambiguous — a
   dotted step reads as a disclosure drawn faintly, a solid slope as a
   measurement taken between two readings. Nothing here is filled: the bands sum
   to the total at every plotted point and no guidance covers all four, so a
   future fill could only be invented.
   `stroke-dasharray` is in viewBox units like every other length in this chart,
   so it scales with the SVG and the narrow breakpoint scales it back up. */
.sat-projection {
  fill: none;
  stroke-width: var(--sat-stroke);
  stroke-dasharray: 1 5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sat-projection-lost { stroke: var(--sat-lost-target); }
.sat-projection-in_orbit { stroke: var(--sat-in_orbit-target); }
.sat-projection-awaiting_launch { stroke: var(--sat-awaiting_launch-target); }
.sat-projection-shipped { stroke: var(--sat-shipped-target); }
.sat-projection-in_production { stroke: var(--sat-in_production-target); }

/* --- The forward stack ----------------------------------------------------
   Past the last disclosure the whole stack continues: four bands, the same
   cumulative construction, the same identity. What changes is HOW it is drawn,
   and every difference here encodes something.

   THE FILL. The rule used to be "nothing out there is filled", and it was right
   while `in_orbit` was the only projected series — filling would have invented
   the other three bands. With all four projected that is no longer true, but a
   solid fill still READS as fact, so the forward bands take a hatch: legible as
   a shape, visibly not the historical fill, and carrying each band's own hue so
   the stack is still followable across the seam. Reduced-opacity flat fill was
   the alternative and was rendered and compared; it read as the same fill drawn
   faintly — a lighter fact — where the hatch reads as a different KIND of
   region. The pattern is declared per band per geometry in the template (ids
   need namespacing, both SVGs are in one document); only the stroke colour is
   here, so no hue is spent in markup.

   No new token: the hatch strokes in the band's own line colour, which is the
   colour that band's disclosed EDGE already draws in. That is why
   .sat-hatch-in_production takes the total's ink rather than copper — same
   reason .sat-edge-in_production does. */
.sat-fwd-band { stroke: none; }
.sat-hatch { stroke-width: 0.9; }
.sat-hatch-lost { stroke: var(--sat-lost-line); }
.sat-hatch-in_orbit { stroke: var(--sat-in_orbit-line); }
.sat-hatch-awaiting_launch { stroke: var(--sat-awaiting_launch-line); }
.sat-hatch-shipped { stroke: var(--sat-shipped-line); }
.sat-hatch-in_production { stroke: var(--sat-total-line); }

/* The forward edges. Colour comes from the band's own .sat-edge-<band> rule —
   the element carries both classes — so a projection is visibly the continuation
   of the line it leaves, and adding a band costs no rule here. What this adds is
   the DASH, and the dash is the evidence tier:

     scheduled  long dash, full weight  — a booked flight, a date on a manifest
     guided     dotted, full weight     — a statement AST made about its future
     implied    dash-dot + floor ticks  — the minimum a stronger claim forces
     run-rate   dotted, muted and thin  — our least-squares fit, nobody's claim
     held       sparse, fainter still   — the last actual, carried flat

   The two weakest are muted by stroke-opacity rather than by a mixed-down
   colour token: opacity is not a colour, it composes with either theme for free,
   and it keeps one token per band per role instead of a second copy of each
   band's hue that would have to be maintained beside the first.

   Four weights that a reader can separate without the legend was the
   requirement. Dash pattern carries three of them and weight carries the
   fourth; the muted pair are never adjacent on live data, since `in_orbit` is
   target-driven and `queue_max` is always run-rate. */
.sat-fwd-edge {
  fill: none;
  stroke-width: var(--sat-stroke);
  stroke-linejoin: round;
}
/* 7-and-4, not the 9-and-5 this started at: a `scheduled` run can be very short
   — the booked Falcon 9 is fourteen days past the last disclosure, about 20
   units — and a dash longer than the segment draws one mark that reads as a
   solid line. Two dashes is the minimum that reads as dashed. */
.sat-fwd-edge.is-scheduled { stroke-dasharray: 7 4; stroke-linecap: butt; }
.sat-fwd-edge.is-guided { stroke-dasharray: 1 5; stroke-linecap: round; }
.sat-fwd-edge.is-implied { stroke-dasharray: 5 3 1 3; stroke-linecap: butt; }
.sat-fwd-edge.is-run-rate {
  stroke-dasharray: 1 5;
  stroke-linecap: round;
  stroke-width: calc(var(--sat-stroke) * 0.75);
  stroke-opacity: 0.5;
}
.sat-fwd-edge.is-held {
  stroke-dasharray: 1 8;
  stroke-linecap: round;
  stroke-width: calc(var(--sat-stroke) * 0.75);
  stroke-opacity: 0.4;
}

/* Up-ticks hanging above an implied edge: the true value lies at or ABOVE this
   line. A dash pattern alone cannot say "floor", and the case that matters here
   is the one where the dash is not even visible — `awaiting_launch` sits at zero
   across nearly the whole forward span, so its edge lies exactly under the
   guided edge it rests on and these ticks are the only ink left to carry the
   claim. Half weight, because they are an annotation on a line and not a second
   line; butt caps so a 4.5-unit tick is 4.5 units long. */
.sat-floor-tick {
  stroke-width: calc(var(--sat-stroke) / 2);
  stroke-linecap: butt;
}

/* The seam. Every other forward signal says "this mark is projected" one mark at
   a time; none of them says where the boundary is, and the boundary is the first
   thing a reader has to find. Hairline in the grid's own ink so it separates
   without competing, with the word beside it — a rule with no label would read
   as a gridline that lost its way. */
.sat-divider {
  stroke: var(--line-strong);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}
.sat-divider-label {
  font-family: var(--font-mono);
  font-size: var(--sat-axis-text);
  fill: var(--ink-faint);
  text-anchor: start;
}

/* The projection's own end value. Muted the same way its edge is, so it cannot
   be read as one of the current-value labels at the last disclosure. */
.sat-fwd-value { fill-opacity: 0.6; }

/* The stated period, when AST gave one rather than a day ("early 2027"). Solid
   where the projection is dotted — it is not a path the number travels, it is
   how wide the window is — and end-capped at the period start so it reads as a
   range and not as a value already achieved. */
.sat-target-range, .sat-target-cap {
  /* Half weight. At full weight it read as a series holding that value across
     the quarter, which is the opposite of what a range says. */
  stroke-width: calc(var(--sat-stroke) / 2);
  stroke-linecap: butt;
}
.sat-target-range-lost, .sat-target-cap-lost { stroke: var(--sat-lost-target); }
.sat-target-range-in_orbit, .sat-target-cap-in_orbit { stroke: var(--sat-in_orbit-target); }
.sat-target-range-awaiting_launch,
.sat-target-cap-awaiting_launch { stroke: var(--sat-awaiting_launch-target); }
.sat-target-range-shipped, .sat-target-cap-shipped { stroke: var(--sat-shipped-target); }
.sat-target-range-in_production,
.sat-target-cap-in_production { stroke: var(--sat-in_production-target); }

/* A target mark is a hollow DIAMOND. The dots already spend fill on the
   restated/carried distinction, so a third fill state of the same shape would
   read as a third kind of disclosure; a different shape says "different kind of
   claim" instead. Hollow, in the muted projection colour, because a target is
   the least certain mark on the chart. */
.sat-target-mark > rect {
  fill: var(--surface);
  stroke-width: var(--sat-stroke);
}
.sat-target-mark-lost > rect { stroke: var(--sat-lost-target); }
.sat-target-mark-in_orbit > rect { stroke: var(--sat-in_orbit-target); }
.sat-target-mark-awaiting_launch > rect { stroke: var(--sat-awaiting_launch-target); }
.sat-target-mark-shipped > rect { stroke: var(--sat-shipped-target); }
.sat-target-mark-in_production > rect { stroke: var(--sat-in_production-target); }
/* Grows on hover, like .sat-dot.is-active — size is the one channel a mark's
   shape and colour have not already spent. The rect is centred on the origin of
   its own <g>, and an SVG element's CSS transform-origin is 0 0, so a plain
   scale() is a scale about the diamond's middle. */
.sat-target-mark.is-active > rect { transform: scale(1.55); }

.sat-axis-x, .sat-axis-y, .sat-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--sat-axis-text);
}
.sat-axis-x { fill: var(--ink-faint); text-anchor: middle; }
.sat-axis-y { fill: var(--ink-faint); text-anchor: end; }
.sat-value { font-weight: 500; }
.sat-value-lost { fill: var(--sat-lost-line); }
.sat-value-in_orbit { fill: var(--sat-in_orbit-line); }
.sat-value-awaiting_launch { fill: var(--sat-awaiting_launch-line); }
.sat-value-shipped { fill: var(--sat-shipped-line); }
.sat-value-in_production { fill: var(--sat-in_production-line); }
.sat-value-total { fill: var(--sat-total-line); }
/* A target's number, beside its diamond. Muted to the projection colour, like
   everything else out past the last disclosure. */
.sat-target-value-lost { fill: var(--sat-lost-target); }
.sat-target-value-in_orbit { fill: var(--sat-in_orbit-target); }
.sat-target-value-awaiting_launch { fill: var(--sat-awaiting_launch-target); }
.sat-target-value-shipped { fill: var(--sat-shipped-target); }
.sat-target-value-in_production { fill: var(--sat-in_production-target); }

/* The chart's accessible twin. Reuses the page's fold furniture rather than
   inventing a second disclosure idiom. */
.sat-table-fold { margin-top: var(--sp-2); }
.sat-table-fold[open] > .fold-summary .fold-chevron { transform: rotate(90deg); }

.sat-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xs);
}
.sat-table th, .sat-table td {
  padding: var(--sp-1) var(--sp-2);
  text-align: right;
  border-bottom: 1px solid var(--line);
}
.sat-table th { color: var(--ink-faint); font-weight: 500; }
.sat-table th:first-child, .sat-table .sat-td-date { text-align: left; }
.sat-table td { color: var(--ink); }
.sat-table td.is-carried { color: var(--ink-faint); }
.sat-table td.is-derived { color: var(--sat-in_production-line); }

/* The Assumptions fold: the rate comparison plus every other thing the forward
   half rests on. Same furniture as the Data fold below it, deliberately — two
   disclosures about the same chart should not be two idioms — and the same
   collapsed weight, so neither shouts over the plot.

   The rate comparison used to sit OUTSIDE any fold, as a bordered block, on the
   argument that it is the most informative number the projection produces and a
   reader who never opens a fold would never meet it. That was half right and
   half a trap: it said the loudest assumption and none of the quiet ones — the
   least-squares fit through seven points, Lost never being forecast, a "≥" being
   a floor, a target silently dropped — so a reader met the one number that
   sounds like a verdict with no way to reach the ones that qualify it. Together
   in one fold labelled for what they are is the honest arrangement. */
.sat-assumptions-fold { margin-top: var(--sp-3); }
.sat-assumptions-fold[open] > .fold-summary .fold-chevron { transform: rotate(90deg); }

/* Framed as arithmetic, in the page's quiet ink and with no alarm colour:
   nothing here says AST will miss anything, only what its own guidance asks of a
   number it has never guided on. */
.sat-rate {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--ink-muted);
  max-width: var(--w-prose);
}
.sat-rate + .sat-rate { margin-top: var(--sp-2); }
.sat-rate-lead {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
}
.sat-rate-num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--ink);
}

/* Targets are a second table under the first, not extra rows in it: every row
   above is something AST disclosed, every row here is something it says it will
   do, and the two must not scan as one column of numbers. The caption carries
   that distinction in words; the dashed top border carries it in form, matching
   the dotted projections on the chart. */
.sat-target-table {
  margin-top: var(--sp-4);
  border-top: 2px dashed var(--line-strong);
}
.sat-target-table caption {
  padding: var(--sp-2) var(--sp-2) var(--sp-1);
  text-align: left;
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Guidance is prose, so it drops the mono face and the right alignment the
   number columns use — header included, or the column would label itself from
   the wrong edge. */
.sat-target-table th:last-child,
.sat-target-table .sat-td-note {
  text-align: left;
  font-family: var(--font-body);
  font-variant-numeric: normal;
  color: var(--ink-muted);
  line-height: 1.4;
}
/* The date the target is PLOTTED at, under the period AST actually stated. */
.sat-target-table .sat-td-sub {
  display: block;
  color: var(--ink-faint);
}
.sat-target-src { white-space: nowrap; }
/* The evidence tier behind a target — a booked flight or a statement. Quiet, in
   the ramp's lower-case, because it qualifies the row rather than heading it. */
.sat-td-tier { font-variant-numeric: normal; color: var(--ink-faint); }

/* A third table, for the projected rows. Same reason the targets are a second
   one: every row in the first is a number AST disclosed on a date, and not one
   of these was disclosed at all. It borrows the target table's dashed rule and
   caption so "not a disclosure" reads as one idea rather than two.

   Each figure carries its tier under it, and an implied one prints with a "≥".
   Only implied gets a treatment of its own here, because it is the only tier
   that changes what the number IS: the others are levels somebody claims the
   series will reach, and a floor is not a level. */
.sat-forward-table {
  margin-top: var(--sp-4);
  border-top: 2px dashed var(--line-strong);
}
.sat-forward-table caption {
  padding: var(--sp-2) var(--sp-2) var(--sp-1);
  text-align: left;
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.sat-forward-table td { color: var(--ink-muted); }
.sat-forward-table td.is-tier-implied { color: var(--ink); }
.sat-forward-table .sat-td-sub {
  display: block;
  font-size: 0.9em;
  color: var(--ink-faint);
}

.sat-note {
  margin: var(--sp-3) 0 0;
  font-size: var(--text-sm);
  color: var(--ink-muted);
  max-width: var(--w-prose);
}
/* A footnote to a footnote: what was NOT drawn and why. Smaller and fainter than
   .sat-note, because it explains an absence rather than the figures present. */
.sat-note-quiet { font-size: var(--text-xs); color: var(--ink-faint); }

/* The narrow breakpoint swaps the GEOMETRY rather than restyling the wide one.
   What used to be here was a set of scale-ups — 11-unit labels to 22, 2-unit
   strokes to 4, dots and diamonds enlarged — because the 760-unit chart painted
   into ~334px at a 0.44 factor. Those are gone: both canvases are pinned to a
   CSS width equal to their viewBox units and scroll inside .sat-chart-viewport,
   so each paints at 1:1 and its type is already the size it was drawn at.
   They also could not fix what actually broke, which is that a Q1-2027 horizon
   doubles the x-span and leaves the disclosed history in half a short plot; only
   a different shape does that. Tick density is likewise the geometry's own
   decision now (app.satellite_chart picks a stride and drops a label that would
   be clipped at the canvas edge), so the is-minor CSS drop is gone with it. */
@media (max-width: 720px) {
  .sat-figure { padding: var(--sp-3) var(--sp-3) var(--sp-2); }
  .sat-caption { justify-content: flex-start; }
  .sat-plot-wide { display: none; }
  .sat-plot-narrow { display: block; }

  /* The Data fold's tables scroll themselves rather than pushing the PAGE
     sideways. Six mono columns do not fit in 350px and never did — opening the
     fold on a phone has always dragged the whole document 82px to the right —
     and this build adds a third table, so it is fixed here rather than made
     worse. `display: block` is what lets a table overflow its own box instead of
     stretching its container; the alternative is a wrapper element around each
     of the three, which is markup spent on a stylesheet's problem. */
  .sat-table { display: block; overflow-x: auto; }

  /* Overlaid on the chart, not docked below it. Docked, the box was ~230px of
     reflow between the plot and everything under it, paid on every tap; the
     narrow geometry is 300px tall now, so the same box fits ON the plot the way
     it always has on desktop. What cannot work here is the desktop positioning
     itself: absolute at a percentage of the canvas places the box relative to a
     760-unit surface that PANS, so it would sit wherever the tapped date is —
     often entirely inside the clipped part of the canvas.

     So the box is pinned to the part the reader can SEE: its zero-height
     anchor (which precedes the svg precisely so its overflow lands on the plot)
     becomes `position: sticky; left: 0` at the scrollport's width, and the
     tooltip flows inside it — top-left of the visible area, holding still while
     the chart pans underneath. `100cqw` is that width, against
     .sat-chart-viewport, the only element that knows it. The container-type is
     scoped to this breakpoint deliberately — the desktop tooltip is absolutely
     positioned at a PERCENTAGE of the canvas, and containment anywhere in that
     chain would be a second containing block to reason about. The crosshair
     still marks the tapped date even when the box covers it, the box's own
     heading names it, and taps pass straight through (pointer-events: none), so
     scrubbing works under the overlay. --sat-tip-x is simply unused here: the
     stylesheet decides which positioning mode applies (see chart.js). */
  .sat-chart-viewport { container-type: inline-size; }
  .sat-tip-anchor {
    position: sticky;
    left: 0;
    z-index: 3;
    width: 100cqw;
    height: 0;
  }
  .sat-tip,
  .sat-tip.is-flipped {
    position: static;
    transform: none;
    width: max-content;
    max-width: calc(100cqw - 2 * var(--sp-2));
    margin: var(--sp-2) 0 0 var(--sp-2);
  }
}

.stat-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-bottom: var(--sp-6);
}

.stat {
  flex: 1;
  min-width: 140px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--sp-3) var(--sp-4);
  box-shadow: var(--shadow-1);
}
.stat-num {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.8rem;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.stat-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
}

/* Caption under a section heading (e.g. the "Past target date" explainer):
   muted, readable measure, not the italic empty-state treatment. */
.section-note {
  font-style: normal;
  color: var(--ink-muted);
  max-width: var(--w-prose);
  margin-top: calc(-1 * var(--sp-2));
  margin-bottom: var(--sp-4);
}

/* Horizon groups inside Upcoming (Next 90 days / Rest of <year> / <next year>
   and beyond). Plain h3 research-report label plus the mono scent count the
   folds use; grouping beats a flat date sort because "by end of <year>"
   guidance all piles onto the Dec-31 sentinel date. */
.horizon-heading { margin-top: var(--sp-5); }

/* Per-year divider in the History list — years are data, so mono/tabular-nums
   rather than the uppercase letterspaced h3 label style. Used as the summary of
   a .year-fold, so the trailing hairline comes from .fold-rule and the vertical
   rhythm lives on .year-fold, not here. */
.year-heading {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: var(--text-sm);
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-faint);
  margin: 0;
}

/* History is hundreds of rows deep, so each year is itself a disclosure. The
   newest year renders open (that is what a reader scans first); older years are
   closed. Same summary furniture as .analysis-fold. */
.year-fold { margin-top: var(--sp-5); }
.year-fold[open] > .fold-summary .fold-chevron { transform: rotate(90deg); }
.year-fold > .ledger-list { margin-top: var(--sp-2); }

/* --- Ledger list ---------------------------------------------------------- */

/* Sprite holder for glyphs repeated per row (the ledger chevron). Never
   rendered itself; <use href="#id"> pulls the symbol out. */
.svg-sprite { display: none; }

/* The ledger is a list of disclosures, not a table: `detail` is prose (median
   ~260 chars) and was ~77% of this page's text, so it is revealed on demand
   while the collapsed line stays scannable. The panel holds the rows; rows are
   separated by hairlines and clipped by overflow so left rails and hover fills
   stay inside the rounded corners. */
.ledger-list {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
  margin-bottom: var(--sp-5);
  overflow: hidden;
}

.ledger-fold { border-bottom: 1px solid var(--line); }
.ledger-fold:last-child { border-bottom: none; }

.ledger-fold > summary { cursor: pointer; list-style: none; }
.ledger-fold > summary::-webkit-details-marker { display: none; }
.ledger-fold > summary:hover { background: color-mix(in srgb, var(--surface-inset) 55%, transparent); }
/* The panel clips its overflow, so the focus ring is drawn inside the row
   instead of 2px outside it (where it would be cut off at the panel edge). */
.ledger-fold > summary:focus-visible { outline-offset: -3px; }

/* Collapsed line: fixed date rail (shared --ep-rail token, so ledger dates line
   up with the episode log's), event text, chips, chevron. The chevron column is
   reserved even on static rows, so the chips right-align down the whole list. */
.ledger-summary {
  display: grid;
  grid-template-columns: var(--ep-rail) minmax(0, 1fr) auto 14px;
  grid-template-areas: "date event marks chev";
  gap: var(--sp-2) var(--sp-4);
  align-items: center;
  padding: var(--sp-3) var(--sp-4);
}

.ledger-date {
  grid-area: date;
  font-size: var(--text-sm);
  color: var(--ink-muted);
  white-space: nowrap;
}

.ledger-event {
  grid-area: event;
  min-width: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* Chips, plus (on a static row) the source. A plain-text source can be a long
   unbroken URL, so this track must be allowed to collapse and break rather than
   push the row sideways. */
.ledger-marks {
  grid-area: marks;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp-2);
  min-width: 0;
  overflow-wrap: anywhere;
}

.ledger-summary .fold-chevron { grid-area: chev; }
.ledger-fold[open] > .ledger-summary .fold-chevron { transform: rotate(90deg); }

/* Revealed prose indents under the event column (rail + row padding + gap), the
   same alignment trick .episode-full uses, so the reading column is one column. */
.ledger-body {
  padding: 0 var(--sp-4) var(--sp-4) calc(var(--ep-rail) + var(--sp-4) + var(--sp-4));
}
.ledger-detail {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--ink-muted);
  max-width: var(--w-prose);
  overflow-wrap: anywhere;
}
.ledger-source { margin: var(--sp-2) 0 0; }

/* Past-target rows are all pending; a quiet amber left rail (the pending hue)
   groups the section without a red "overdue" alarm. The rail is keyed on the
   ROW, not on the containing list, because the "Last 30 days" spotlight repeats
   these rows outside the past-target section and they must keep their status
   treatment there. */
.ledger-past-target { border-left: 3px solid var(--paint-amber-fg); }

/* --- Reconciled predictions (resolved_by) --------------------------------- */

/* A resolved prediction lives in History because it is settled, but it is not
   itself a confirmed fact — so it takes the green rail (the confirmed hue) the
   way a past-target row takes the pending amber. Grouping, not alarm: it marks
   the handful of History rows that are forecasts-with-outcomes rather than
   recorded events. */
.ledger-resolved { border-left: 3px solid var(--resolved-fg); }

/* Rides the marks track beside the category chip so the state is legible on the
   collapsed line. Same pill geometry as .cat-chip but lowercase and un-spaced:
   this is a state, not a taxonomy label, and the difference should be visible
   at a glance where the two sit together. */
.resolved-mark {
  display: inline-block;
  padding: 1px 9px;
  border-radius: var(--radius-pill);
  background: var(--resolved-bg);
  color: var(--resolved-fg);
  font-size: var(--text-xs);
  font-weight: 700;
  white-space: nowrap;
}

/* The outcome, disclosed under the prediction it settles — the point of the
   whole feature is that the two read together with the original forecast left
   untouched. */
.ledger-resolution {
  margin: var(--sp-3) 0 0;
  font-size: var(--text-sm);
  max-width: var(--w-prose);
  overflow-wrap: anywhere;
}
.resolution-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--resolved-fg);
}
.resolution-date { color: var(--ink-muted); font-size: var(--text-xs); }

.source-plain { color: var(--ink-faint); font-size: var(--text-xs); overflow-wrap: anywhere; }
.source-link { font-weight: 600; white-space: nowrap; font-size: var(--text-xs); }

/* --- Evidence tiers (event_sources) --------------------------------------- */

/* A ledger row's badge says how good its evidence is — the axis `confidence`
   never covered, and the reason ~half the ledger (podcast hosts talking) used to
   render identically to an 8-K. It is neutral on purpose: the category chip
   sitting beside it already carries hue, so authority is encoded as FILL
   DENSITY, which survives greyscale and does not compete with either the
   epistemic paints or the accent. Same pill geometry as .cat-chip so the marks
   track keeps one rhythm; the ramp runs solid ink → solid mid-ink → inset +
   strong border → hairline outline → bare text → dashed. Case moves with it
   too: the first-party/consequential tiers shout, the rest speak quietly. */
.src-badge {
  display: inline-block;
  padding: 1px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  line-height: 1.45;
  white-space: nowrap;
}

.src-tier-filing {
  background: var(--src-filing-bg); color: var(--src-filing-fg);
  border-color: var(--src-filing-line);
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
}
.src-tier-company {
  background: var(--src-company-bg); color: var(--src-company-fg);
  border-color: var(--src-company-line);
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
}
.src-tier-exec {
  background: var(--src-exec-bg); color: var(--src-exec-fg);
  border-color: var(--src-exec-line);
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
}
.src-tier-tracker {
  background: var(--src-tracker-bg); color: var(--src-tracker-fg);
  border-color: var(--src-tracker-line);
  font-weight: 600;
}
.src-tier-news {
  background: var(--src-news-bg); color: var(--src-news-fg);
  border-color: var(--src-news-line);
  font-weight: 600;
}
.src-tier-podcast {
  background: var(--src-podcast-bg); color: var(--src-podcast-fg);
  border-color: var(--src-podcast-line);
  font-weight: 500;
}
.src-tier-unknown {
  background: var(--src-unknown-bg); color: var(--src-unknown-fg);
  border-color: var(--src-unknown-line); border-style: dashed;
  font-weight: 500;
}

/* Corroboration — the most interesting thing this page can show: the same event
   attested by two independent kinds of source. The group keeps the badges
   together when the marks track wraps; the "+" reads as "and", which a bare
   count would not. A third-and-beyond source collapses into .src-corrob. */
.src-group {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-width: 0;
}
.src-plus {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-faint);
}
.src-corrob {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink-muted);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  padding: 0 5px;
}

/* Every attesting source, disclosed in a corroborated row's body. Single-source
   rows (the overwhelming majority) keep the plain .ledger-source line instead,
   so nothing here touches them. */
.ledger-sources { margin: var(--sp-3) 0 0; }
.sources-label {
  margin: 0 0 var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}
.source-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}
.source-list li {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* A pending mark, used ONLY in the "Last 30 days" spotlight. Everywhere else the
   section already determines confidence (DESIGN.md), so a badge would encode
   nothing; there, confirmed and pending rows sit together and it is the whole
   point of the section that a reader can tell them apart. Same pill geometry as
   .resolved-mark, in the pending amber. */
.pending-mark {
  display: inline-block;
  padding: 1px 9px;
  border-radius: var(--radius-pill);
  background: var(--pending-bg);
  color: var(--pending-fg);
  font-size: var(--text-xs);
  font-weight: 700;
  white-space: nowrap;
}

/* --- Recency spotlight ("Last 30 days") ----------------------------------- */

/* First section on /watch, and the only one that mixes confirmed and pending
   rows — so its heading carries the count the way the folds do, and its rows
   carry an explicit .pending-mark. h2 is not a flex container by default (h3
   is), so the count needs its own gap and its own size here. */
.recent-section h2 {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: 0;
}
.recent-section h2 .fold-count { font-size: var(--text-sm); }

/* 5. RESPONSIVE ============================================================ */

@media (max-width: 720px) {
  .header-inner { flex-wrap: wrap; }
  .main-nav { gap: var(--sp-4); }
  .content { padding: var(--sp-5) var(--sp-4) var(--sp-7); }
  .ep-row { grid-template-columns: 1fr auto; }
  .ep-when {
    grid-column: 1 / -1;
    flex-direction: row;
    align-items: baseline;
    gap: var(--sp-3);
    order: -1;
    padding-top: 0;
  }
  /* Rail collapses to a full-width top row here, so drop the desktop indent. */
  .episode-full { padding-left: var(--sp-5); }
  .episode-page { padding: var(--sp-5); }

  /* Two columns here, so the same six cells turn from 3-wide-by-2-tall into
     2-wide-by-3-tall: /watch reads From|To, Section|Category, Source|Apply and
     the episode list Search|From, To|Type, Speaker|Apply. NOTHING spans the row
     — Apply included, so every cell stays the same size at this width too. A
     third of a phone clips a date field's own glyphs ("mm/dd/yyy"); half of one
     fits them whole with no type step-down.
     The 2-up only fits because the touch rule above strips the native date-input
     appearance (-webkit-appearance: none) — that is what lets a date field
     honour width:100% and shrink into half a row instead of rendering at its
     wider intrinsic size (mobile Safari/Chrome otherwise ignore the width and
     the row overflows). */
  .filter-row { grid-template-columns: repeat(2, 1fr); }

  /* Ledger rows: the date rail collapses to its own row above the event, the
     same move .episode-card makes, and the chips drop to a third row so nothing
     competes for width. No horizontal scroll at any width. */
  .ledger-summary {
    grid-template-columns: minmax(0, 1fr) 14px;
    grid-template-areas:
      "date date"
      "event chev"
      "marks marks";
    align-items: start;
  }
  .ledger-marks { justify-content: flex-start; }
  /* Rail is a top row here, so drop the desktop indent under it. */
  .ledger-body { padding-left: var(--sp-4); }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}
