/* live-banner.css — Phase 1 styles.
   Colors reference the existing v1 palette (values mirrored for now; Phase 2
   extracts them into design tokens). When active the banner is a STICKY bar pinned
   above the nav header: it stays visible as you scroll (z-index 1100, above the
   header's 1000). The header sticks just below it — its `top` is offset by the
   live `--crab-banner-h` custom property that live-banner.js measures each render
   (0 when the banner is idle/hidden, so the header pins to the very top as before). */

:root {
  --crab-banner-bg: #2b2721;
  --crab-banner-fg: #f6ecd3;
  --crab-banner-muted: #c8bfa7;
  --crab-banner-border: #3d3830;
  --crab-banner-live: #d4603a;
  --crab-banner-live-glow: rgba(212, 96, 58, 0.45);
  --crab-banner-studio: #e08a4f;
  --crab-banner-accent: #a3ad7e;
  --crab-next-bg: #211e19;
}

.crab-live-banner[hidden] { display: none !important; }

/* Enter/leave is a CSS `grid-template-rows: 0fr ↔ 1fr` transition on __reveal,
   driven by animateBanner() toggling [data-reveal] when the show flips On Air ↔
   Dormant. This pushes the page content below smoothly without the per-frame
   full-document reflow that scripted `height` caused (the old choppiness). Duration =
   --dur-med, so prefers-reduced-motion (token → 0ms) makes it an instant toggle. */
