/* jungli.st — Pirate Radio Transmission
 *
 * The feed is a live broadcast log. Black + caution-yellow + alert-red,
 * crisp Archivo grotesque headlines over an IBM Plex Mono "teletype" body
 * (meta/summary), with CRT scanline + film-grain overlays for texture.
 *
 * Category rails carry meaning:
 *   news    -> caution-yellow (the signal)
 *   release -> alert-red      (hot / new dubplate)
 *   social  -> pale ink       (chatter)
 *   mix     -> broadcast cyan (radio shows / podcasts / DJ sets — the dial tone)
 */

:root {
  color-scheme: dark;

  /* palette — warm-black & paper, not the default cold grey */
  --bg:        #0c0c0c;
  --bg-panel:  #131210;
  --ink:       #f4f1e8;       /* warm off-white (newsprint) */
  --ink-dim:   #9a968c;
  --ink-faint: #868176;       /* warm faint ink — AA 4.5:1 on bg-panel (4.83) & bg (5.04) */
  --yellow:    #ffe500;       /* caution-tape */
  --red:       #ff3b30;       /* ON AIR / alert */
  --mix:       #4ec9c0;       /* broadcast cyan — the 4th rail (radio/mixes); AA on bg-panel (9.8:1) & takes #000 fill ink */
  --line:      #2a2723;       /* warm dark hairline */
  --ink-on-accent: #000;      /* text on a yellow/red/rail fill — caution-tape ink */

  --font-display: "Archivo", system-ui, -apple-system, "Helvetica Neue", sans-serif;
  --font-mono:    "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --maxw: 680px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  padding-bottom: 6rem;
  position: relative;
}

/* ── Overlays: scanlines + grain (CSS-only, no requests) ──────────────── */
body::before,
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
}
/* CRT scanlines */
body::before {
  background: repeating-linear-gradient(
    0deg, transparent 0 2px, rgba(0, 0, 0, 0.5) 2px 3px);
  opacity: 0.32;
}
/* film grain via inline SVG fractal noise */
body::after {
  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.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: 0.045;
  mix-blend-mode: screen;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--yellow);
  color: var(--ink-on-accent);
  padding: 0.5rem 0.9rem;
  font-weight: 700;
  z-index: 10000;
}
.skip:focus { left: 0; }

/* visually hidden but exposed to assistive tech (page heading, SR-only labels) */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Station: broadcast sheet ─────────────────────────────────────────────
   Masthead (wordmark + live light) → caution-tape rule → console status band
   → category filter. Sticky as one unit; each control gets its own row so the
   tune-in key never fights the readout for width (the old overflow). */
.station {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  padding: 0.85rem 1.1rem 0;
}

/* Masthead — oversized wordmark + ident, live light stacked at the right. */
.mast {
  display: flex;
  align-items: center;
  gap: 0.8rem;
}
.ident {
  width: 42px;
  height: 42px;
  object-fit: cover;
  border: 2px solid var(--yellow);
  transform: rotate(-4deg);
  box-shadow: 3px 3px 0 #000;
  flex: none;
}
.wordmark {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.7rem, 7vw, 2.3rem);
  letter-spacing: -0.045em;
  line-height: 0.8;
  color: var(--ink);
  text-decoration: none;
  min-width: 0;   /* let the right-side live block keep its space, never collide */
}
.wordmark .dot { color: var(--red); }
.wordmark .tld { color: var(--yellow); }

/* @keyframes eq is retained — the player's transmission meter (.player--playing
   .player__eq i) still uses it (the header signal meter itself was removed: an
   animation bound to ~always-ON-AIR freshness just read as decoration). */
@keyframes eq { to { transform: scaleY(1); } }

.onair {
  margin-left: auto;   /* pin the live light to the right of the masthead */
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-weight: 700;
  letter-spacing: 0.16em;
  font-size: 0.72rem;
  color: var(--red);
  white-space: nowrap;
}
.onair__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--red);
  box-shadow: 0 0 9px var(--red);
  animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
/* OFF AIR — feed stale/unreachable: greyed, static dot (no live pulse) */
.onair.onair--off { color: var(--ink-faint); }
.onair.onair--off .onair__dot {
  background: var(--ink-faint);
  box-shadow: none;
  animation: none;
}

