/* ============================================================================
   ANTITUBE — ENGINEERED EDITORIAL (FS-19) + RECORD-RED motion identity (FS-27)
   Dark-first, high-contrast: Linear/Vercel structural restraint with the
   energy a media app needs. One strong variable sans for UI (Instrument
   Sans) + a tight mono for metadata (Martian Mono). Restrained accent,
   semantic status colors carried over from the guard/empty-state idiom.
   Motion is purposeful and quick (150–250ms), easing over distance;
   prefers-reduced-motion is honored. FS-27/FS-28 sanction exactly one
   looping animation family: the camera RECORD blink (the wordmark i-dot +
   the on-stage REC/CUEING state chip, opacity-only → compositor-only),
   which reduces to static under prefers-reduced-motion via the global
   animation kill below. Every FS-27/FS-28 animation touches ONLY transform
   and opacity — never a layout property. Fonts are loaded from the document
   head (preconnect + stylesheet), not via CSS @import.
   Behavioral contracts preserved: stage states, poster→video fade,
   sr-only gesture controls with focus reveal, data-admin-guard idiom.
   MOTION ENGINE HANDOVER: when the client's Motion (framer-motion) engine is
   active it stamps data-motion-engine="js" (body at mount; the player at its
   first directional transition) and drives stage/burst/flash as one-shot
   springs — the keyframes below gate themselves off via :not() and remain the
   complete no-JS / reduced-motion fallback. rec-blink is NOT handed over: the
   sanctioned looping family stays CSS-only, here.
   ========================================================================== */

:root {
  color-scheme: dark;

  /* surface */
  --bg-0:#08090b;                       /* page */
  --bg-1:#0d0f12;                       /* raised panel */
  --bg-2:#13151a;                       /* table / chip fill */
  --line:rgba(233,235,240,0.10);        /* hairline (decorative) */
  --line-strong:rgba(233,235,240,0.22); /* interactive borders */

  /* ink — all text tokens hold ≥4.5:1 on --bg-0 */
  --ink:#eef0f3;                        /* primary */
  --ink-2:#b7bcc5;                      /* secondary  (~9.5:1) */
  --ink-3:#8f96a1;                      /* metadata   (~5.9:1) */

  /* restrained accent + semantic status (guard/empty-state lineage) */
  --accent:#8fd0c6;                     /* cold mint-teal carrier */
  --fault:#e07a63;                      /* alarm (≥4.5:1 on bg) */
  --pass:#a9bd85;                       /* gate pass */
  --rec:#ff3742;                        /* FS-27 camera-record red — the brand motion accent */

  /* type */
  --sans:"Instrument Sans", "Helvetica Neue", Arial, sans-serif;
  --mono:"Martian Mono", ui-monospace, "SFMono-Regular", monospace;
  --fs-2xs:11px;  /* mono meta floor — nothing renders smaller */
  --fs-xs:12px;
  --fs-sm:13.5px;
  --fs-md:15px;
  --fs-lg:18px;

  /* motion — purposeful, quick, distance-eased */
  --t-fast:150ms;
  --t-med:220ms;
  --ease:cubic-bezier(0.2, 0, 0, 1);

  /* shape — one small radius for controls/chips, one medium for panels/docks */
  --radius-sm:4px;
  --radius-md:8px;

  font-family: var(--sans);
  font-size: 16px;
  background: var(--bg-0);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body { background: var(--bg-0); overflow: hidden; }
a { color: inherit; text-decoration: none; }
::selection { background: var(--accent); color: var(--bg-0); }

/* visible focus everywhere (WCAG AA focus order) */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

/* accessible-hidden helper (h1 landmarks on media routes) */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ---- atmosphere: one static grain texture + a functional legibility vignette.
   No scanlines, no flicker, no animation loops (FS-19 slot 5). */
body::before {
  content:""; position: fixed; inset: 0; z-index: 31; pointer-events: none;
  opacity: 0.035; mix-blend-mode: screen;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.swipe-player::after, .permalink::after, .channel-page::after {
  content:""; position: fixed; inset: 0; z-index: 30; pointer-events: none;
  background: radial-gradient(140% 120% at 50% 45%, transparent 62%, rgba(0,0,0,0.5) 100%);
}

/* ============================ MASTHEAD ============================ */
.topbar {
  position: fixed; inset: 0 0 auto; z-index: 40;
  height: 56px; display: flex; align-items: center; justify-content: space-between;
  padding: 0 clamp(16px, 4vw, 34px);
  background: linear-gradient(180deg, rgba(8,9,11,0.72), transparent);
}
.brand {
  display: inline-flex; align-items: baseline; gap: 10px;
  color: var(--ink);
}
/* FS-27: lowercase wordmark so the i carries a real dot position; the dot itself is the
   camera record light — a dotless ı with an absolutely positioned blinking dot. The dot is
   out of flow (position:absolute) so the wordmark's metrics never move: zero layout shift. */
.brand .wordmark {
  font-family: var(--sans); font-weight: 680; font-size: 17px;
  letter-spacing: 0.005em; text-transform: none; line-height: 1;
}
.wordmark-i { position: relative; display: inline-block; }
.rec-dot {
  position: absolute; left: 50%; top: 0.10em;
  width: 0.17em; height: 0.17em; border-radius: 50%;
  background: var(--rec);
  box-shadow: 0 0 7px 1px rgba(255, 55, 66, 0.65); /* static glow — painted once, never animated */
  transform: translateX(-50%);
  animation: rec-blink 1.1s cubic-bezier(0.4, 0, 0.6, 1) infinite; /* opacity-only: compositor */
}
@keyframes rec-blink { 0%, 44% { opacity: 1; } 58%, 82% { opacity: 0.16; } 100% { opacity: 1; } }
.brand-sub {
  font-family: var(--mono); font-size: var(--fs-2xs); font-weight: 400;
  letter-spacing: 0.18em; color: var(--ink-3); text-transform: uppercase;
}
.brand-sub::before { content: "/ "; color: var(--accent); }
.brand:hover .brand-sub::before, .brand[aria-current="page"] .brand-sub::before { color: var(--rec); }
.topbar nav {
  display: flex; align-items: center; gap: clamp(12px, 2vw, 22px);
  font-family: var(--sans); font-weight: 500; font-size: var(--fs-xs);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-2);
}
/* FS-27 nav motion: an underline that slides in via transform:scaleX (compositor-only);
   the current route holds it in record-red. */
.topbar nav a { position: relative; padding: 6px 2px; transition: color var(--t-fast) var(--ease); }
.topbar nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 2px; height: 1px;
  background: var(--ink-2); transform: scaleX(0); transform-origin: left;
  transition: transform var(--t-med) var(--ease);
}
.topbar nav a:hover { color: var(--ink); }
.topbar nav a:hover::after, .topbar nav a:focus-visible::after { transform: scaleX(1); }
.topbar nav a[aria-current="page"] { color: var(--ink); }
.topbar nav a[aria-current="page"]::after { transform: scaleX(1); background: var(--rec); }