.crab-live-banner {
  position: sticky;
  top: 0;
  z-index: 1100;              /* above the nav header (1000) so it pins on top while scrolling */
  background: linear-gradient(to bottom, var(--crab-banner-bg), #23201a);
  border-bottom: 1px solid var(--crab-banner-border);
  color: var(--crab-banner-fg);
  font-size: 0.95rem;
}

/* The collapsing row track. At rest it's 1fr (fully open) so overflow can stay
   visible and the live dot's pulsing box-shadow glow is never clipped; the clipping
   .is-revealing class is applied by JS only for the duration of a slide. */
.crab-live-banner__reveal {
  display: grid;
  grid-template-rows: 1fr;
  /* A single full-width column so __inner spans the bar (max-width:1200px, centered)
     and its flex layout can spread — lamp far left, CTA far right. Without this the
     implicit `auto` column sizes to content and the whole bar collapses to the middle.
     minmax(0,1fr) lets a long now-playing title shrink instead of overflowing. */
  grid-template-columns: minmax(0, 1fr);
  transition: grid-template-rows var(--dur-med) var(--ease-in-out);
}
.crab-live-banner__reveal > * {
  min-height: 0;
  opacity: 1;
  transition: opacity var(--dur-med) var(--ease-in-out);
}
.crab-live-banner.is-revealing .crab-live-banner__reveal > * { overflow: hidden; }
.crab-live-banner[data-reveal="closed"] .crab-live-banner__reveal { grid-template-rows: 0fr; }
.crab-live-banner[data-reveal="closed"] .crab-live-banner__reveal > * { opacity: 0; }

/* Now-playing module cards (homepage "We're live!" card + the /music module) fade and
   slide in/out when a track appears/clears, driven by the .is-shown class toggled in
   live-banner.js (revealCard). `hidden` is the removed-from-flow end state; these cards
   start at opacity:0 so a no-JS visit simply shows nothing (progressive enhancement).
   --dur-med → 0ms under prefers-reduced-motion makes it an instant swap. */
[data-now-playing],
[data-crab-nowplaying-music] {
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity var(--dur-med) var(--ease-in-out),
              transform var(--dur-med) var(--ease-in-out);
}
[data-now-playing].is-shown,
[data-crab-nowplaying-music].is-shown {
  opacity: 1;
  transform: translateY(0);
}

.crab-live-banner__inner {
  /* width:100% is REQUIRED now that __inner is a grid item of __reveal: its
     `margin: 0 auto` makes auto inline margins, which on a grid item suppress the
     default stretch and shrink it to content width (the "squished to the middle"
     bug). An explicit width fills the full-width column; max-width + auto margins
     still cap it at 1200 and center it. (box-sizing: border-box is global.) */
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0.65rem 1rem;
  display: flex;
  align-items: center;
  gap: 0.9rem;
}

/* Only the state matching [data-state] is visible. */
.crab-live-banner__state { display: none; flex: 1; align-items: center; gap: 0.75rem; }
.crab-live-banner[data-state="pre"]            .crab-live-banner__state[data-for-state="pre"],
.crab-live-banner[data-state="live-rotation"]  .crab-live-banner__state[data-for-state="live-rotation"],
.crab-live-banner[data-state="live-studio"]    .crab-live-banner__state[data-for-state="live-studio"],
.crab-live-banner[data-state="post"]           .crab-live-banner__state[data-for-state="post"],
.crab-live-banner[data-state="new-episode"]    .crab-live-banner__state[data-for-state="new-episode"] {
  display: flex;
}

.crab-live-banner__dot {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 50%;
  background: var(--crab-banner-muted);
  flex-shrink: 0;
}
.crab-live-banner__dot--live {
  background: var(--crab-banner-live);
  box-shadow: 0 0 0 0 var(--crab-banner-live-glow);
  animation: crab-pulse 2.2s ease-out infinite;
}
.crab-live-banner__dot--studio {
  background: var(--crab-banner-studio);
  box-shadow: 0 0 0 0 var(--crab-banner-live-glow);
  animation: crab-pulse 2.2s ease-out infinite;
}
.crab-live-banner__dot--new {
  background: var(--crab-banner-accent);
}

@keyframes crab-pulse {
  0%   { box-shadow: 0 0 0 0 var(--crab-banner-live-glow); }
  70%  { box-shadow: 0 0 0 14px rgba(212, 96, 58, 0); }
  100% { box-shadow: 0 0 0 0 rgba(212, 96, 58, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .crab-live-banner__dot--live,
  .crab-live-banner__dot--studio { animation: none; }
}

.crab-live-banner__text {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
  flex: 1;
  min-width: 0;
}

.crab-live-banner__label {
  font-weight: 700;
  letter-spacing: 0.01em;
  overflow-wrap: anywhere;
}

.crab-live-banner__meta {
  color: var(--crab-banner-muted);
  font-size: 0.85rem;
  overflow-wrap: anywhere;
}

.crab-live-banner__cta {
  background: var(--crab-banner-accent);
  color: #1a1713;
  padding: 0.4rem 0.85rem;
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.85rem;
  text-decoration: none;
  white-space: nowrap;
  transition: background 140ms ease;
}
/* The CTA carries two labels: the full stream label (desktop) and a compact
   "Listen Live" (mobile stack, where the button shares a row with the song info).
   Only one shows at a time — see the media queries below. */
.crab-live-banner__cta-short { display: none; }
.crab-live-banner__cta:hover,
.crab-live-banner__cta:focus-visible {
  background: #b6bf8d;
  outline: none;
}

/* Mobile: stack the CTA onto its own full-width row. The CTA lives *inside*
   .crab-live-banner__state (the flex:1 child), so the wrap that drops it to a new
   line has to happen on __state — not just on __inner. */
@media (max-width: 640px) {
  .crab-live-banner__inner {
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 0.5rem 0.6rem;
  }
  .crab-live-banner__state { flex-wrap: wrap; }
  .crab-live-banner__text { flex: 1 1 100%; }
  /* Re-assert full width over the desktop "flex: 0 1 auto" (higher-specificity)
     live-state rule, and drop the mini now-playing module onto its own row. */
  .crab-live-banner[data-state="live-rotation"] .crab-live-banner__text,
  .crab-live-banner[data-state="live-studio"]   .crab-live-banner__text { flex: 1 1 100%; }
  /* Mobile stack: the Listen-Live button and the now-playing info SHARE one row
     under the status text — button hugged left, song info pushed to the right edge.
     `order` flips the DOM (the now-playing card precedes the CTA in markup) so the
     compact "Listen Live" button sits to the LEFT; margin-left:auto on the card eats
     the gap between them. The body stays shrunk-to-content + flush-right for a clean
     margin. (On wider screens the card sits inline in the single banner row,
     untouched.) */
  /* Scoped with .crab-live-banner so margin-left:auto out-specifies the later
     `.crab-now-playing--banner { margin: 0 }` base block (equal specificity would
     otherwise let that later rule win and kill the push-right). */
  .crab-live-banner .crab-now-playing--banner {
    flex: 0 1 auto;
    order: 2;
    margin-left: auto;
    min-width: 0;
    /* Hard cap the card so a long song title can never grow it wide enough to wrap the
       "Listen Live" button onto its own row. 130px reserves the compact button + gap;
       clamping the card's hypothetical size keeps it on the button's line, and the song
       fields marquee-scroll whatever then overflows (see .crab-np-marquee-inner). */
    max-width: calc(100% - 130px);
  }
  .crab-now-playing--banner .crab-now-playing__body { flex: 0 1 auto; text-align: right; }
  /* Let the fields fill the (capped) body so the marquee measures overflow against the
     real available width, not a fixed 220px. */
  .crab-now-playing--banner .crab-now-playing__song,
  .crab-now-playing--banner .crab-now-playing__artist,
  .crab-now-playing--banner .crab-now-playing__album { max-width: none; }
  .crab-live-banner__cta {
    order: 1;
    flex: 0 0 auto;
    align-self: center;
    padding: 0.5rem 0.9rem;
    font-size: 0.8rem;
  }
  /* Swap to the compact label on the shared row. */
  .crab-live-banner__cta-full { display: none; }
  .crab-live-banner__cta-short { display: inline; }
  .crab-live-banner__meta { font-size: 0.8rem; }

  /* New-episode and post-show banners never stack their CTA. Their text column
     yields all remaining width and truncates before the button can wrap, keeping
     the banner to its compact desktop height even on very narrow phones. */
  .crab-live-banner[data-state="new-episode"] .crab-live-banner__state,
  .crab-live-banner[data-state="post"] .crab-live-banner__state {
    flex-wrap: nowrap;
    align-items: center;
    min-width: 0;
  }
  .crab-live-banner[data-state="new-episode"] .crab-live-banner__text,
  .crab-live-banner[data-state="post"] .crab-live-banner__text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
  }
  .crab-live-banner[data-state="new-episode"] .crab-live-banner__label,
  .crab-live-banner[data-state="new-episode"] .crab-live-banner__meta,
  .crab-live-banner[data-state="post"] .crab-live-banner__label,
  .crab-live-banner[data-state="post"] .crab-live-banner__meta {
    display: block;
    overflow: hidden;
    overflow-wrap: normal;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .crab-live-banner[data-state="new-episode"] .crab-live-banner__cta,
  .crab-live-banner[data-state="post"] .crab-live-banner__cta {
    flex: 0 0 auto;
    order: 1;
  }
}

/* Small phones: trim padding and type so the longest state (live-studio) fits
   without horizontal scroll down to ~280px. */
@media (max-width: 400px) {
  .crab-live-banner__inner { padding: 0.55rem 0.7rem; gap: 0.4rem 0.5rem; }
  /* Tighter gap so the compact "Listen Live" button + song info still share one row
     down here — the song text ellipsises to fit. */
  .crab-live-banner__state { gap: 0.45rem; }
  .crab-live-banner__label { font-size: 0.88rem; }
  .crab-live-banner__meta { font-size: 0.78rem; }
}

/* ---------- Next-show card ---------- */

.crab-next-show[hidden] { display: none !important; }

/* Idle shows the next-date block; live swaps to the "We're live!" block. */
.crab-next-show [data-next-live] { display: none; }
.crab-next-show[data-state="live-rotation"] [data-next-default],
.crab-next-show[data-state="live-studio"]   [data-next-default] { display: none; }
.crab-next-show[data-state="live-rotation"] [data-next-live],
.crab-next-show[data-state="live-studio"]   [data-next-live] { display: block; }

.crab-next-show__live-dot {
  display: inline-block;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--crab-banner-live);
  box-shadow: 0 0 0 0 var(--crab-banner-live-glow);
  animation: crab-pulse 2.2s ease-out infinite;
  vertical-align: baseline;
  margin-right: 0.15rem;
}
@media (prefers-reduced-motion: reduce) {
  .crab-next-show__live-dot { animation: none; }
}

.crab-next-show {
  position: relative;
  background: var(--crab-next-bg);
  border: 1px solid var(--crab-banner-border);
  border-radius: 14px;
  padding: 1.5rem 1.75rem;
  margin: 2rem auto;
  max-width: 680px;
  color: var(--crab-banner-fg);
}

.crab-next-show__eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.72rem;
  color: var(--crab-banner-accent);
  margin: 0 0 0.35rem;
  font-weight: 600;
}

.crab-next-show__date {
  margin: 0 0 0.5rem;
  font-size: 1.75rem;
  line-height: 1.15;
}
.crab-next-show__time {
  color: var(--crab-banner-muted);
  font-size: 1rem;
  font-weight: 400;
}

.crab-next-show__tags {
  position: absolute;
  top: 1rem;
  right: 1.25rem;
  display: flex;
  gap: 0.4rem;
  z-index: 1;
}
.crab-next-show__tags [hidden] { display: none; }

.crab-next-show__studio {
  color: var(--crab-banner-muted);
  font-size: 0.95rem;
  margin: 0 0 1rem;
}
.crab-next-show__studio[hidden] { display: none; }

.crab-next-show__badge {
  display: inline-block;
  background: var(--crab-banner-studio);
  color: #1a1713;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  margin-right: 0.5rem;
  vertical-align: 2px;
}

.crab-next-show__actions {
  display: flex;
  gap: 0.75rem;
  flex-wrap: nowrap;
}

.crab-next-show__btn {
  background: var(--crab-banner-accent);
  color: #1a1713;
  padding: 0.55rem 1.1rem;
  white-space: nowrap;
  flex-shrink: 1;
  min-width: 0;
  text-align: center;
  border-radius: 999px;
  font-weight: 600;
  text-decoration: none;
  transition: background 140ms ease;
  font-size: 0.9rem;
}
.crab-next-show__btn:hover,
.crab-next-show__btn:focus-visible {
  background: #b6bf8d;
  outline: none;
}
.crab-next-show__btn--ghost {
  background: transparent;
  color: var(--crab-banner-accent);
  border: 1px solid var(--crab-banner-accent);
}
.crab-next-show__btn--ghost:hover,
.crab-next-show__btn--ghost:focus-visible {
  background: rgba(163, 173, 126, 0.1);
  color: var(--crab-banner-fg);
}

/* Mobile — shrink card and buttons to keep actions side-by-side */
@media (max-width: 640px) {
  .crab-next-show {
    padding: 1.25rem 1rem;
  }
  .crab-next-show__actions {
    gap: 0.5rem;
  }
  .crab-next-show__btn {
    display: block;
    padding: 0.55rem 0.6rem;
    font-size: 0.75rem;
    letter-spacing: 0.02em;
    flex: 1 1 0;
    min-width: 0;
    text-align: center;
  }
}
@media (max-width: 360px) {
  .crab-next-show__btn {
    font-size: 0.7rem;
    padding: 0.5rem 0.4rem;
  }
}
@media (max-width: 300px) {
  /* Extreme narrow: allow wrapping as a last resort */
  .crab-next-show__actions { flex-wrap: wrap; }
}

/* ---------- Schedule-page "We're live!" section ---------- */

.schedule-live[hidden] { display: none !important; }

.schedule-live {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  background: linear-gradient(to bottom, var(--crab-banner-bg), #23201a);
  border: 1px solid var(--crab-banner-border);
  border-radius: 12px;
  padding: 0.85rem 1.1rem;
  margin: 0 0 1.5rem;
  color: var(--crab-banner-fg);
}

.schedule-live__text {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
  flex: 1;
  min-width: 0;
}
.schedule-live__label { font-weight: 700; }
.schedule-live__meta {
  color: var(--crab-banner-muted);
  font-size: 0.85rem;
  overflow-wrap: anywhere;
}
.schedule-live [data-studio][hidden] { display: none; }

.schedule-live .btn { white-space: nowrap; }

@media (max-width: 640px) {
  .schedule-live { flex-wrap: wrap; align-items: flex-start; gap: 0.6rem; }
  .schedule-live .btn { width: 100%; text-align: center; }
}
@media (max-width: 400px) {
  .schedule-live { padding: 0.7rem 0.85rem; }
  .schedule-live__meta { font-size: 0.8rem; }
}

/* ---------- Now playing (homepage card, /music/ module, /studio/ preview) ---------- */

.crab-now-playing[hidden] { display: none !important; }

.crab-now-playing {
  display: flex;
  gap: 0.9rem;
  align-items: center;
  margin: 1rem 0;
  padding: 0.75rem;
  background: rgba(0, 0, 0, 0.18);
  border: 1px solid var(--crab-banner-border);
  border-radius: 12px;
  text-align: left;
}

.crab-now-playing__art {
  width: 72px;
  height: 72px;
  flex-shrink: 0;
  border-radius: 6px;
  object-fit: cover;
  background: #36322c;
}

.crab-now-playing__body { min-width: 0; flex: 1; }

.crab-now-playing__eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.65rem;
  color: var(--crab-banner-live);
  font-weight: 700;
  margin: 0 0 0.2rem;
}
/* "Live" glow: the red Now-playing eyebrow glows, and each cycle a halo of light gathers on the
   letters then radiates outward off the glyphs and dissipates — a wave of light flowing off the
   letters (pure text-shadow on the letterforms; no size change, no rings). Same red-light
   character as the on-air tally pulse. The period is a CSS var so a per-track tempo could drive it
   later (BPM isn't in our track data — iTunes/Spotify search carry no tempo).
   Gated on prefers-reduced-motion to match the on-air dots — so in iOS Low Power Mode / Reduce
   Motion the whole thing rests together (glow + dots) rather than the glow pulsing alone. */
@media (prefers-reduced-motion: no-preference) {
  .crab-now-playing__eyebrow {
    animation: crab-np-pulse var(--np-pulse-period, 2.2s) ease-out infinite;
  }
}
/* Mirror crab-pulse's envelope exactly so the glow and the on-air dot ping in unison: flare at 0%,
   the halo radiates off the letters and fades by 70%, then rest 70→100% (the dot's ring is gone
   over that span too). Re-flares at the next 0%. ease-out + 2.2s + phase-pinned by live-banner.js. */
@keyframes crab-np-pulse {
  0%   { text-shadow: 0 0 4px rgba(212, 96, 58, 0.90), 0 0 12px rgba(212, 96, 58, 0.55); }
  70%  { text-shadow: 0 0 2px rgba(212, 96, 58, 0.18), 0 0 18px rgba(212, 96, 58, 0.00); }
  100% { text-shadow: 0 0 2px rgba(212, 96, 58, 0.18), 0 0 18px rgba(212, 96, 58, 0.00); }
}
.crab-now-playing__song {
  font-weight: 700;
  font-size: 1.05rem;
  line-height: 1.2;
  margin: 0;
  overflow-wrap: anywhere;
}
.crab-now-playing__artist {
  color: var(--crab-banner-fg);
  margin: 0.1rem 0 0;
  font-size: 0.95rem;
  overflow-wrap: anywhere;
}
.crab-now-playing__album {
  color: var(--crab-banner-muted);
  font-size: 0.85rem;
  font-style: italic;
  margin: 0.1rem 0 0;
  overflow-wrap: anywhere;
}
.crab-now-playing__album:empty { display: none; }

.crab-now-playing__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.55rem;
}
.crab-np-chip {
  display: inline-block;
  background: rgba(163, 173, 126, 0.14);
  border: 1px solid var(--crab-banner-accent);
  color: var(--crab-banner-accent);
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
  font-size: 0.78rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background 140ms ease, color 140ms ease;
}
.crab-np-chip:hover,
.crab-np-chip:focus-visible {
  background: var(--crab-banner-accent);
  color: #1a1713;
  outline: none;
}