/* Caution-tape signal band under the masthead. */
.rule {
  height: 6px;
  margin-top: 0.8rem;
  background: repeating-linear-gradient(135deg,
    var(--yellow) 0 11px, #000 11px 22px);
}

/* Console status band — one honest readout line + the tune-in key. Wraps the
   key to its own row before anything clips (kills the old header overflow). */
.band {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  flex-wrap: wrap;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--line);
}
.status {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.08em;
  color: var(--ink-dim);
  text-transform: uppercase;
}
.band .tunein { margin-left: auto; }

/* ── Category filter (in the sticky header) ──────────────────────────────
   The bottom row of the station. The active tab fills with that category's rail
   colour, so the control IS the colour legend. Scrolls horizontally if the tabs
   ever exceed the width, rather than wrapping or clipping. */
.filterbar {
  display: flex;
  gap: 0.4rem;
  padding: 0.7rem 0 0.85rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.filterbar::-webkit-scrollbar { display: none; }
.filter {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 0.64rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-dim);
  background: transparent;
  border: 1px solid var(--line);
  padding: 0.38rem 0.7rem;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
/* only inactive tabs react to hover — the pressed tab keeps its on-accent ink
   (don't let hover override #000 to near-white over a yellow/red fill) */
.filter:not([aria-pressed="true"]):hover { color: var(--ink); border-color: var(--ink-dim); }
/* active = filled in the category's rail colour (the legend) */
.filter[aria-pressed="true"] { color: var(--ink-on-accent); border-color: transparent; }
.filter[data-cat="all"][aria-pressed="true"]     { background: var(--ink); }
.filter[data-cat="news"][aria-pressed="true"]    { background: var(--yellow); }
.filter[data-cat="release"][aria-pressed="true"] { background: var(--red); }
.filter[data-cat="social"][aria-pressed="true"]  { background: var(--ink-dim); }
.filter[data-cat="mix"][aria-pressed="true"]     { background: var(--mix); }

/* client-side filtering — hide non-matching cards (works for paginated AND
   live-prepended cards; "all"/unset matches nothing here, so everything shows) */
.feed[data-filter="news"]    .log:not([data-category="news"]),
.feed[data-filter="release"] .log:not([data-category="release"]),
.feed[data-filter="social"]  .log:not([data-category="social"]),
.feed[data-filter="mix"]     .log:not([data-category="mix"]) { display: none; }

/* ── Station sign-on (scroll-away orientation) ──────────────────────────── */
.signon {
  max-width: var(--maxw);
  margin: 1.2rem auto 0;
  padding: 0 1rem;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  line-height: 1.5;
  letter-spacing: 0.03em;
  color: var(--ink-dim);
}
.signon strong { color: var(--ink); font-weight: 600; }
/* caution-yellow on-air tick, tying the ident to the broadcast palette */
.signon::before {
  content: "";
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 0.5rem;
  background: var(--yellow);
  vertical-align: 0.05em;
}

/* ── Feed ─────────────────────────────────────────────────────────────── */
.feed {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0.4rem 1.1rem 0;
}

/* ── Log entry — broadcast-sheet row ───────────────────────────────────────
   Each transmission is a log-sheet line: a left timecode gutter (clock code over
   relative "ago", split by the category rail tick) and a roomy body. Entries are
   separated by hairlines, not boxed — the sheet reads as one continuous log. */
.log {
  --rail: var(--yellow);
  position: relative;
  display: grid;
  grid-template-columns: 56px 1fr;
  padding: 1.3rem 0;
  border-bottom: 1px solid var(--line);
  animation: log-in 0.45s cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: calc(var(--i, 0) * 55ms);
  /* The feed only appends — cards are never recycled — so after long scrolling
     the browser would lay out & paint hundreds of off-screen logs each reflow.
     `content-visibility: auto` skips render work for entries outside the viewport
     while keeping them in the DOM (find-in-page, a11y, the observer all intact).
     `auto 200px` seeds the placeholder height so the scrollbar stays stable. */
  content-visibility: auto;
  contain-intrinsic-size: auto 200px;
}
.log[data-category="news"]    { --rail: var(--yellow); }
.log[data-category="release"] { --rail: var(--red); }
.log[data-category="social"]  { --rail: var(--ink-dim); }
.log[data-category="mix"]     { --rail: var(--mix); }

/* Timecode gutter — the log-sheet spine. The rail tick is the only category
   colour on the left and lights up on hover (honest interactive affordance). */
.log__gutter {
  position: relative;
  padding-right: 0.85rem;
}
.log__gutter::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0.25rem;
  bottom: 0.25rem;
  width: 2px;
  background: var(--rail);
  opacity: 0.7;
  transition: opacity 0.18s ease, box-shadow 0.18s ease;
}
.log__code {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.05rem;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--rail);
}
.log__ago {
  display: block;
  margin-top: 0.3rem;
  font-family: var(--font-mono);
  font-size: 0.54rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.log__main {
  padding-left: 1rem;
  min-width: 0;   /* let long titles wrap instead of stretching the grid column */
}

.log--link { cursor: pointer; }   /* whole-card click-through (app.js); links opt out */
.log--link:hover { background: rgba(255, 255, 255, 0.018); }
.log:hover .log__gutter::after { opacity: 1; box-shadow: 0 0 12px var(--rail); }
@keyframes log-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.log__meta {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
.log__src {
  font-family: var(--font-mono);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-size: 0.7rem;
  color: var(--rail);
}
.log__cat {
  font-size: 0.55rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-dim);
  border: 1px solid var(--line);
  padding: 0.1rem 0.4rem;
}

.log__title {
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 700;
  line-height: 1.22;
  letter-spacing: -0.018em;
  margin-top: 0.55rem;
  overflow-wrap: break-word;   /* a long unbroken title can't overflow the column */
}
.log__title a {
  color: var(--ink);
  text-decoration: none;
  background-image: linear-gradient(var(--yellow), var(--yellow));
  background-size: 0% 2px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size 0.2s ease, color 0.2s ease;
}
.log__title a:hover { color: var(--yellow); background-size: 100% 2px; }

.log__summary {
  margin-top: 0.6rem;
  color: var(--ink-dim);
  font-size: 0.85rem;
  font-weight: 400;
  line-height: 1.72;        /* generous leading so the mono body never reads as a wall */
  max-width: 46ch;          /* cap the measure for comfortable reading */
  word-break: break-word;
}

/* Responsive 16:9 embed (overrides the iframe's fixed width/height attrs). */
.embed {
  position: relative;
  margin-top: 0.8rem;
  overflow: hidden;
  background: #000;
  border: 1px solid var(--line);
  aspect-ratio: 16 / 9;
}
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* ── Retune control ("▲ newest" / "N new") ───────────────────────────────
   One fixed console-key (cut-corner, like .state__retry) that fades in after a long
   scroll. When live re-fetch buffers fresh transmissions it flips to the hot
   red alert rail and pulses like the ON-AIR dot — honest signal: the glow
   means there is genuinely new content waiting up the dial. */
.totop {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 30;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-on-accent);
  background: var(--yellow);
  border: 0;
  padding: 0.5rem 0.8rem;
  cursor: pointer;
  /* cut corner — shares the console-key shape with .state__retry */
  clip-path: polygon(0 0, 100% 0, 100% 100%, 9px 100%, 0 calc(100% - 9px));
  box-shadow: 3px 3px 0 #000;
  /* hidden until scrolled down / new items pending */
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity 0.25s ease,
              transform 0.25s cubic-bezier(0.2, 0.7, 0.2, 1),
              visibility 0.25s;
}
.totop--show { opacity: 1; visibility: visible; transform: none; }
.totop:hover {
  background: var(--bg);
  color: var(--yellow);
  box-shadow: inset 0 0 0 1.5px var(--yellow), 3px 3px 0 #000;
}
.totop__icon { font-size: 0.8em; line-height: 1; }