button, .account-pill {
  min-height: 32px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); padding: 0 14px;
  background: transparent; color: var(--ink-2);
  font-family: var(--sans); font-weight: 550; font-size: var(--fs-xs); letter-spacing: 0.06em; text-transform: uppercase;
}
/* one-shot hover raise (transform transition, not an animation loop) */
button { cursor: pointer; transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); }
button:hover { color: var(--ink); border-color: var(--ink-3); background: rgba(233,235,240,0.04); transform: translateY(-1px); }
.account-pill { color: var(--ink-2); display: inline-flex; align-items: center; }

/* nav-resident buttons (the Watch/Listen mode toggle, Claim account): chip-styled like every
   button, but sized/toned into the nav family; the engaged Listen mode holds the accent. */
.topbar nav .nav-button { min-height: 26px; padding: 0 10px; letter-spacing: 0.08em; }
.topbar nav .nav-button[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }

/* honest follow states (FE-003 contract) */
button[data-following="true"] { color: var(--pass); border-color: var(--pass); }
button[data-follow-requires-account="true"] {
  color: var(--accent); border-color: var(--accent); text-transform: none; letter-spacing: 0.02em;
}

/* ============================ STAGE — full-screen media ============================ */
/* DYNAMIC viewport height, not 100vh (2026-08-16).
   `100vh` on mobile is the LARGEST viewport — the one with the browser chrome retracted. Android
   Chrome puts its address bar at the BOTTOM, so the shell was taller than what the viewer could see
   and everything anchored to its bottom slid underneath: the founder's phone showed "Still lake at
   altitude" cut in half by the now-playing bar. `.permalink` and `.lineup-index` already used this
   fallback pair; the tune surface — the one a shared link actually opens — was the one that missed
   it. The `100vh` line stays first as the fallback for engines without dvh. */
/* ── THE FEED SHELL: what BOTH viewports share, and nothing either has to undo ──────────────────
   It used to BE the stage — `height:100dvh; overflow:hidden` — so the scrolling viewport existed
   only as a list of cancellations, and inheriting the stage by accident rendered ten items at top:0
   in a one-viewport document: a feed that is correct in the DOM and unusable on screen.
   The two viewports are now PEERS. Neither is the default; each says what it is. */
.feed-shell { position: relative; background: var(--bg-0); }
/* STAGE — one item at a time, the player's viewport. The 100vh line stays first as the fallback for
   engines without dvh. */
.feed-shell.swipe-player { height: 100vh; height: 100dvh; overflow: hidden; }
/* COLUMN — a scrolling feed, and it scrolls ITSELF.
   ⚠ "GROWS WITH ITS CONTENT; THE DOCUMENT SCROLLS" WAS WRONG, AND FROZE THE WHOLE SURFACE
   (found by an adversarial pass 2026-09-07, before it deployed). `body { overflow: hidden }` is set
   on line 73 here AND again in wall.css, and per CSS Overflow the body's value PROPAGATES TO THE
   VIEWPORT — so there is no document scroll for a content-sized column to rely on. Measured in real
   Chrome on the shipped markup: six wheel events, End and PageDown all left `scrollY` at 0 on a
   4065px document, while `window.scrollTo()` still worked (programmatic scrolling is permitted on a
   hidden viewport — which is exactly why the tracer went green on a page no human could move).
   With no scroll, the 600px sentinel never intersected and `loadMore()` was unreachable: an
   infinite feed that showed one item, forever.
   THIS FILE ALREADY CARRIED THE ANSWER TWICE — `.lineup-index` (QA 2026-08-13 H2) and wall.css's
   `.gov` (caught in a browser 2026-08-16) are the same defect with the same remedy: a BOUNDED height
   so the element is its own scroll container, plus its own `overflow-y`. `scroll-snap-type` needs
   that too — it applies only to a scroll container, so the column's snapping was inert as well.
   The 100vh line stays first as the fallback for engines without dvh. */
.feed-shell.scroll-feed {
  height: 100vh; height: 100dvh;
  overflow-y: auto;
  /* The column owns its gesture: a scroll that reaches either end must not chain to the body. */
  overscroll-behavior-y: contain;
  scroll-snap-type: y mandatory;
}
/* Same as .lineup-index (QA H2): a shared permalink could not scroll past the fold. */
.permalink { height: 100vh; height: 100dvh; overflow-y: auto; background: var(--bg-0); }
/* FS-27: the stage owns its touch surface — native scroll is suppressed in CSS so the
   drag gesture's pointer listeners can stay fully passive (no preventDefault, no jank). */
.swipe-player { position: relative; isolation: isolate; touch-action: none; }