/* Full-width variant: the /music/ page module + /studio/ preview */
.crab-nowplaying-module[hidden] { display: none !important; }
.crab-nowplaying-module {
  /* Match the body content width (page-header / table) instead of spanning the full
     viewport: same max-width + centered + 1.5rem side padding as .page-header. */
  max-width: var(--max-width, 1200px);
  margin: 0 auto;
  padding: 1.5rem 1.5rem 0;
}
.crab-nowplaying-module__eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--crab-banner-live);
  margin: 0 0 0.5rem;
}
.crab-now-playing--music {
  margin: 0;
  padding: 0.85rem 1rem;
  gap: 1rem;
  background: linear-gradient(to bottom, var(--crab-banner-bg), #23201a);
  border-radius: 14px;
}
.crab-now-playing--music .crab-now-playing__art { width: 84px; height: 84px; border-radius: 8px; }
.crab-now-playing--music .crab-now-playing__song { font-size: 1.1rem; }

/* The homepage "We're live!" card, the public /music/ module, and the /studio/ live
   preview share one layout: the streaming-link chips sit on their OWN full-width row,
   below the art + song info. Their links div is a direct child of .crab-now-playing
   (moved out of __body) so flex-wrap + a 100% basis can drop it to the next line — at
   every width. (The top banner carries no links and is excluded.) */
.crab-next-show .crab-now-playing,
.crab-nowplaying-module .crab-now-playing,
.crab-studio__preview .crab-now-playing { flex-wrap: wrap; }
.crab-next-show .crab-now-playing__links,
.crab-nowplaying-module .crab-now-playing__links,
.crab-studio__preview .crab-now-playing__links { flex-basis: 100%; }

/* Public "recently played" — a quiet, read-only disclosure under the /music/ module. It
   inherits the live-module identity (dark card, --crab-banner-* tokens, .crab-np-chip pills);
   the timestamp column is the structural device — a live set unfolding in real time. */
.crab-np-recent[hidden] { display: none !important; }
.crab-np-recent { margin: 0.6rem 0 0; }

/* Homepage "We're live!" panel: "Recently played" belongs to the now-playing
   card, so snug it up under the dark box; the space it gives up moves below it
   so the Listen Live button stays put and reads as standing on its own.
   Scoped to [data-next-live] so the /music/ live module is unchanged. */
.crab-next-show [data-next-live] .crab-now-playing { margin-bottom: 0.3rem; }
.crab-next-show [data-next-live] .crab-np-recent { margin-top: 0; }
.crab-next-show [data-next-live] .crab-np-recent + .crab-next-show__actions { margin-top: 0.7rem; }

.crab-np-recent__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  background: none;
  border: 0;
  padding: 0.3rem 0.1rem;
  cursor: pointer;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--crab-banner-muted);
  transition: color 140ms ease;
}
.crab-np-recent__toggle:hover { color: var(--crab-banner-fg); }
.crab-np-recent__toggle:focus-visible {
  outline: 2px solid var(--crab-banner-accent);
  outline-offset: 2px;
  border-radius: 4px;
}
.crab-np-recent__chevron {
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform var(--dur-fast, 140ms) var(--ease-out, ease);
}
.crab-np-recent__toggle[aria-expanded="true"] .crab-np-recent__chevron { transform: rotate(-180deg); }