/* "N new" — fresh transmissions waiting: hot red rail + ON-AIR-style pulse */
.totop--new { background: var(--red); animation: totop-pulse 1.5s ease-in-out infinite; }
.totop--new:hover {
  background: var(--bg);
  color: var(--red);
  box-shadow: inset 0 0 0 1.5px var(--red), 3px 3px 0 #000;
}
@keyframes totop-pulse {
  0%, 100% { box-shadow: 3px 3px 0 #000, 0 0 0 0 rgba(255, 59, 48, 0); }
  50%      { box-shadow: 3px 3px 0 #000, 0 0 14px 2px rgba(255, 59, 48, 0.55); }
}

/* ── Player ("tune in" — continuous playback) ────────────────────────────
   Audio-first docked bar (bottom-left; .totop owns bottom-right). The video
   screen collapses to height 0 — the iframe STAYS rendered (never display:none)
   so audio keeps playing while collapsed. Console-key vocabulary throughout;
   no album-art / streaming-app chrome (.impeccable.md anti-reference). */

/* tune-in trigger — at the dial, beside ON-AIR. Console-key (cut corner), outlined
   at rest so it doesn't fight the yellow retune/retry keys, fills yellow on hover. */
.tunein {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink);
  background: var(--bg-panel);
  border: 1px solid var(--line);
  padding: 0.35rem 0.7rem;
  cursor: pointer;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 8px 100%, 0 calc(100% - 8px));
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.tunein:hover { color: var(--ink-on-accent); background: var(--yellow); border-color: transparent; }
/* Active (tuned in): the toggle reads "engaged/live" with a persistent yellow
   fill — the same accent the hover invites — so on/off is unambiguous at a glance
   and the ■ "tune out" glyph + label make the reverse action explicit. */
.tunein[aria-pressed="true"] { color: var(--ink-on-accent); background: var(--yellow); border-color: transparent; }
.tunein__icon { font-size: 0.85em; line-height: 1; }
/* Reserve the widest label ("tune out") so toggling in⇄out never resizes the
   button. The ▶/■ icon is width-stable, so the label is the only variable; an
   em-based, label-scoped floor is padding- and breakpoint-independent (measured
   "tune out" ≈ 5.62em @0.7rem; left-aligned so "tune in" trails reserved space). */
.tunein__label { min-width: 5.65em; text-align: left; }

.player {
  position: fixed;
  left: 1rem;
  bottom: 1rem;
  z-index: 30;
  width: min(360px, calc(100vw - 2rem));
  background: var(--bg-panel);
  border: 1px solid var(--line);
  box-shadow: 3px 3px 0 #000;
}
/* Slide-up entrance on tune-in — fires when the [hidden] attribute is removed
   (display:none → block restarts the animation). Shares the totop's easing. */
.player:not([hidden]) { animation: player-in 0.25s cubic-bezier(0.2, 0.7, 0.2, 1); }
@keyframes player-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* video screen — collapsed by default (height 0). Overrides .embed's aspect-ratio
   so the iframe (position:absolute, height:100% of 0) is 0px yet still RENDERED;
   audio continues. Expanding restores the 16:9 box. */
.player__screen.embed {
  margin: 0;
  height: 0;
  aspect-ratio: auto;
  border: 0;
  transition: height 0.25s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.player--expanded .player__screen.embed {
  height: auto;
  aspect-ratio: 16 / 9;
  border-bottom: 1px solid var(--line);
}

.player__bar {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.5rem 0.6rem;
}

/* Live transmission meter — HONEST signal: rests faint and static, and only
   lights yellow + animates while .player--playing is set (the sole EQ now; the
   always-on header meter was removed as redundant decoration). */
.player__eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 13px; flex-shrink: 0; }
.player__eq i {
  width: 2px;
  height: 100%;
  background: var(--ink-faint);     /* resting: unlit */
  transform: scaleY(0.3);
  transform-origin: bottom;
}
.player--playing .player__eq i {
  background: var(--yellow);         /* lit while transmitting */
  animation: eq 0.85s ease-in-out infinite alternate;
}
.player--playing .player__eq i:nth-child(2) { animation-duration: 0.5s; }
.player--playing .player__eq i:nth-child(3) { animation-duration: 1.05s; }

/* Station readout — a teletype state tag stacked over the source · title. */
.player__now {
  flex: 1;
  min-width: 0;                /* let the column shrink so the track ellipsis works */
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  font-family: var(--font-mono);
}
.player__tag {
  font-size: 0.55rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--yellow);
}
.player__tag:empty { display: none; }   /* errors clear the tag → no false "transmitting" */
.player__track {
  font-size: 0.72rem;
  line-height: 1.3;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* hero (play/pause) + a breath + the tight secondary cluster (next/expand/close) */
.player__controls { display: inline-flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.player__group { display: inline-flex; gap: 0.3rem; }

/* console-key control buttons (play/next/expand/close) */
.pbtn {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  line-height: 1;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  width: 1.9rem;
  height: 1.9rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.pbtn:hover { color: var(--yellow); border-color: var(--ink-dim); }

/* Primary transport — the hero key. Filled with the signal colour (same CTA
   vocabulary as .totop/.state__retry) so it's unmistakably the main action;
   hover inverts to the outlined ring those keys use. */
.pbtn--primary { color: var(--ink-on-accent); background: var(--yellow); border-color: transparent; }
.pbtn--primary:hover {
  color: var(--yellow);
  background: var(--bg);
  border-color: transparent;
  box-shadow: inset 0 0 0 1.5px var(--yellow);
}

/* Locked transport while tuning in — dimmed, no hover lift (close stays enabled). */
.pbtn:disabled { opacity: 0.4; cursor: default; }
.pbtn:disabled:hover { color: var(--ink); border-color: var(--line); box-shadow: none; }
.pbtn--primary:disabled:hover { color: var(--ink-on-accent); background: var(--yellow); }

/* Autoplay blocked (mobile policy): the player is loaded but paused. Pulse a ring
   around the (already-yellow) play key to invite a tap — the readout reads
   "tap ▶ to start". One tap then starts the station. */
.player--cued .pbtn--primary { animation: cue-pulse 1.15s ease-out infinite; }
@keyframes cue-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(255, 229, 0, 0.6); }
  70%  { box-shadow: 0 0 0 10px rgba(255, 229, 0, 0); }
  100% { box-shadow: 0 0 0 0 rgba(255, 229, 0, 0); }
}

/* Play-order toggle — a TEXT key among the glyph transport buttons. Auto width
   (both "random"/"latest" are 6 mono chars, so it never resizes), uppercase to
   sit with the station's teletype voice. Reuses the .pbtn shell + hover. */
.pbtn--order {
  width: auto;
  padding: 0 0.5rem;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* "tuning in" pulses as a working indicator until the first frame is ready. */
.player--loading .player__tag { animation: tunein-pulse 1.2s ease-in-out infinite; }
@keyframes tunein-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }

/* ── Boot skeleton (fetch in flight) ────────────────────────────────────
   Approximates a card's footprint so the swap to real content shifts layout
   as little as possible. Shimmer is a warm-dark sweep — texture, not flash. */
.skeleton {
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-left: 5px solid var(--line);   /* stands in for the category rail */
  padding: 0.9rem 1.05rem 1rem 1.3rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.sk-line {
  display: block;
  height: 0.7rem;
  background: linear-gradient(90deg, var(--line) 25%, #3a352e 45%, var(--line) 65%);
  background-size: 300% 100%;
  animation: sk-shimmer 1.4s ease-in-out infinite;
}
.sk-meta  { width: 38%; height: 0.6rem; }
.sk-title { width: 86%; height: 1rem; }
.sk-title--short { width: 52%; }
.sk-sum   { width: 68%; }
@keyframes sk-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: 0 0; }
}

/* empty-filtered notice — a category filter matched nothing loaded */
.filter-empty {
  max-width: var(--maxw);
  margin: 2.5rem auto;
  padding: 0 1rem;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.05em;
  color: var(--ink-dim);
}

/* ── Loader / sentinel / states ──────────────────────────────────────── */
.sentinel { height: 1px; }
.loader {
  text-align: center;
  font-size: 0.7rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 1.8rem 1rem;
  min-height: 1rem;
}
.state {
  max-width: var(--maxw);
  margin: 3.5rem auto;
  padding: 1.5rem;
  text-align: center;
  color: var(--ink-dim);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
}
.state strong {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.4rem;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--red);
  margin-bottom: 0.5rem;
}
/* retry console-key in empty/error states — recovery without a page reload */
.state__retry {
  margin-top: 1.1rem;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-on-accent);
  background: var(--yellow);
  border: 0;
  padding: 0.5rem 0.9rem;
  cursor: pointer;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 9px 100%, 0 calc(100% - 9px));
  box-shadow: 3px 3px 0 #000;
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.state__retry:hover {
  background: var(--bg);
  color: var(--yellow);
  box-shadow: inset 0 0 0 1.5px var(--yellow), 3px 3px 0 #000;
}
.state__retry[disabled] { opacity: 0.5; cursor: default; }
.state__retry[disabled]:hover { background: var(--yellow); color: var(--ink-on-accent); box-shadow: 3px 3px 0 #000; }

/* ── Focus & motion ──────────────────────────────────────────────────── */
:focus-visible { outline: 2px solid var(--yellow); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .player--playing .player__eq i, .player--loading .player__tag, .player:not([hidden]), .onair__dot, .log, .totop--new, .sk-line, .player--cued .pbtn--primary { animation: none; }
  /* reduced-motion still flags the cued state statically (the readout says "tap ▶ to start") */
  .player--cued .pbtn--primary { box-shadow: 0 0 0 3px rgba(255, 229, 0, 0.5); }
  .log { transition: border-color 0.18s ease; }
  .log:hover { transform: none; }
  /* fade only — no slide; visibility still toggles so it stays keyboard-safe */
  .totop { transform: none; transition: opacity 0.2s ease, visibility 0.2s; }
  /* player appears without slide/animation; expand snaps rather than glides */
  .player__screen.embed { transition: none; }
}

/* ── Touch input: 44px tap targets (finger, any viewport) ────────────── */
@media (pointer: coarse) {
  /* No hover on touch — surface the title's link underline at rest so the
     headline reads as tappable (the whole card is the click target). */
  .log__title a { background-size: 100% 2px; }
  .totop { min-height: 44px; padding-inline: 1rem; }
  .state__retry { min-height: 44px; padding-inline: 1.1rem; }
  /* roomier filter tabs on touch (sticky-header economy keeps them under 44px) */
  .filter { min-height: 38px; padding-inline: 0.8rem; }
  /* 44px finger targets for the player controls + tune-in trigger */
  .pbtn { width: 44px; height: 44px; }
  .pbtn--order { width: auto; padding: 0 0.7rem; }   /* text key keeps auto width at 44px touch height */
  .tunein { min-height: 38px; padding-inline: 0.9rem; }
}

/* ── Small screens ───────────────────────────────────────────────────── */
@media (max-width: 560px) {
  .station { padding: 0.75rem 0.9rem 0; }
  .status { font-size: 0.62rem; }
  /* The status readout fills the row at phone widths, wrapping TUNE IN onto its
     own line — so let it own that line as a full-width console key (deliberate),
     rather than floating orphaned at the right. Inline-right stays on desktop. */
  .band .tunein { width: 100%; margin-left: 0; justify-content: center; }
  /* tighter timecode gutter on phones — more room for the headline */
  .log { grid-template-columns: 48px 1fr; padding: 1.15rem 0; }
  .log__main { padding-left: 0.8rem; }
  .log__title { font-size: 1.18rem; }

  /* Player → full-width docked bottom bar (the mobile pattern), instead of the
     360px desktop card that overran the bottom-right retune key. */
  .player {
    left: 0.6rem;
    right: 0.6rem;
    width: auto;
  }
  /* While the player is open, lift the retune key clear of the bar so the "N new"
     freshness pulse stays visible and thumbs can't mis-hit it. The offset clears
     the collapsed bar (≈44px touch controls + ~1rem padding + 1px borders ≈ 3.9rem)
     plus the player's own 1rem bottom inset and a gap. Only the collapsed bar is
     persistent; an expanded video is a deliberate watch-mode and may overlap. */
  body.player-open .totop { bottom: 5.5rem; }
}