/* the active video fills the screen; poster + video share one fixed layer → zero layout shift */
.video-pool { position: fixed; inset: 0; z-index: 1; pointer-events: none; background: var(--bg-0); }
.video-pool video { display: none; width: 100%; height: 100%; object-fit: cover; background: #000; }
.video-pool video[data-pool-state="active"] { display: block; }

/* one item on the stage; the rest never paint (single-stage guarantee).
   ⚠ SCOPED TO THE STAGE. Unscoped, this was the layout every `.feed-item` got whether or not its
   viewport wanted it — and `data-stage-state` is the STAGE's mechanism, so only the stage consults
   it. A column renders every item; it does not ask which one is on stage. */
.swipe-player .feed-item {
  position: absolute; inset: 0; z-index: 2;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity var(--t-med) var(--ease), visibility 0s linear var(--t-med);
}
.swipe-player .feed-item[data-stage-state="active"] { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: 0s; will-change: transform, opacity; }
/* COLUMN items: normal flow, one viewport each, snapping.
   ⚠ `position: relative` IS LOAD-BEARING, and omitting it stacked the whole feed into one box.
   "Nothing to undo" was true of `.swipe-player .feed-item`, which is scoped — but `.media-frame`
   (:232 `position:absolute; inset:0`) and the item's overlay chrome are NOT scoped, and an
   absolutely-positioned child resolves against its nearest POSITIONED ancestor. With the item
   `static`, that ancestor was `.feed-shell` (`position: relative`, :184), so every item's poster,
   title and scrim escaped its own box and piled onto the shell: one hugely-stretched poster across
   the full 4500px column with all the titles collected at the bottom. The item must be the
   containing block its children already assume. */
.scroll-feed .feed-item { position: relative; min-height: 100dvh; scroll-snap-align: start; z-index: 2; }

/* FS-27 directional stage-enter: the incoming item slides from its travel direction
   (translate3d + opacity only). data-stage-direction is set by go() before the flip. */
.swipe-player:not([data-motion-engine="js"])[data-stage-direction="next"] .feed-item[data-stage-state="active"] { animation: stage-next 360ms var(--ease); }
.swipe-player:not([data-motion-engine="js"])[data-stage-direction="prev"] .feed-item[data-stage-state="active"] { animation: stage-prev 360ms var(--ease); }
@keyframes stage-next { from { opacity: 0; transform: translate3d(0, 26px, 0); } to { opacity: 1; transform: none; } }
@keyframes stage-prev { from { opacity: 0; transform: translate3d(0, -26px, 0); } to { opacity: 1; transform: none; } }
/* one-shot stage settle for activations that carry no travel direction (SSR first paint,
   single-item permalink stages): a quiet fade + 4px rise. The directional stage-enter rules
   above are more specific and take over once go() stamps data-stage-direction. The JS engine
   stamps the player only at its FIRST directional transition, so this first-paint settle is
   never cancelled mid-flight by hydration. */
.swipe-player:not([data-motion-engine="js"]) .feed-item[data-stage-state="active"] { animation: stage-settle var(--t-med) var(--ease); }
@keyframes stage-settle { from { opacity: 0; transform: translate3d(0, 4px, 0); } to { opacity: 1; transform: none; } }

/* the poster occupies the same fixed box as the video; fades only once frames paint.
   Child combinator on purpose: the poster is the frame's only DIRECT img — the Listen
   staging's .listen-art img (below) must not inherit the full-bleed poster treatment. */
.media-frame { position: absolute; inset: 0; z-index: 0; overflow: hidden; background: #000; }
.media-frame > img { width: 100%; height: 100%; object-fit: cover; opacity: 0.92; transition: opacity var(--t-med) var(--ease); }
.feed-item[data-stage-state="active"][data-video-playing="true"] .media-frame > img { opacity: 0; }
/* FS-33 functional motion #2 (the second of the two sanctioned looping functional motions, with
   the wall's column drift): the Ken-Burns poster pan. It lives on the ACTIVE feature's poster
   while that poster is the visible visual — i.e. until data-video-playing fades it once frames
   paint (:210). Transform-only (compositor); the global prefers-reduced-motion kill (:603-604)
   stills it. (Shipped here, on the real player markup, after the prototype's `.screen .poster`
   rule sat dead in wall.css — the shipped player has no .screen/.poster elements.) */
@keyframes kenburns { from { transform: scale(1); } to { transform: scale(1.15); } }
.feed-item[data-stage-state="active"]:not([data-video-playing="true"]) .media-frame > img { animation: kenburns 24s linear infinite alternate; }
/* FS-29 media-brightness fix: the pooled video lives in the FIXED .video-pool (z1), BEHIND the
   active feed-item (z2). The poster fades on play, but the media-frame's own opaque black fill
   still occluded the decoded video — so the stage rendered ≈black (composited luminance ~4/255)
   despite the clip decoding brightly. Drop the fill once frames paint so the pooled video shows
   through (measured: ~4 → ~96/255). Static state toggle keyed on data-video-playing — a paint,
   not a layout change; no animation of a forbidden property; CLS-neutral. Reverts to the #000
   backdrop between clips (CUEING) so the poster/letterbox still has its fill before frames paint. */
.feed-item[data-stage-state="active"][data-video-playing="true"] .media-frame { background: transparent; }
.media-frame::before {
  content: "LIVE DECODE";
  position: absolute; top: 78px; left: clamp(16px,4vw,34px); z-index: 3;
  font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.22em; color: var(--accent);
  opacity: 0.85;
}
/* FS-27 brand-motion signature + FS-28 media-legibility affordance: the active stage always
   carries an honest state chip top-right — "● CUEING" until the clip actually paints frames,
   "● REC" while it does (keyed on data-video-playing, so it never claims REC on a frozen
   poster). Dark content therefore always reads as a live, designed stage — never as broken
   media. Opacity-only blink (compositor); reduced-motion → static text via the global kill. */
.feed-item[data-stage-state="active"] .media-frame::after {
  position: absolute; top: 78px; right: clamp(16px,4vw,34px); z-index: 3;
  font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.22em;
}
.feed-item[data-stage-state="active"]:not([data-video-playing="true"]) .media-frame::after {
  content: "● CUEING";
  color: var(--ink-3);
  animation: rec-blink 2.2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.feed-item[data-stage-state="active"][data-video-playing="true"] .media-frame::after {
  content: "● REC";
  color: var(--rec);
  animation: rec-blink 1.1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* BUGHUNT WAVE 2 (2026-09-25) — the media-error reader. onMediaFailure marks the pool video
   data-media-error="1" and the handler mirrors that mark onto the owning feed-item as
   data-media-failed="1"; until now nothing consumed either, so a dead clip on a non-tuned surface
   sat on poster/black frame blinking "● CUEING" forever. This rule surfaces the truth instead.
   MUST stay AFTER the CUEING/REC rules: the CUEING selector also matches a failed item (failure ⇒
   not playing) and specificities are equal, so source order is what lets the failure state win. */
.feed-item[data-stage-state="active"][data-media-failed="1"] .media-frame::after {
  content: "● NO SIGNAL";
  color: var(--rec);
  animation: none;
}
.caption-strip { display: none; }

/* ============================ LISTEN STAGING ============================ */
/* Audio-first items (data-audio-first="true", tag-derived) always stage as audio objects;
   body[data-mode="listen"] — the TopNav Watch/Listen toggle, persisted client-side —
   extends the same staging to EVERY item. The pooled <video> stays exactly where it is
   as the audio source: dimmed/desaturated to a backdrop (static filter — never animated),
   never removed. In Watch mode a video item renders byte-identically to before: every
   rule here is gated on one of those two selectors. The only motion is the one-shot
   rise on activation, killed with everything else under prefers-reduced-motion. */
.listen-art, .listen-meta { display: none; }
.feed-item[data-audio-first="true"] .media-frame,
body[data-mode="listen"] .feed-item .media-frame {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 28px;
}
/* the full-bleed poster steps back to a faint backdrop; while frames paint, the video shows
   through the frame's tinted glass below at the same muted level */
.feed-item[data-audio-first="true"] .media-frame > img,
body[data-mode="listen"] .feed-item .media-frame > img {
  position: absolute; inset: 0; opacity: 0.18; filter: saturate(0.35) brightness(0.6);
}
.feed-item[data-audio-first="true"][data-stage-state="active"][data-video-playing="true"] .media-frame,
body[data-mode="listen"] .feed-item[data-stage-state="active"][data-video-playing="true"] .media-frame {
  background: rgba(8, 9, 11, 0.55);
  backdrop-filter: saturate(0.35) brightness(0.75);
}
.feed-item[data-audio-first="true"] .listen-art,
body[data-mode="listen"] .feed-item .listen-art { display: block; position: relative; z-index: 2; }
.listen-art img {
  display: block; width: min(52vh, 68vw, 440px); height: min(52vh, 68vw, 440px); object-fit: cover;
  background: var(--bg-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius-md);
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.55);
}
/* one-shot art cue on activation — same rise as the readout, restarted by the attribute flip */
.swipe-player:not([data-motion-engine="js"]) .feed-item[data-stage-state="active"] .listen-art { animation: rise var(--t-med) var(--ease) backwards; }
.feed-item[data-audio-first="true"] .listen-meta,
body[data-mode="listen"] .feed-item .listen-meta {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  position: relative; z-index: 2; max-width: min(80vw, 480px); text-align: center;
}
.listen-meta strong {
  font-family: var(--sans); font-weight: 680; font-size: clamp(20px, 2.4vw, 30px);
  line-height: 1.12; letter-spacing: -0.015em; color: var(--ink);
  text-shadow: 0 1px 24px rgba(0,0,0,0.65); text-wrap: balance;
}
.listen-meta strong a { transition: color var(--t-fast) var(--ease); }
.listen-meta strong a:hover { color: var(--accent); }
.listen-meta span { font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-3); }
.listen-meta span::before { content: "CH / "; color: var(--accent); }
/* the centered meta carries the title in Listen staging; the bottom-left readout keeps only
   its provenance affordances (channel link + AI-generated label) so nothing reads twice */
.feed-item[data-audio-first="true"] .post-copy h2,
.feed-item[data-audio-first="true"] .post-copy p,
body[data-mode="listen"] .feed-item .post-copy h2,
body[data-mode="listen"] .feed-item .post-copy p { display: none; }

/* readout scrim + FS-28 "monitor glass": one static full-bleed paint layering the original
   bottom legibility scrim with a faint top glow and an edge vignette, so a pure-black frame
   reads as a designed broadcast stage with depth — never as dead pixels. Painted once,
   NEVER animated; pointer-events none. */
.feed-item::before {
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background:
    linear-gradient(0deg, rgba(8,9,11,0.9), transparent 44%),
    radial-gradient(120% 70% at 50% 0%, rgba(143,208,198,0.05), transparent 55%),
    radial-gradient(150% 130% at 50% 45%, transparent 66%, rgba(0,0,0,0.4) 100%);
}
/* FS-28: the readout's bottom offset clears the single-line hint zone AND the now-playing
   dock below it (dock ≤64px + hint bottom 78px + ~22px line + breathing room) at every
   viewport height — the hint can never sit on the caption, even while visible (or
   permanently visible under reduced motion), and the dock covers neither. */
.post-copy {
  position: absolute; left: clamp(16px,4vw,40px); bottom: clamp(108px, 13vh, 118px); right: auto;
  z-index: 4; max-width: min(600px, 72vw); padding: 0;
}
/* FS-27 title microanimation: keyed on data-stage-state so the readout re-cues for EVERY
   newly active item (attribute flip restarts the animation), not just first paint. All
   transform/opacity; the elements hold their layout box throughout → zero layout shift.
   Hidden stages never match the selector, so off-screen items run no motion at all. */
.swipe-player:not([data-motion-engine="js"]) .feed-item[data-stage-state="active"] .post-copy > * { animation: rise var(--t-med) var(--ease) backwards; }
.swipe-player:not([data-motion-engine="js"]) .feed-item[data-stage-state="active"] .post-copy h2 { animation: title-cue 520ms var(--ease) 50ms backwards; }
.swipe-player:not([data-motion-engine="js"]) .feed-item[data-stage-state="active"] .post-copy p { animation-delay: 110ms; }
@keyframes rise { from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none} }
@keyframes title-cue { from { opacity: 0; transform: translate3d(0, 0.45em, 0); } to { opacity: 1; transform: none; } }
/* the record signature echoed under the active title: a red cue tick sweeping in via
   scaleX. Absolutely positioned below the baseline — occupies no flow space (CLS 0). */
.post-copy h2 { position: relative; }
.post-copy h2::after {
  content: ""; position: absolute; left: 2px; bottom: -9px; width: 34px; height: 2px;
  background: var(--rec); transform: scaleX(0); transform-origin: left; opacity: 0;
}
.swipe-player:not([data-motion-engine="js"]) .feed-item[data-stage-state="active"] .post-copy h2::after { animation: cue-tick 640ms var(--ease) 200ms forwards; }
@keyframes cue-tick { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 0.9; } }
/* JS-engine stand-in for the cue-tick ::after (WAAPI cannot reach pseudo-elements):
   the engine appends this real span with the same geometry and drives its one-shot
   sweep; base-invisible, so it never paints on the CSS fallback path. */