/* Instant show/hide — deliberately NO height animation. Animating grid-template-rows
   here forced a full re-layout of the long /music/ song table on every frame of the
   expand (multi-second cursor lag on that page); a plain display toggle is free. */
.crab-np-recent__reveal[data-reveal="closed"] { display: none; }

.crab-np-recent__list {
  list-style: none;
  margin: 0.35rem 0 0;
  padding: 0;
}
.crab-np-recent__row {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--crab-banner-border);
}
.crab-np-recent__row:first-child { border-top: 0; }
.crab-np-recent__time {
  flex: none;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.72rem;
  color: var(--crab-banner-muted);
  padding-top: 0.15rem;
  min-width: 2.6em;
}
.crab-np-recent__art {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 4px;
  object-fit: cover;
  background: #36322c;
}
.crab-np-recent__body { min-width: 0; flex: 1; }
.crab-np-recent__main {
  font-size: 0.9rem;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.crab-np-recent__seg {
  font-style: italic;
  color: var(--crab-banner-accent);
}
.crab-np-recent__sub {
  font-size: 0.78rem;
  font-style: italic;
  color: var(--crab-banner-muted);
  margin-top: 0.1rem;
  overflow-wrap: anywhere;
}
.crab-np-recent__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: 0.4rem;
}
/* Smaller chips than the now-playing card — the recent rows are supporting, not the hero. */
.crab-np-recent__links .crab-np-chip {
  padding: 0.12rem 0.5rem;
  font-size: 0.7rem;
}