.post-copy h2 .cue-tick-js {
  position: absolute; left: 2px; bottom: -9px; width: 34px; height: 2px;
  background: var(--rec); transform: scaleX(0); transform-origin: left; opacity: 0;
}
.channel-link {
  font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-2);
}
.channel-link::before { content: "CH / "; color: var(--accent); }
.channel-link:hover { color: var(--ink); }
.provenance-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 8px; font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3); }
.ai-generated-label { color: var(--ink); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); padding: 2px 7px; background: rgba(8,9,11,0.55); }

/*
  THE PLAYER SHOWS THE TITLE AND LITTLE ELSE (founder-directed 2026-08-16, both viewports).

  On a phone the block above the title read "CH / @FIELD" then an "AI-GENERATED" chip then the name —
  three lines of chrome for one piece of information anyone actually wants. The channel is already
  named in the header ("● FIELD", linked to the station), so the kicker repeated a fact from 40px
  away, and navigation survives its removal.

  HIDDEN, NOT DELETED, and deliberately so: the markup stays in both the SSR component and the
  client template, which are held in lockstep — dropping it from one is how those two drift. It also
  keeps this a one-line reversal.

  ⚠ THE AI-GENERATED CHIP IS A DISCLOSURE, not decoration. `provenance/labeling integrity` is a
  PROTECTED DIMENSION of the ratified constitution, and AI-content transparency is a live regulatory
  expectation (EU AI Act Art. 50 is the obvious one) for a PUBLIC service. This is a founder decision
  taken while the site is a CLOSED preview behind basic-auth, where no member of the public can be
  misled. It must be reconsidered before anything here is opened to the public — see known-gaps.
*/
.post-copy .channel-link,
.post-copy .provenance-row { display: none; }
/* Listen mode carries the same kicker under its artwork. */
.listen-meta span { display: none; }
.post-copy h2 {
  margin: 12px 0 8px; font-family: var(--sans); font-weight: 680;
  font-size: clamp(24px, 3vw, 40px); line-height: 1.06; letter-spacing: -0.015em;
  color: var(--ink); text-shadow: 0 1px 24px rgba(0,0,0,0.65); text-wrap: balance;
}
.post-copy h2 a { transition: color var(--t-fast) var(--ease); }
.post-copy h2 a:hover { color: var(--accent); }
.post-copy p { max-width: min(480px, 58ch); margin: 0; font-family: var(--sans); font-weight: 420; font-size: var(--fs-md); line-height: 1.6; color: var(--ink-2); }
.tag-row { display: none; }
.post-copy small { display: none; }

/* actions are GESTURES — buttons stay in the DOM (a11y/JS/tests) but are visually hidden */
.feed-actions, .stage-nav {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* A MUTED player must offer a visible way back to sound (QA 2026-08-13 H3). Autoplay policy forces
   the sound-on player to fall back to muted, and the only unmute affordances were the M key and this
   control — which .feed-actions clips to 1x1px and reveals only on :focus-visible, a state touch
   never enters. On a phone there was no way to restore audio at all. The reveal is keyed on the
   reflected `data-audio-muted` state so it appears exactly when it is useful and disappears once
   sound is on. */
[data-audio-muted="true"] .feed-actions button[data-unmute-toggle],
.feed-actions button:focus-visible,
.stage-nav button:focus-visible {
  position: fixed; left: 50%; bottom: 5.4rem; transform: translateX(-50%);
  width: auto; height: auto; margin: 0; padding: 0.55rem 1.1rem;
  clip: auto; clip-path: none; overflow: visible;
  z-index: 60;
  background: var(--bg-1); color: var(--ink);
  border: 1px solid var(--accent); border-radius: var(--radius-md);
  font-family: var(--sans); font-weight: 550; font-size: var(--fs-xs); letter-spacing: 0.08em; text-transform: uppercase;
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.feed-loading-sentinel { display: none; }
/* FE: honest empty/exhausted-feed panel — fills the stage, never a black void. Mirrors the
   .empty-state idiom with a DEAD AIR eyebrow (the feed's own broadcast language). */
.feed-empty { position: absolute; inset: 0; z-index: 2; display: grid; place-content: center; gap: 16px; padding: 24px; text-align: center; }
.feed-empty h2 { margin: 0; font-family: var(--sans); font-weight: 680; font-size: clamp(28px,4.5vw,56px); line-height: 1.04; letter-spacing: -0.02em; color: var(--ink); text-wrap: balance; }
.feed-empty h2::before { content: "DEAD AIR"; display: block; font-family: var(--mono); font-weight: 400; font-size: var(--fs-2xs); letter-spacing: 0.3em; color: var(--fault); margin-bottom: 16px; }
.feed-empty p { margin: 0 auto; max-width: 44ch; font-family: var(--sans); font-weight: 420; font-size: var(--fs-md); color: var(--ink-2); }
.feed-empty a, .feed-empty button { justify-self: center; background: none; cursor: pointer; font-family: var(--sans); font-weight: 550; font-size: var(--fs-xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); padding: 8px 14px; transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); }
.feed-empty a:hover, .feed-empty button:hover { color: var(--ink); border-color: var(--accent); }
.feed-empty button:disabled { opacity: 0.6; cursor: default; }

/* just-in-time gesture hint + action confirmation (one-shot, non-looping).
   FS-28: the hint's bottom offset clears the ≤64px now-playing dock pinned below it. */
.gesture-hint {
  position: fixed; left: 50%; bottom: 78px; transform: translateX(-50%); z-index: 36;
  font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-3); pointer-events: none; text-align: center; line-height: 2;
  white-space: nowrap; /* FS-28: one line by construction — the readout clearance assumes it */
  animation: hint-fade 6s var(--ease) forwards;
}
.gesture-hint b { color: var(--ink-2); font-weight: 500; }
@keyframes hint-fade { 0%{opacity:0} 10%,70%{opacity:1} 100%{opacity:0} }
/* FS-28: two hint variants — coarse pointers and narrow widths get the short one-liner
   (keyboard keys are meaningless there, and the long legend wrapped over the readout);
   fine-pointer desktop keeps the full key legend. */
.gesture-hint .hint-touch { display: none; }
.gesture-hint .hint-keys { display: inline; }
@media (pointer: coarse), (max-width: 760px) {
  .gesture-hint .hint-touch { display: inline; }
  .gesture-hint .hint-keys { display: none; }
}
/* FS-28: the confirmation flash is a legible chip against ANY media — dark backdrop, hairline
   border, text shadow (all static paint; only opacity/transform ever animate). */
.action-flash {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%,-50%); z-index: 37;
  font-family: var(--sans); font-weight: 600; font-size: clamp(16px, 2vw, 19px);
  letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--ink); pointer-events: none; opacity: 0;
  padding: 12px 22px 12px calc(22px + 0.24em);
  background: rgba(8, 9, 11, 0.78);
  border: 1px solid var(--line-strong); border-radius: var(--radius-md);
  box-shadow: 0 6px 32px rgba(0, 0, 0, 0.5);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.85);
}
body:not([data-motion-engine="js"]) .action-flash[data-show="1"] { animation: flash 950ms var(--ease) forwards; }
.action-flash[data-kind="mark"] { color: var(--rec); border-color: color-mix(in srgb, var(--rec) 55%, transparent); }
@keyframes flash { 0%{opacity:0; transform:translate(-50%,-50%) scale(0.96)} 22%{opacity:1; transform:translate(-50%,-50%) scale(1)} 100%{opacity:0; transform:translate(-50%,-50%) scale(1.02)} }