/* Compact variant: the top live banner. Chrome stripped; height bounded to the art,
   with song/artist/album pinned to the art's top/center/bottom edges. */
.crab-now-playing--banner {
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  gap: 0.6rem;
  align-items: stretch;
}
.crab-now-playing--banner .crab-now-playing__art {
  width: 60px;
  height: 60px;
  border-radius: 4px;
}
.crab-now-playing--banner .crab-now-playing__body {
  display: flex;
  flex-direction: column;
  justify-content: space-between; /* song → top, artist → center, album → bottom */
  min-width: 0;
}
/* Single-line rows with ellipsis so a long title can't blow out the banner
   (overrides the base modules' overflow-wrap: anywhere). Zero the inherited
   per-line margins and use one line-height so space-between spaces the three
   rows evenly within the (taller) art height. */
.crab-now-playing--banner .crab-now-playing__song,
.crab-now-playing--banner .crab-now-playing__artist,
.crab-now-playing--banner .crab-now-playing__album {
  margin: 0;
  line-height: 1.3;
  max-width: 220px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  overflow-wrap: normal;
}
.crab-now-playing--banner .crab-now-playing__song { font-size: 0.82rem; font-weight: 700; }
.crab-now-playing--banner .crab-now-playing__artist { font-size: 0.78rem; }
.crab-now-playing--banner .crab-now-playing__album { font-size: 0.72rem; }