/* FS-27 mark burst (FS-28: enlarged + brighter so it carries against black media): a core
   record dot plus two expanding rings, all transform/opacity (compositor-only),
   position:fixed with zero footprint (no layout). Base opacity is 0, so under
   prefers-reduced-motion (animations killed globally) the burst simply never appears; the
   flash chip still confirms the action. The spawning element is removed by JS after ~1100ms. */
.mark-burst { position: fixed; z-index: 38; width: 0; height: 0; pointer-events: none; }
.mark-burst span { position: absolute; left: 0; top: 0; border-radius: 50%; opacity: 0; }
.mark-burst-core {
  width: 18px; height: 18px; margin: -9px 0 0 -9px;
  background: var(--rec); box-shadow: 0 0 26px 4px rgba(255, 55, 66, 0.85);
}
.mark-burst-ring {
  width: 96px; height: 96px; margin: -48px 0 0 -48px;
  border: 2px solid var(--rec);
}
/* CSS-fallback voices of the burst (the Motion engine drives the same spans as
   one-shot springs when body[data-motion-engine="js"] is stamped) */
body:not([data-motion-engine="js"]) .mark-burst-core { animation: burst-core 680ms var(--ease) forwards; }
body:not([data-motion-engine="js"]) .mark-burst-ring { animation: burst-ring 740ms var(--ease) forwards; }
body:not([data-motion-engine="js"]) .mark-burst-ring-late { animation-delay: 140ms; animation-duration: 860ms; }
@keyframes burst-core {
  0% { opacity: 0; transform: scale(0.2); }
  25% { opacity: 1; transform: scale(1.3); }
  60% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.9); }
}
@keyframes burst-ring {
  0% { opacity: 0.9; transform: scale(0.25); }
  100% { opacity: 0; transform: scale(1.55); }
}

/* the optimistic marked state (visible in the keyboard focus-reveal chip) */
button[data-marked="true"] { color: var(--rec); border-color: var(--rec); }

/* ============================ LINEUP (curated programme watch surface) ============================ */
/* A curated lineup renders as the full swipe player (the whole programme is one page); the marquee
   names the programme so the viewer knows they are watching a curated block, not the For-You feed.
   Static paint only — no animation (FS-19 slot 5); one line, ellipsised, clears the masthead. */
.lineup-marquee {
  position: fixed; top: 64px; left: 50%; transform: translateX(-50%); z-index: 41;
  display: flex; align-items: baseline; gap: 12px; max-width: min(86vw, 720px);
  padding: 6px 13px; background: rgba(13, 15, 18, 0.72); border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm); backdrop-filter: blur(10px);
  font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-2); white-space: nowrap; overflow: hidden; pointer-events: none;
}
.lineup-marquee::before { content: "\25B8 PROGRAMME"; color: var(--accent); flex: none; }
.lineup-marquee .lineup-name { color: var(--ink); font-weight: 500; overflow: hidden; text-overflow: ellipsis; }
.lineup-marquee .lineup-count { color: var(--ink-3); flex: none; }
.lineup-marquee .lineup-note { color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; }
@media (pointer: coarse), (max-width: 760px) {
  .lineup-marquee { top: 54px; gap: 8px; max-width: 92vw; }
  .lineup-marquee .lineup-note { display: none; }
}

/* ============================ LINEUP INDEX (programme browse surface) ============================ */
/* Pure catalog paint: panel cards on the raised surface, mono metadata, one-shot hover transitions
   only (FS-27/28 — rec-blink stays the sole loop). The schedule-board sibling of the LEDGER idiom. */
/* BOUNDED height, not min-height (QA 2026-08-13 H2): `body{overflow:hidden}` propagates to the
   viewport, so the page never scrolls; a route root can only scroll ITSELF if its height is bounded.
   With min-height the box is content-sized, scrollHeight === clientHeight, and this `overflow-y`
   never engages — everything below the fold was unreachable by mouse or touch. `dvh` after `vh` so
   mobile browsers use the dynamic viewport once the URL bar collapses. */
.lineup-index { height: 100vh; height: 100dvh; padding: 104px clamp(16px,4vw,40px) 56px; overflow-y: auto; background: var(--bg-0); }
.lineup-index-head { max-width: 720px; margin: 0 0 32px; }
.lineup-index-head p { margin: 0 0 8px; font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.22em; text-transform: uppercase; color: var(--accent); }
.lineup-index-head p::before { content: "\25B8 PROGRAMME / "; color: var(--ink-3); }
.lineup-index-head h1 { margin: 0 0 10px; font-family: var(--sans); font-weight: 680; font-size: clamp(28px,3.4vw,48px); line-height: 1.04; letter-spacing: -0.02em; color: var(--ink); }
.lineup-index-head span { display: block; font-family: var(--sans); font-weight: 420; font-size: var(--fs-md); color: var(--ink-2); max-width: 520px; line-height: 1.55; }
.lineup-index-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; max-width: 720px; }
.lineup-card {
  display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 16px; align-items: center;
  padding: 12px; background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-md);
  transition: border-color var(--t-fast) var(--ease);
}
.lineup-card:hover { border-color: var(--accent); }
.lineup-card img, .lineup-card-blank { width: 112px; height: 63px; object-fit: cover; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius-sm); display: block; }
.lineup-card-body { display: grid; gap: 4px; min-width: 0; }
.lineup-card-title { font-family: var(--sans); font-weight: 550; font-size: var(--fs-md); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lineup-card-note { font-family: var(--sans); font-weight: 420; font-size: var(--fs-xs); color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lineup-card-meta { font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3); }
.lineup-index-empty { max-width: 720px; padding: 40px 0; display: grid; gap: 16px; justify-items: start; }
.lineup-index-empty p { margin: 0; font-family: var(--sans); font-weight: 420; font-size: var(--fs-md); color: var(--ink-2); }
.lineup-index-empty p::before { content: "OFF AIR"; display: block; font-family: var(--mono); font-weight: 400; font-size: var(--fs-2xs); letter-spacing: 0.3em; color: var(--ink-3); margin-bottom: 12px; }
.lineup-index-empty a, .lineup-index-more a { font-family: var(--sans); font-weight: 550; font-size: var(--fs-xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); padding: 8px 14px; transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); }
.lineup-index-empty a:hover, .lineup-index-more a:hover { color: var(--ink); border-color: var(--accent); }
.lineup-index-more { margin: 24px 0 0; max-width: 720px; display: flex; }

/* ============================ CHANNEL ============================ */
.channel-page { height: 100vh; height: 100dvh; overflow-y: auto; background: var(--bg-0); }
.channel-page .video-pool { position: absolute; }
.channel-hero {
  position: relative; z-index: 5; display: grid; grid-template-columns: 96px minmax(0,640px); gap: 28px; align-items: center;
  padding: 104px clamp(16px,4vw,40px) 40px;
}
.channel-hero img { width: 96px; height: 96px; object-fit: cover; border: 1px solid var(--line-strong); border-radius: var(--radius-md); }
.channel-hero p { margin: 0 0 8px; font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent); }
.channel-hero p::before { content: "STATION / "; color: var(--ink-3); }
.channel-hero h1 { margin: 0 0 10px; font-family: var(--sans); font-weight: 680; font-size: clamp(28px,3.4vw,48px); line-height: 1.04; letter-spacing: -0.02em; color: var(--ink); text-wrap: balance; }
.channel-hero span { display: block; font-family: var(--sans); font-weight: 420; font-size: var(--fs-md); color: var(--ink-2); max-width: 520px; line-height: 1.55; }
.channel-retention-row { display: flex; align-items: center; gap: 14px; margin-top: 20px; }
.channel-retention-row button { color: var(--ink); border-color: var(--ink-3); }
.channel-retention-row small { font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3); }
.channel-page .swipe-player { position: relative; height: 100vh; height: 100dvh; min-height: 100dvh; }

/* ============================ GUARD / 404 (semantic idiom of record) ============================ */
.empty-state { min-height: 100vh; display: grid; place-content: center; gap: 18px; padding: 24px; text-align: center; }
.empty-state h1 { margin: 0; font-family: var(--sans); font-weight: 680; font-size: clamp(32px,5vw,64px); line-height: 1.02; letter-spacing: -0.02em; color: var(--ink); text-wrap: balance; }
.empty-state h1::before { content: "NO SIGNAL"; display: block; font-family: var(--mono); font-weight: 400; font-size: var(--fs-2xs); letter-spacing: 0.3em; color: var(--fault); margin-bottom: 18px; }
.empty-state p { margin: 0; font-family: var(--sans); font-weight: 420; font-size: var(--fs-md); color: var(--ink-2); }
.empty-state a { justify-self: center; font-family: var(--sans); font-weight: 550; font-size: var(--fs-xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); padding: 8px 14px; transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); }
.empty-state a:hover { color: var(--ink); border-color: var(--accent); }

/* ============================ LEDGER (admin, flag-on only) ============================ */
.admin-queue, .demand-scorecard { min-height: 100vh; padding: 104px clamp(16px,4vw,40px) 56px; overflow-y: auto; }
.admin-queue h1, .demand-scorecard h1 { margin: 0 0 28px; font-family: var(--sans); font-weight: 680; font-size: clamp(24px,3vw,40px); letter-spacing: -0.015em; color: var(--ink); }
.admin-queue h1::before, .demand-scorecard h1::before { content: "OPERATOR / LEDGER"; display: block; font-family: var(--mono); font-weight: 400; font-size: var(--fs-2xs); letter-spacing: 0.22em; color: var(--accent); margin-bottom: 12px; }
.admin-queue table, .demand-scorecard table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: var(--fs-xs); background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-md); overflow: hidden; }
.admin-queue th, .admin-queue td, .demand-scorecard th, .demand-scorecard td { padding: 12px 14px; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; }
.admin-queue th, .demand-scorecard th { color: var(--ink-3); font-family: var(--sans); font-weight: 550; font-size: var(--fs-2xs); letter-spacing: 0.12em; text-transform: uppercase; background: var(--bg-2); }
.demand-scorecard tr[data-gate="pass"] { box-shadow: inset 2px 0 0 var(--pass); }
.demand-scorecard tr[data-gate="pass"] td:last-child { color: var(--pass); }
.demand-scorecard tr[data-gate="fail"] { box-shadow: inset 2px 0 0 var(--fault); }
.demand-scorecard tr[data-gate="fail"] td:last-child { color: var(--fault); }
.demand-scorecard .suspect-marker { display: inline-flex; align-items: center; margin-left: 10px; padding: 2px 7px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); color: var(--ink-3); text-transform: uppercase; font-size: var(--fs-2xs); letter-spacing: 0.1em; }
.demand-scorecard .suspect-marker[data-suspect="true"] { border-color: var(--fault); color: var(--fault); background: color-mix(in srgb, var(--fault) 10%, transparent); }
.demand-scorecard[data-house-concentration-fail="true"] { outline: 1px solid var(--fault); }

/* ============================ NOW-PLAYING DOCK ============================ */
/* Persistent transport strip pinned to the bottom edge (one per player, populated by JS on
   activation). The progress rail's fill width is written by JS on timeupdate — deliberately
   NOT a CSS animation, so the dock adds zero looping motion (FS-27/28). The gesture-hint and
   readout offsets above are raised to clear this ≤64px dock zone. */