/* Marquee for banner song-info fields that overflow their (width-capped) column on
   narrow screens. live-banner.js wraps the text in .crab-np-marquee-inner, measures the
   overflow, and sets --marquee-shift (negative px) + --marquee-dur, then adds .is-marquee.
   The field clips (overflow:hidden, above); the inner span ping-pongs left↔right with a
   brief hold at each end (the 6%/47–53%/94% keyframe stops), ease-in-out, slowly. */
.crab-np-marquee-inner { display: inline-block; white-space: nowrap; }
.is-marquee > .crab-np-marquee-inner {
  animation: crab-marquee var(--marquee-dur, 6s) ease-in-out infinite;
  will-change: transform;
}
@keyframes crab-marquee {
  0%, 6%    { transform: translateX(0); }
  47%, 53%  { transform: translateX(var(--marquee-shift, 0)); }
  94%, 100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .is-marquee > .crab-np-marquee-inner { animation: none; transform: none; }
}

/* Push everything after the meta text (the now-playing module + the CTA) into the
   right-hand void. The text stops growing and takes an auto right margin that eats the
   free space — so the CTA hugs the right edge whether or not the now-playing module is
   present. Scoped to the live states. */
.crab-live-banner[data-state="live-rotation"] .crab-live-banner__text,
.crab-live-banner[data-state="live-studio"]   .crab-live-banner__text {
  flex: 0 1 auto;
  margin-right: auto;
}