.now-playing {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 39;
  display: flex; align-items: center; gap: 14px;
  height: 52px; padding: 0 clamp(16px, 4vw, 34px);
  background: rgba(13, 15, 18, 0.92);
  border-top: 1px solid var(--line);
  backdrop-filter: blur(14px);
}
.now-playing[hidden] { display: none; } /* display:flex above would defeat the UA [hidden] rule */
.np-rail { position: absolute; left: 0; right: 0; top: -1px; height: 2px; background: rgba(233, 235, 240, 0.08); }
.np-rail-fill { width: 0%; height: 100%; background: var(--accent); }
.np-art { width: 32px; height: 32px; flex: none; object-fit: cover; background: var(--bg-2); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); }
.np-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.np-title { font-family: var(--sans); font-weight: 550; font-size: var(--fs-sm); line-height: 1.2; letter-spacing: -0.01em; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-channel { font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-time { font-family: var(--mono); font-size: var(--fs-2xs); letter-spacing: 0.08em; font-variant-numeric: tabular-nums; color: var(--ink-3); white-space: nowrap; }
.np-transport { display: flex; gap: 8px; flex: none; }
.np-transport button { min-height: 30px; padding: 0 12px; }
/* Share affordance (the visible dock button + the sr-only per-item control): a CSS-masked share
   glyph in currentColor — no inline SVG, so the SSR button and its client twin stay trivially in
   lockstep. Static paint, no new motion (motion law). */
.share-icon { display: inline-flex; align-items: center; justify-content: center; }
.np-transport .share-icon { padding: 0 10px; }
.share-icon::before {
  content: ""; display: block; width: 16px; height: 16px; background-color: currentColor;
  -webkit-mask: var(--ico-share) center / contain no-repeat;
  mask: var(--ico-share) center / contain no-repeat;
}
:root {
  --ico-share: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='18'%20cy='5'%20r='3'/%3E%3Ccircle%20cx='6'%20cy='12'%20r='3'/%3E%3Ccircle%20cx='18'%20cy='19'%20r='3'/%3E%3Cline%20x1='8.59'%20y1='13.51'%20x2='15.42'%20y2='17.49'/%3E%3Cline%20x1='15.41'%20y1='6.51'%20x2='8.59'%20y2='10.49'/%3E%3C/svg%3E");
}
/* Listen mode gives the dock more visual weight — still static paint, no new motion */
body[data-mode="listen"] .now-playing { height: 64px; background: rgba(13, 15, 18, 0.97); }
body[data-mode="listen"] .np-art { width: 42px; height: 42px; }
body[data-mode="listen"] .np-title { font-size: var(--fs-md); }

/* ============================ RESPONSIVE ============================ */
@media (max-width: 760px) {
  .topbar { height: 48px; padding: 0 14px; }
  .topbar nav a:nth-child(2) { display: none; }
  /* FS-28: the readout clears the single-line hint zone below it — the 380px bottom-stack
     overlap (pre-existing, worsened by the longer FS-27 hint legend) is structurally gone:
     hint = one nowrap line above the ≤64px now-playing dock, readout ends 34px above the
     hint. Holds even when reduced-motion keeps the hint permanently visible. */
  .post-copy { left: 16px; right: 16px; bottom: 104px; max-width: none; }
  .post-copy h2 { font-size: clamp(20px, 6.4vw, 28px); }
  .post-copy p { font-size: 14px; line-height: 1.55; }
  .gesture-hint { bottom: 70px; line-height: 1.5; white-space: nowrap; letter-spacing: 0.12em; }
  .media-frame::before,
  .feed-item[data-stage-state="active"] .media-frame::after { top: 64px; }
  .channel-hero { grid-template-columns: 64px 1fr; padding: 84px 16px 28px; gap: 16px; }
  .channel-hero img { width: 64px; height: 64px; }
  .now-playing { gap: 10px; padding: 0 14px; }
  .np-time { display: none; } /* the rail still shows progress; the strip keeps one clean line */
  .listen-art img { width: min(46vh, 78vw); height: min(46vh, 78vw); }
  .listen-meta strong { font-size: clamp(18px, 5.6vw, 24px); }
}

/*
  THE PHONE DECLUTTER (founder-directed 2026-08-16, from a real-device screenshot).

  What that screenshot showed, all at once in the bottom third: a title clipped mid-glyph by the
  now-playing bar, and the bar itself repeating that same title truncated to a single character
  beside a 32px thumbnail of the video already filling the screen. Above it, four header items on
  one row with "BACK TO THE WALL" wrapping onto two lines into the LISTEN button, plus two state
  chips. Every one of those is defensible on a 1440px desktop; together on a 390px phone they read
  as noise.

  The rule applied throughout: on a phone, say a thing ONCE.
*/
@media (max-width: 560px) {
  /* The bar's art and title duplicate what is already on screen — the poster IS the video, and the
     title sits directly above. At this width the label had collapsed to "S" and the channel to "@F",
     which is not information, only the appearance of it. Transport controls stay: those are the
     reason the bar exists. */
  .np-art,
  .np-copy { display: none; }
  .now-playing { gap: 0; justify-content: space-around; }

  /* State chips are a desktop affordance. They sit at top:78px, directly under a header that is
     already crowded, and they narrate a state the picture shows. */
  .media-frame::before,
  .feed-item[data-stage-state="active"] .media-frame::after { display: none; }

  /* BUGHUNT WAVE 2 follow-up (2026-09-25): the FAILURE chip is exempt from the declutter above.
     CUEING/REC narrate a state the picture already shows; a dead clip is the one state it does NOT
     — poster/black frame, transport dead. Hiding the failure state too handed phones the original
     finding back verbatim. Higher specificity than the hide rule above; CUEING/REC stay hidden
     exactly as the 2026-08-16 rule directs. */
  .feed-item[data-stage-state="active"][data-media-failed="1"] .media-frame::after { display: block; }

  /* More clearance than the 760px rule gives, now that the shell is honest about its own height. */
  .post-copy { bottom: 96px; }
  .post-copy h2 { font-size: clamp(19px, 5.8vw, 24px); line-height: 1.2; }
}

/* ============================ MOTION SAFETY ============================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition-duration: 0.01ms !important; }
  .gesture-hint { opacity: 1; }
  /* FS-28: feedback without motion — the flash chip shows statically (JS clears it on a
     timer); the mark burst stays suppressed (its children's base opacity is 0). */
  .action-flash[data-show="1"] { opacity: 1; }
}

/* ===================== NAVIGATION BETWEEN DOCUMENTS =====================
   Measured 2026-08-19 from a founder screencast of antitube.tv, frame-by-frame against the
   recorder's own clock (which kept ticking at 26–30fps throughout, so the capture is faithful):
   31 whole-screen cuts in 46 seconds, and 37% of the recording was a FROZEN screen waiting for the
   next document — single waits of 1.2s, 2.0s, 2.9s, 3.6s. The wall's drift was never the problem
   (117fps unthrottled on the same machine); the problem is that every interaction here is a full
   document load, this app has zero hydration by design, and the browser holds the old frame
   unchanged until the new document paints. A viewer cannot distinguish that from a hang.

   `navigation: auto` cross-fades the two documents instead of snapping between them. It costs no
   JavaScript and no hydration, which is the only kind of fix this app can take. It does not make
   the navigation faster — nothing in CSS can — it makes the ARRIVAL continuous. The departure half
   (immediate feedback on click) is navigation-transition.ts.

   NOT a looping animation: a view transition is one-shot per navigation, so the ratified count of
   continuous motions (rec-blink trio, Ken-Burns pan, the wall's column drift) is unchanged. */
@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 220ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

/* The departure ramp driven by navigation-transition.ts. Static styles only — every value that
   moves is set by Motion, so this rule can never become a second animation source for the same
   element (the double-drive class the motion engine's :not([data-motion-engine="js"]) gates exist
   to prevent). REC red, 2px, top edge: the wall's own accent, not a new colour. */
.nav-signal {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
  width: 100%;
  height: 2px;
  background: var(--rec, #e5484d);
  transform: scaleX(0);
  transform-origin: 0 50%;
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  /* A viewer who asked for stillness gets the hard cut back — a cross-fade is still motion. The JS
     half refuses to arm under the same query, so the ramp never appears either. */
  @view-transition { navigation: none; }
  .nav-signal { display: none; }
}