@media (max-width: 480px) {
  .crab-now-playing { flex-direction: column; align-items: flex-start; }
  .crab-now-playing--music .crab-now-playing__art { width: 90px; height: 90px; }
  /* …but the top banner's mini card stays INLINE — art beside the track text, never
     stacked. The generic column rule above is for the homepage/ /music/ cards; the
     banner is a compact horizontal strip and the space-between body layout needs the
     art's height to spread song/artist/album across. */
  .crab-now-playing--banner { flex-direction: row; align-items: stretch; }
  /* …and the homepage "We're live!" card + the /music/ module ALSO stay inline: art on
     the left, song/artist/album to its right (art size unchanged). The links still wrap
     to their own full-width row below, via the flex-wrap rules above. */
  .crab-next-show .crab-now-playing,
  .crab-nowplaying-module .crab-now-playing,
  .crab-studio__preview .crab-now-playing { flex-direction: row; align-items: center; }
}
@media (max-width: 360px) {
  .crab-now-playing { padding: 0.6rem; gap: 0.6rem; }
  .crab-now-playing__art { width: 64px; height: 64px; }
  .crab-now-playing--music { padding: 0.85rem 0.9rem; }
  .crab-now-playing--music .crab-now-playing__art { width: 72px; height: 72px; }
  .crab-now-playing__song { font-size: 1rem; }
  .crab-now-playing--music .crab-now-playing__song { font-size: 1.1rem; }
  .crab-now-playing__artist { font-size: 0.88rem; }
  /* Keep recent rows horizontal but tighter so a long title can't blow out the viewport. */
  .crab-np-recent__row { gap: 0.45rem; }
  .crab-np-recent__art { width: 34px; height: 34px; }
  .crab-np-recent__time { min-width: 0; }
}

/* ---------- Schedule-page special-broadcasts list ---------- */

.schedule-exceptions[hidden] { display: none !important; }
.schedule-exceptions {
  background: var(--crab-next-bg);
  border: 1px solid var(--crab-banner-studio);
  border-left-width: 4px;
  border-radius: 10px;
  padding: 1rem 1.25rem;
  margin: 0 0 1.5rem;
  color: var(--crab-banner-fg);
}
.schedule-exceptions h2 {
  margin: 0 0 0.35rem;
  font-size: 1.15rem;
  color: var(--crab-banner-studio);
}
.schedule-exceptions__intro {
  margin: 0 0 0.6rem;
  color: var(--crab-banner-muted);
  font-size: 0.9rem;
}
.schedule-exceptions__list {
  margin: 0;
  padding-left: 1.1rem;
  line-height: 1.5;
}
.schedule-exceptions__list li { margin: 0.15rem 0; }

/* ---------- Schedule-page calendar subscriptions ---------- */
/* Two side-by-side options (live-only / every show), each with a Subscribe + Download
   pair. Structure copies .crab-stats__grid's auto-fit two-up (components.css:723) and the
   .crab-next-show__btn / --ghost primary+secondary pair above; the live card carries the
   same --color-studio left border that flags in-studio elsewhere on this page. */

.cal-subs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-4);
  margin: var(--space-6) 0 var(--space-4);
}

.cal-sub {
  display: flex;
  flex-direction: column;
  background: var(--color-bg-raised);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  padding: var(--space-5);
}
/* The in-studio flag, matching .schedule-exceptions' treatment. */
.cal-sub--live {
  border-color: var(--color-studio);
  border-left-width: 4px;
}

.cal-sub__eyebrow {
  margin: 0 0 var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-ink-dim);
}
.cal-sub--live .cal-sub__eyebrow { color: var(--color-studio); }

.cal-sub__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-md);
  line-height: var(--lh-snug);
  color: var(--color-ink);
}

.cal-sub__desc {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  line-height: var(--lh-normal);
  color: var(--color-ink-dim);
  /* Pushes the actions to the card's bottom edge so both cards' buttons line up
     regardless of how long the copy runs. */
  flex: 1 1 auto;
}
.cal-sub__desc strong { color: var(--color-ink-muted); }

.cal-sub__actions {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.cal-sub__btn {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-accent-hi);
  border-radius: var(--radius-pill);
  background: var(--color-accent-hi);
  color: var(--color-bg);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.cal-sub__btn--ghost {
  background: transparent;
  color: var(--color-accent-hi);
}
.cal-sub__btn:hover,
.cal-sub__btn:focus-visible {
  background: var(--color-accent-cream);
  border-color: var(--color-accent-cream);
  color: var(--color-bg);
}

.cal-subs__note {
  margin: 0 0 var(--space-6);
  font-size: var(--text-sm);
  color: var(--color-ink-dim);
}

/* iOS Safari inflates tap targets oddly when a flex child wraps mid-row; at the narrowest
   widths give each button its own full-width row instead. */
@media (max-width: 360px) {
  .cal-sub__actions { flex-direction: column; }
}
