/* ==========================================================================
   Underground Existence — site.css
   Single stylesheet for the redesigned public site.
   Design source: design-refs/redesign-desktop.png / redesign-mobile.png
   ========================================================================== */

/* ---- Fonts (self-hosted, root-relative — Arweave manifest requirement) ---- */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-v20-latin-regular.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-v20-latin-500.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-v20-latin-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Special Elite';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/special-elite-v20-latin-regular.woff2') format('woff2');
}
@font-face {
  font-family: 'Gately';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/gately-v1-regular.woff2') format('woff2');
}
@font-face {
  font-family: 'Rationale';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/rationale-v1-regular.woff2') format('woff2');
}

/* ---- Tokens ----
   Two themes live in this file. DARK (oxblood — the brand default) is defined
   here on :root. LIGHT (warm ink on paper, same red, opt-in) overrides the
   colour half of this set in the "Light theme" block at the end of the file.
   Absence of the `data-theme` attribute IS dark, so a page whose JS never runs
   renders exactly as it always has.

   Rule for new work: no raw colour literals outside these two blocks. */
:root {
  --bg-base: #0a0606;
  --bg-panel: #140d0d;
  --bg-card: #150e0e;
  --bg-elevated: #1c1212;
  /* --accent is the HOVER/filled state (maroon, matches the play button);
     --accent-bright is the rest state, whitened per the site-wide recolor. */
  --accent: #470f0f;
  --accent-bright: #ffffff;
  --accent-dim: #7a0e16;
  --on-accent: #ffffff;
  --paper: #e8e0d0;
  --paper-ink: #2a231c;
  --text-primary: #f2ece4;
  --text-secondary: #b7aaa3;
  --text-muted: #8a7d77;
  --hairline: rgba(255, 255, 255, 0.08);
  --hairline-accent: rgba(255, 255, 255, 0.45); /* was --hairline-red; renamed because
     light theme's equivalent is not red. Do NOT reinstate the old name as an alias:
     `--hairline-red: var(--hairline-accent)` substitutes at :root, so inside the dark
     island it would resolve to the PAPER hairline, not the red one. */

  /* translucent chrome that floats over page content */
  --scrim-header: rgba(10, 6, 6, 0.94);
  --scrim-player: rgba(12, 7, 7, 0.96);
  --scrim-search: rgba(8, 5, 5, 0.97);
  --scrim-panel: rgba(10, 6, 6, 0.92);
  --scrim-backdrop: rgba(8, 5, 5, 0.62); /* behind the shared dialog */
  /* opaque chrome — the mobile menu can't be translucent, it drops over content.
     Identical in both themes: the chrome stays dark in light theme too. */
  --chrome-solid: #0a0606;

  /* hairline-weight veils: hover washes and the 1px inset lip on chrome */
  --veil-hover: rgba(255, 255, 255, 0.025);
  --veil-inset: rgba(255, 255, 255, 0.04);
  --veil-inset-strong: rgba(255, 255, 255, 0.05);
  --track-empty: rgba(255, 255, 255, 0.14);
  --track-empty-strong: rgba(255, 255, 255, 0.18);

  /* accent-derived washes and glows */
  --accent-veil: rgba(255, 255, 255, 0.08);
  --accent-veil-strong: rgba(255, 255, 255, 0.14);
  --accent-glow: rgba(255, 255, 255, 0.7);
  --accent-glow-soft: rgba(255, 255, 255, 0.4);

  /* composite shadows */
  --shadow-card-hover: 0 14px 34px rgba(0, 0, 0, 0.5), 0 0 24px rgba(255, 255, 255, 0.12);
  --shadow-frame: 0 0 0 1px rgba(255, 255, 255, 0.15), 0 0 44px rgba(255, 255, 255, 0.16), 0 24px 60px rgba(0, 0, 0, 0.55);
  /* the homepage stage + selector cards: a quiet grey hairline and a plain
     drop shadow, so the frames blend into the page instead of glowing red */
  --frame-border-soft: rgba(255, 255, 255, 0.09);
  --shadow-frame-soft: 0 18px 44px rgba(0, 0, 0, 0.45);
  --shadow-portrait: 0 0 34px rgba(255, 255, 255, 0.14), 0 20px 48px rgba(0, 0, 0, 0.5);
  --shadow-panel: 0 8px 20px rgba(0, 0, 0, 0.4);
  /* floating runtime UI (share popover, deep-link banner, resume bar — all
     injected by ue-links.js / ue-resume.js, which read these same tokens) */
  --shadow-float: 0 12px 30px rgba(0, 0, 0, 0.5);
  /* resting elevation on cards. Dark doesn't use one — it separates surfaces
     by luminance (#0a0606 page vs #150e0e card) and a shadow on near-black is
     invisible anyway. On paper that trick is unavailable, so light defines a
     real one; keeping the token here at `none` means the base rule can carry
     it unconditionally without touching dark's render. */
  --shadow-card-rest: none;
  --shadow-logo: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.6));
  --text-shadow-title: 0 2px 0 rgba(0, 0, 0, 0.4);
  --text-shadow-number: 0 1px 6px rgba(0, 0, 0, 0.8);

  /* scrims painted over artwork — legibility, not decoration */
  /* Two jobs: a light top wash so the mix number reads, and a heavy bottom
     ramp that the overlaid title/artist/meta sit on. The thumbnails average
     56/255 luminance, but a skyline's lit windows are locally bright — this
     ramp is what guarantees the type survives them. */
  /* Used by exactly one thing: .mix-card-cover::after. The lower stop moved
     36% -> 50% when the tile lost its genre/duration/date rows — the ramp had
     been sized to keep four stacked lines legible, and against the new
     two-element plate it was darkening a third of the artwork for nothing. */
  --cover-scrim: linear-gradient(180deg, rgba(10, 6, 6, 0.45) 0%, transparent 22%, transparent 50%, rgba(6, 4, 4, 0.84) 100%);
  --hero-scrim: linear-gradient(160deg, rgba(255, 255, 255, 0.14), transparent 45%), linear-gradient(0deg, rgba(10, 6, 6, 0.35), transparent 40%);
  --portrait-scrim: linear-gradient(180deg, transparent 55%, rgba(10, 6, 6, 0.55));
  --img-filter: contrast(1.05) saturate(1.02);
  --img-filter-hero: contrast(1.06) saturate(1.05);

  /* waveform canvases — read from player.js as well as from CSS */
  --wave-played: #ffffff;
  --wave-unplayed: rgba(242, 236, 228, 0.82);
  --wave-remaining: rgba(154, 143, 138, 0.55);

  /* full-page texture: film grain in dark; grain + CRT scanlines in light */
  --grain-opacity: 0.14;
  --grain-blend: normal;
  --scanline-opacity: 0;
  --scanline-pitch: 3px;
  --scanline-ink: rgba(0, 0, 0, 0.55);

  --font-mono: 'IBM Plex Mono', 'Courier New', monospace;
  --font-body: 'Rationale', Arial, sans-serif;
  --font-display: 'Special Elite', 'Courier New', serif;
  --font-script: 'Gately', cursive;
  /* square edges for now (owner 2026-10-05) — were 12px / 16px */
  --radius-card: 0;
  --radius-frame: 0;
  --player-bar-height: 84px;
  --page-max: 1560px;
  --page-pad: clamp(16px, 4vw, 56px);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ---- Reset / base ---- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
/* rotated collage props (receipt ticket, torn corners) paint past the page
   edge on iOS and let the page pan sideways — clip, never scroll, on x.
   `clip` (with `hidden` fallback) avoids creating a scroll container, so
   position: sticky keeps working */
html, body { overflow-x: hidden; overflow-x: clip; }
body {
  margin: 0;
  background: var(--bg-base);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.5;
  padding-bottom: calc(var(--player-bar-height) + 24px);
  min-height: 100vh;
  min-height: 100dvh;
  position: relative;
}
/* keyboard focus — accent ring, mouse clicks unaffected */
:focus-visible {
  outline: 2px solid var(--accent-bright);
  outline-offset: 3px;
  border-radius: 2px;
}
/* film grain over everything */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 2000;
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--grain-blend);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* CRT scanlines — second fixed layer, off (opacity 0) in dark, on in light.
   Separate from the grain layer because the two need different blend modes:
   grain multiplies into the paper, scanlines sit on top as ruled ink. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 2001;
  opacity: var(--scanline-opacity);
  background-image: repeating-linear-gradient(
    to bottom,
    var(--scanline-ink) 0 1px,
    transparent 1px var(--scanline-pitch)
  );
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button {
  font-family: var(--font-body);
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  padding: 0;
}
h1, h2, h3, h4, p, ul { margin: 0; padding: 0; }
ul { list-style: none; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.page-shell {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 0 var(--page-pad);
}
.icon-svg { width: 1em; height: 1em; fill: currentColor; vertical-align: -0.12em; }

/* ==========================================================================
   Header / nav
   ========================================================================== */
#site-header {
  position: sticky;
  top: 0;
  z-index: 900;
  background: var(--scrim-header);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hairline-accent);
  box-shadow: inset 0 -1px 0 var(--veil-inset);
}
#site-header-inner {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 0 var(--page-pad);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  height: 72px;
}
#site-wordmark { display: inline-flex; align-items: center; flex: none; min-width: 0; }
#site-wordmark-logo { height: clamp(32px, 3vw, 44px); width: auto; display: block; }
/* the tagline beside the UE mark: small white script */
#site-wordmark { gap: clamp(8px, 1vw, 12px); }
#site-wordmark-tagline {
  font-family: var(--font-script);
  font-size: clamp(18px, 1.7vw, 23px);
  line-height: 1;
  color: #fff;
  white-space: nowrap;
  transform: translateY(1px) rotate(-2deg);
}
#primary-nav { display: flex; align-items: center; gap: clamp(18px, 3vw, 42px); }
.primary-nav-link {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-secondary);
  padding: 6px 2px;
  position: relative;
  transition: color 0.2s ease;
}
.primary-nav-link:hover { color: var(--text-primary); }
.primary-nav-link.is-active { color: var(--accent-bright); }
.primary-nav-link::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -4px;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.35s var(--ease-out-expo);
}
.primary-nav-link:hover::after { transform: scaleX(1); }
.primary-nav-link.is-active::after { transform: scaleX(1); }
#nav-desktop-actions { display: flex; align-items: center; gap: 20px; }
#nav-search-toggle, #mobile-search-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  letter-spacing: 0.14em;
  color: var(--text-secondary);
  transition: color 0.2s ease;
}
#nav-search-toggle:hover, #mobile-search-toggle:hover { color: var(--text-primary); }
#nav-search-divider { width: 1px; height: 22px; background: var(--hairline); }
/* theme toggle — same affordance as the search button, icon-only.
   The button's icon (#i-sun / #i-moon), aria-label and aria-pressed are
   swapped by the inline theme script in partials/head.html; CSS only
   carries the resting/hover/active treatment. */
#nav-theme-toggle, #mobile-theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
  transition: color 0.2s ease, transform 0.3s var(--ease-out-expo);
}
#nav-theme-toggle:hover, #mobile-theme-toggle:hover { color: var(--text-primary); }
#nav-theme-toggle:active, #mobile-theme-toggle:active { transform: scale(0.9); transition-duration: 0.1s; }
#nav-theme-toggle .icon-svg { width: 18px; height: 18px; }
#mobile-nav-actions { display: none; align-items: center; gap: 18px; }
#mobile-menu-toggle { font-size: 20px; color: var(--text-primary); display: inline-flex; }

/* Mobile slide-down menu */
#mobile-menu {
  display: none;
  border-top: 1px solid var(--hairline);
  background: var(--chrome-solid);
  padding: 12px var(--page-pad) 20px;
}
#mobile-menu.is-open { display: block; }
.mobile-menu-link {
  display: block;
  padding: 13px 2px;
  font-size: 15px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-secondary);
  border-bottom: 1px solid var(--hairline);
}
.mobile-menu-link.is-active { color: var(--accent-bright); }

@media (max-width: 1020px) {
  #primary-nav, #nav-desktop-actions { display: none; }
  #mobile-nav-actions { display: flex; }
  #site-header-inner { height: 60px; }
}
@media (min-width: 1021px) {
  #mobile-menu { display: none !important; }
}
@media (max-width: 700px) {
  /* logo scales up for mobile prominence; capped well short of the
     old barcode-mark's fill-to-edge size, which this chunkier 4.5:1
     mark made look oversized at the same width */
  #site-wordmark-logo { height: clamp(34px, 9vw, 48px); }
  #site-header-inner { height: auto; min-height: 60px; padding-top: 10px; padding-bottom: 10px; }
}

/* ==========================================================================
   Eyebrows / section headings
   ========================================================================== */
.section-eyebrow {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent-bright);
  display: inline-block;
  padding-bottom: 6px;
  border-bottom: 2px solid var(--accent);
}
.section-eyebrow--plain {
  color: var(--text-primary);
  border-bottom-color: var(--text-primary);
}
.section-header-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 26px;
}
.section-view-all {
  font-size: 13px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent-bright);
  white-space: nowrap;
}
.section-view-all:hover { color: var(--text-primary); }

/* ==========================================================================
   Featured hero
   ========================================================================== */
#featured-mix-hero {
  display: grid;
  grid-template-columns: minmax(340px, 560px) 1fr;
  gap: clamp(28px, 4vw, 64px);
  align-items: center;
  padding: clamp(28px, 4vw, 56px) 0 clamp(32px, 4vw, 60px);
  position: relative;
}

/* hero image frame */
#hero-collage-frame {
  position: relative;
  border-radius: var(--radius-frame);
  border: 1px solid var(--frame-border-soft);
  box-shadow: var(--shadow-frame-soft);
}
#hero-collage-frame .hero-image-clip {
  /* MUST stay positioned: the still/video/artwork layers inside are absolute,
     and without this their containing block is #hero-collage-frame — which
     puts them outside this element's overflow, so the border-radius stops
     clipping them and the hero renders with square corners. */
  position: relative;
  border-radius: var(--radius-frame);
  overflow: hidden;
  /* SQUARE, because the content is square: every generated mix promo is
     720x720 (verified across the catalog), and the skyline thumbnails the hero
     swaps to are generated square too. The old 4/3.4 box meant the promo either
     letterboxed inside it or got cropped. Matching the media means the film
     fills the frame exactly, with nothing trimmed and no bars. */
  aspect-ratio: 1 / 1;
  background: var(--bg-panel);
}
#hero-collage-frame .hero-image-clip picture {
  display: block;
  width: 100%;
  height: 100%;
}
/* Three stacked layers in one frame: the still (always painted, and the
   video's poster), the promo clip, and the playing mix's artwork. Only one is
   visible at a time — `data-hero-mode` on #hero-media decides which, so a
   failed fetch, a blocked autoplay or no JS at all simply leaves the still.
   They cross-fade rather than cut, because the swap happens under the user's
   eye the moment they press play. */
#hero-collage-frame .hero-image-clip picture,
#hero-collage-frame .hero-image-clip img,
#hero-collage-frame .hero-image-clip video,
#hero-collage-frame .hero-image-clip .cover-fallback {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--img-filter-hero);
}
#hero-promo-video,
#hero-mix-art {
  opacity: 0;
  /* a soft cross-fade, not a cut: every switch (film / cover / portrait, a
     refresh, a tile click) eases in under the visitor's eye */
  transition: opacity 0.9s ease;
}
/* The promo film is CONTAINED, not cropped: these are 16:9 renders and the
   hero frame is ~4/3.4, so `cover` was cutting the sides off a composition
   that was authored to be seen whole. It letterboxes against the dark well
   (--bg-panel), which is the same recess the artwork sits in.
   The still and the mix artwork keep `cover` — they are stills chosen to fill
   the frame, and containing them would leave bars for no reason. */
/* needs the full ancestor chain: the grouped rule above that sets `cover` is
   `#hero-collage-frame .hero-image-clip video` (0,1,1,1), so a bare
   `#hero-promo-video` (0,1,0,0) loses to it */
#hero-collage-frame .hero-image-clip #hero-promo-video { object-fit: contain; }
/* `artist` rides the same <img> layer as `art` — one still layer, two meanings,
   see showStill() in the homepage's inline hero script. */
#hero-media[data-hero-mode="promo"] #hero-promo-video,
#hero-media[data-hero-mode="art"] #hero-mix-art,
#hero-media[data-hero-mode="artist"] #hero-mix-art { opacity: 1; }
/* A portrait is not a 720x720 skyline frame — contain it rather than crop a
   face to fill a square. */
#hero-collage-frame .hero-image-clip #hero-mix-art { object-fit: cover; }
#hero-media[data-hero-mode="artist"] #hero-mix-art { object-fit: contain; }

/* ---- the source thumbs: film / cover / artist -------------------------
   Inside the frame, under the square, so the choice reads as belonging to the
   image it changes. Each button is hidden by the script until its source
   resolves for the current mix, and the strip hides itself when nothing is
   left — a single thumb is not a choice. */
#hero-source-thumbs {
  display: flex;
  gap: 10px;
  padding: 12px;
  border-top: 1px solid var(--hairline);
}
.hero-thumb {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  width: 64px;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.hero-thumb-frame {
  position: relative;
  display: block;
  width: 64px;
  height: 64px;
  border-radius: 0;
  overflow: hidden;
  background: var(--bg-panel);
  border: 1px solid var(--hairline);
  transition: border-color 0.2s var(--ease-out-expo), transform 0.16s var(--ease-out-expo);
}
.hero-thumb-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: var(--img-filter);
}
.hero-thumb-badge {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(8, 5, 5, 0.42);
  color: #fff;
}
.hero-thumb-badge .icon-svg { width: 16px; height: 16px; }
.hero-thumb-label {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color 0.2s ease;
}
@media (hover: hover) and (pointer: fine) {
  .hero-thumb:hover .hero-thumb-frame { border-color: var(--accent-bright); }
  .hero-thumb:hover .hero-thumb-label { color: var(--text-primary); }
}
.hero-thumb:active .hero-thumb-frame { transform: scale(0.96); transition-duration: 0.1s; }
.hero-thumb[aria-pressed="true"] .hero-thumb-frame {
  border-color: var(--accent-bright);
  box-shadow: 0 0 0 1px var(--accent-bright);
}
.hero-thumb[aria-pressed="true"] .hero-thumb-label { color: var(--text-primary); }
@media (max-width: 540px) {
  #hero-source-thumbs { gap: 8px; padding: 10px; }
  .hero-thumb, .hero-thumb-frame { width: 56px; }
  .hero-thumb-frame { height: 56px; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-thumb-frame, .hero-thumb-label { transition: none; }
  .hero-thumb:active .hero-thumb-frame { transform: none; }
}

/* --- the branded plate, worn only in `art` mode ------------------------
   Wordmark at the top, mix details at the bottom, over the skyline frame —
   the same furniture the generated promo films carry burnt in, so the hero
   reads as one piece whether it is playing the film or showing the mix.
   Everything here sits ON artwork, so the ink is light in both themes and
   the plate provides its own scrim rather than trusting the image. */
#hero-mix-plate {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: clamp(14px, 2.2vw, 22px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease-out-expo);
  background:
    linear-gradient(180deg, rgba(6, 4, 4, 0.72), transparent 26%),
    linear-gradient(0deg, rgba(6, 4, 4, 0.82), transparent 38%);
}
#hero-media[data-hero-mode="art"] #hero-mix-plate { opacity: 1; }

/* The handwritten artist mark on the hero cover — the same mark, the same
   renderer and the same one division as the cards (see the "handwritten artist
   mark" block), so the hero and the grid read as one system.

   Shown for the mix COVER only. Over the promo film it would fight the
   furniture already burnt into the frame, and over a selector's portrait it
   would repeat what that page already says. It sits under the plate (z-index 3)
   and over the artwork, and the frame is its sizing container. */
#hero-collage-frame .hero-image-clip { container-type: inline-size; }
#hero-art-mark {
  z-index: 2;
  opacity: 0;
  transition: opacity 0.5s var(--ease-out-expo);
  /* the plate's title/artist occupy the bottom of the frame, so the mark takes
     the picture above it — same reasoning as the mix card's own padding */
  padding-bottom: 12%;
  --script-fit: 0.78;
  --script-h: 34cqi;
}
#hero-media[data-hero-mode="art"] #hero-art-mark { opacity: 1; }
/* full ancestor chain: the wordmark is an <img> inside .hero-image-clip, so the
   grouped "fill the frame" rule (#hero-collage-frame .hero-image-clip img —
   absolute, inset 0, 100%x100%) outranks a bare #hero-plate-logo and stretched
   it across the entire hero */
#hero-collage-frame .hero-image-clip #hero-plate-logo {
  position: static;
  inset: auto;
  width: clamp(120px, 46%, 250px);
  height: auto;
  object-fit: contain;
  filter: none;
  align-self: flex-start;
}
#hero-plate-title {
  font-family: var(--font-body);
  font-size: clamp(19px, 2.2vw, 30px);
  line-height: 1.12;
  color: #f6f3ee;
  text-wrap: balance;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.55);
}
#hero-plate-artist {
  margin-top: 4px;
  font-family: var(--font-script);
  font-size: clamp(20px, 2.4vw, 32px);
  line-height: 1.1;
  color: #e01e2b;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.5);
}

/* --- controls on the film: mute + share -------------------------------- */
#hero-media-controls {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 4;
  display: flex;
  gap: 8px;
  opacity: 0;
  transition: opacity 0.3s var(--ease-out-expo);
}
/* they belong to the film, so they appear with it — and stay reachable by
   keyboard even while the hero is still showing the poster */
#hero-media[data-hero-mode="promo"] #hero-media-controls,
#hero-media[data-hero-mode="art"] #hero-media-controls,
#hero-media:hover #hero-media-controls,
#hero-media-controls:focus-within { opacity: 1; }
/* touch screens have no hover to reveal them, and the refresh button lives here */
@media (hover: none) { #hero-media-controls { opacity: 1; } }
#hero-media-controls button {
  width: 38px;
  height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.28);
  background: rgba(6, 4, 4, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #f6f3ee;
  transition: background 0.2s ease, border-color 0.2s ease, transform 0.15s var(--ease-out-expo);
}
#hero-media-controls button:hover {
  background: rgba(6, 4, 4, 0.78);
  border-color: rgba(255, 255, 255, 0.5);
}
#hero-media-controls button:active { transform: scale(0.94); transition-duration: 0.1s; }
#hero-media-controls .icon-svg { width: 16px; height: 16px; }
/* the mute button shows the state it will MOVE to, like the theme toggle:
   muted now => offer sound */
#hero-mute-toggle .icon-unmuted { display: none; }
#hero-media[data-hero-muted="off"] #hero-mute-toggle .icon-muted { display: none; }
#hero-media[data-hero-muted="off"] #hero-mute-toggle .icon-unmuted { display: block; }
@media (prefers-reduced-motion: reduce) {
  #hero-mix-plate, #hero-media-controls { transition: none; }
}
/* the artwork wins outright once a mix is playing — the clip stops paying for
   itself the moment there is something truer to show */
#hero-media[data-hero-mode="art"] #hero-promo-video { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  #hero-promo-video, #hero-mix-art { transition: none; }
}
#hero-collage-frame .hero-image-clip::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--radius-frame);
  background: var(--hero-scrim);
  pointer-events: none;
}
#featured-mix-content { position: relative; min-width: 0; }
#featured-mix-title {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(34px, 4.6vw, 64px);
  line-height: 1.06;
  color: var(--text-primary);
  margin: 18px 0 2px;
  text-shadow: var(--text-shadow-title);
}
#featured-artist-signature {
  font-family: var(--font-script);
  font-size: clamp(40px, 4.4vw, 62px);
  line-height: 1.15;
  /* exempt from the site-wide accent recolor: the mix title keeps the brand
     red regardless of what --accent-bright now points to elsewhere */
  color: #e01e2b;
  display: inline-block;
  transform: rotate(-2deg);
  margin: 2px 0 10px;
}
#featured-artist-signature:hover { color: var(--text-primary); }
#featured-meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  font-size: 14px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: 26px;
}
#featured-meta-row .meta-dot { color: var(--accent); }
.meta-chip { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.meta-chip .icon-svg { color: var(--text-muted); width: 15px; height: 15px; }

#featured-player-row {
  display: flex;
  align-items: center;
  gap: clamp(16px, 2vw, 28px);
}
#featured-waveform { flex: 1; min-width: 0; }
@media (max-width: 540px) {
  /* waveform was squeezed to ~180px next to the play/download buttons on
     phones — give it its own full-width row above the controls instead */
  #featured-player-row { flex-wrap: wrap; justify-content: center; row-gap: 18px; }
  #featured-waveform { order: -1; flex-basis: 100%; }
}

/* circular play buttons */
.play-circle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 2px solid var(--accent-bright);
  color: var(--accent-bright);
  background: var(--accent-veil);
  transition: background 0.2s var(--ease-out-expo), color 0.2s var(--ease-out-expo), transform 0.16s var(--ease-out-expo);
  flex: none;
}
.play-circle:hover {
  background: var(--accent);
  color: var(--on-accent);
}
/* The one control on this site that had no press state, while .download-btn
   beside it always has. Play is the most-pressed thing on the page; it has to
   answer the finger before the audio does. */
.play-circle:active { transform: scale(0.96); transition-duration: 0.1s; }
/* No margin nudge on the glyph. A play triangle does need optical centring —
   its centroid sits 1/3 from the base, not at its bounding-box centre — but
   that compensation is already BUILT INTO the icon: #i-play spans x 8→20.9 in
   a 24-unit viewBox, so its centroid lands at 12.3 against a centre of 12.
   The old `margin-left: 3px/2px` applied it a second time, pushing the glyph
   +4.47px right of centre on the 76px button and +2.72px on the 42/46px ones
   (measured). #i-pause spans 7→17 and is centred exactly, which is why it
   needed its own `margin-left: 0` to undo the nudge — with the nudge gone,
   both glyphs are correct and the override is unnecessary. */
.play-circle--lg { width: 76px; height: 76px; }
.play-circle--lg .icon-svg { width: 26px; height: 26px; }
.play-circle--sm { width: 42px; height: 42px; border-width: 1.5px; }
.play-circle--sm .icon-svg { width: 15px; height: 15px; }
.play-circle .icon-pause { display: none; }
.play-circle.is-playing .icon-play { display: none; }
.play-circle.is-playing .icon-pause { display: block; }

/* waveform — interactive scrubber (click/drag seeks; mixes are served
   from a Range-capable ar.io gateway, see MIX_AUDIO_SCRUBBING.md) */
.waveform-track { position: relative; height: 64px; cursor: pointer; touch-action: none; }
.waveform-track canvas { width: 100%; height: 100%; display: block; }
.waveform-progress-line {
  position: relative;
  height: 2px;
  background: var(--track-empty);
  margin-top: 12px;
  cursor: pointer;
  touch-action: none;
}
/* generous invisible hit area for the thin progress line */
.waveform-progress-line::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: -10px; bottom: -10px;
}
.waveform-progress-line .progress-fill {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 0%;
  background: var(--accent-bright);
}
.waveform-progress-line .progress-dot {
  position: absolute;
  top: 50%;
  left: 0%;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--accent-bright);
  transform: translate(-50%, -50%);
  box-shadow: 0 0 10px var(--accent-glow);
}
.waveform-time-row {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
  font-family: var(--font-mono);
  font-size: 15px;
  letter-spacing: 0.06em;
}
.waveform-time-row .time-current { color: var(--accent-bright); }
.waveform-time-row .time-total { color: var(--text-muted); }

@media (max-width: 1020px) {
  #featured-mix-hero { grid-template-columns: 1fr; }
  #featured-mix-content { text-align: center; }
  #featured-meta-row { justify-content: center; }
}

/* ==========================================================================
   Featured hero — Signal stage
   The media frame and the copy share ONE grid cell: the film / cover /
   portrait is the stage's backdrop and the title, artist, meta and waveform
   sit over its lower edge, so the page opens as a player. This block only
   re-lays-out the hero above — the markup, every id and the inline hero
   script are unchanged, so a selector-tile click still repoints the copy
   (player.js pointHeroAt) and the media (ue-hero-repointed).
   Height follows the viewport so the Selectors header and the top of its
   cards stay above the fold; tune --hero-stage-reserve to show more or less.
   In light theme the stage is still artwork, so it joins the dark island.
   ========================================================================== */
#featured-mix-hero {
  /* FULL-VIEWPORT STAGE: the section is exactly the visible window between the
     sticky nav and the fixed player bar (nav 72 + 1px border, 61 at <=1020;
     bar 84, 68 at <=900), so the stage fills the screen on arrival and nothing
     sits behind the bar. vh first, dvh over it where supported, so mobile
     browser chrome never leaves a gap or a hidden strip. min-height keeps a
     usable stage in short landscape windows (the page then just scrolls). */
  --hero-nav-h: 73px;
  --hero-bar-h: var(--player-bar-height);
  --hero-stage-pad: clamp(8px, 1.4vw, 16px);
  height: calc(100vh - var(--hero-nav-h) - var(--hero-bar-h));
  height: calc(100dvh - var(--hero-nav-h) - var(--hero-bar-h));
  min-height: 420px;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: 0;
  align-items: stretch;
  padding: var(--hero-stage-pad) 0;
}
@media (max-width: 1020px) { #featured-mix-hero { --hero-nav-h: 61px; } }
@media (max-width: 900px)  { #featured-mix-hero { --hero-bar-h: 68px; } }
#featured-mix-hero #hero-collage-frame,
#featured-mix-hero #featured-mix-content { grid-area: 1 / 1; }
/* Fade from black on load (hero script, data-hero-boot): the built-in still is
   never shown once JS runs — it may be a different mix than the one the player
   picks — so the clip is black and the copy is hidden until the first real mix
   lands; then the copy fades in here and the cover fades in via #hero-mix-art. */
#featured-mix-hero[data-hero-boot] #hero-still { opacity: 0; }
/* a mix change resets the stage instantly before replaying the intro */
#featured-mix-hero[data-hero-resetting] #featured-mix-content > *,
#featured-mix-hero[data-hero-resetting] #hero-promo-video,
#featured-mix-hero[data-hero-resetting] #hero-mix-art { transition: none !important; }
#featured-mix-hero[data-hero-boot] #hero-collage-frame .hero-image-clip { background: #000; }
/* ...and the copy then rises in top to bottom, one line after another:
   eyebrow, artist, mix title, duration, waveform — 120ms apart, starting just
   after the cover begins its own fade. */
#featured-mix-hero[data-hero-boot] #featured-mix-content > * {
  transition: opacity 0.8s ease, transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
#featured-mix-hero[data-hero-boot="pending"] #featured-mix-content > * {
  opacity: 0;
  transform: translateY(10px);
}
#featured-mix-hero[data-hero-boot] #featured-mix-content > :nth-child(1) { transition-delay: 0.15s; }
#featured-mix-hero[data-hero-boot] #featured-mix-content > :nth-child(2) { transition-delay: 0.27s; }
#featured-mix-hero[data-hero-boot] #featured-mix-content > :nth-child(3) { transition-delay: 0.39s; }
#featured-mix-hero[data-hero-boot] #featured-mix-content > :nth-child(4) { transition-delay: 0.51s; }
#featured-mix-hero[data-hero-boot] #featured-mix-content > :nth-child(5) { transition-delay: 0.63s; }
@media (prefers-reduced-motion: reduce) {
  #featured-mix-hero[data-hero-boot] #featured-mix-content > * { transition: opacity 0.3s ease; transition-delay: 0s; }
  #featured-mix-hero[data-hero-boot="pending"] #featured-mix-content > * { transform: none; }
}
#featured-mix-hero #hero-collage-frame { height: 100%; min-height: 0; overflow: hidden; }
#featured-mix-hero #hero-collage-frame .hero-image-clip { aspect-ratio: auto; height: 100%; }
/* a backdrop fills the stage: the film and the portrait are cropped to it
   rather than letterboxed (the portrait keeps the face in the upper third) */
#featured-mix-hero #hero-collage-frame .hero-image-clip #hero-promo-video { object-fit: cover; }
#featured-mix-hero #hero-media[data-hero-mode="artist"] #hero-mix-art { object-fit: cover; object-position: 50% 25%; }
/* the stage's own copy names the mix, so the cover's plate and the script
   mark would say it twice — kept in the DOM (the script still writes them) */
#featured-mix-hero #hero-mix-plate,
#featured-mix-hero #hero-art-mark { display: none; }
/* the scrim the copy reads against: the stage deepens toward its lower-left */
#featured-mix-hero #hero-collage-frame .hero-image-clip::after {
  background:
    linear-gradient(180deg, transparent 8%, color-mix(in srgb, var(--bg-panel) 55%, transparent) 42%, color-mix(in srgb, var(--bg-panel) 94%, transparent) 88%),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-panel) 72%, transparent) 0%, transparent 64%);
}
/* film / cover / artist chooser: lives in the player bar, beside the
   now-playing thumbnail (the homepage hero script moves #hero-source-thumbs
   into #player-bar-source-slot). Small square chips, no visible labels (the
   text stays in the button for screen readers and tooltips). */
@keyframes ue-soft-in { from { opacity: 0; } to { opacity: 1; } }
/* each chip eases in when its source resolves (the script un-hides it) */
#player-bar-source-slot .hero-thumb:not([hidden]) { animation: ue-soft-in 0.6s ease both; }
#player-bar-source-slot #hero-source-thumbs {
  position: static;
  gap: 6px;
  border-top: 0;
  padding: 0;
  margin: 0;
  background: none;
}
#player-bar-source-slot .hero-thumb,
#player-bar-source-slot .hero-thumb-frame { width: 34px; }
#player-bar-source-slot .hero-thumb-frame { height: 34px; border-radius: 0; }
#player-bar-source-slot .hero-thumb-badge .icon-svg { width: 12px; height: 12px; }
/* visually hidden, still announced */
#player-bar-source-slot .hero-thumb-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
@media (max-width: 540px) {
  #player-bar-source-slot #hero-source-thumbs { gap: 4px; }
  #player-bar-source-slot .hero-thumb,
  #player-bar-source-slot .hero-thumb-frame { width: 26px; }
  #player-bar-source-slot .hero-thumb-frame { height: 26px; border-radius: 0; }
  #player-bar-source-slot .hero-thumb-badge .icon-svg { width: 10px; height: 10px; }
}
#featured-mix-hero #hero-media-controls { top: clamp(12px, 2vw, 20px); right: clamp(12px, 2vw, 20px); bottom: auto; }

#featured-mix-hero #featured-mix-content {
  z-index: 2;
  align-self: end;
  display: grid;
  gap: clamp(6px, 1.2svh, 14px);
  padding: clamp(18px, min(3.5vw, 4svh), 52px) clamp(20px, 3.5vw, 52px);
  text-align: left;
  /* the copy only covers the lower part of the stage; let clicks through its
     empty box to the frame, and catch them again on the copy itself */
  pointer-events: none;
}
#featured-mix-hero #featured-mix-content > * { pointer-events: auto; }
#featured-mix-hero #featured-mix-content .section-eyebrow { justify-self: start; }
#featured-mix-hero #featured-mix-title {
  margin: 0;
  font-size: clamp(34px, min(7vw, 7svh), 128px);
  line-height: 0.9;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
#featured-mix-hero #featured-artist-signature {
  justify-self: start;
  margin: 0;
  font-size: clamp(26px, min(3.6vw, 4.2svh), 54px);
}
#featured-mix-hero #featured-meta-row { justify-content: flex-start; margin-bottom: 0; }
#featured-mix-hero #featured-meta-row { flex-wrap: wrap; row-gap: 6px; }
/* genre / date / length are hidden for now, so the row is empty — take no room */
#featured-mix-hero #featured-meta-row:not(:has(*)) { display: none; }
#featured-mix-hero .waveform-track { height: clamp(48px, min(9vw, 8svh), 120px); }
/* The hero has no play button of its own: the player bar's play button plays
   the hero's mix, and the waveform (click/drag) starts it too. The anchor stays
   in the DOM — it is the hero's queue entry (player.js featuredAnchor: href,
   data-title/-artist/-img, data-queue-idx) — it is only never shown, so the
   waveform takes the row. The download button is hidden the same way (player.js
   still repoints #featured-download-button by id; it just never shows). */
#featured-mix-hero #featured-play-button,
#featured-mix-hero #featured-download-button { display: none; }

/* ==========================================================================
   Mix cards (homepage selector tiles / grids)
   ========================================================================== */
#homepage-selectors-section {
  padding-bottom: clamp(12px, 2vw, 24px);
  content-visibility: auto;
  contain-intrinsic-size: 900px;
}
.mix-card-grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(14px, 1.6vw, 24px);
}
@media (max-width: 1500px) { .mix-card-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 1020px) { .mix-card-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 760px)  { .mix-card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 540px)  { .mix-card-grid { grid-template-columns: 1fr; } }

/* OVERLAY CARD — the artwork IS the card.
   No panel, no border, no body block underneath: the image fills the tile and
   every piece of metadata sits on top of it. Two consequences drive the rest
   of this section:
   1. Everything here sits ON ARTWORK, never on a themed surface, so the card
      re-declares its ink locally (the same trick the dark island uses, and it
      needs the same `color` line — tokens alone don't reach elements that only
      inherit). It reads identically in both themes, because the ground is the
      photograph, not the page.
   2. The cover is wrapped in the play anchor, so the overlay must not swallow
      clicks — .mix-card-body is pointer-events:none and hands them back on its
      own interactive children. */
.mix-card {
  background: transparent;
  border: none;
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card-rest);
  overflow: hidden;
  position: relative;
  isolation: isolate;
  color: var(--text-primary);
  --text-primary: #f6f3ee;
  --text-secondary: rgba(246, 243, 238, 0.84);
  --text-muted: rgba(246, 243, 238, 0.6);
  --accent: #470f0f;
  --accent-bright: #ffffff;
  --hairline: rgba(255, 255, 255, 0.22);
  /* An EDGE, not a background. On paper the resting shadow separates the tile,
     but in dark the scrimmed bottom of the image is within a few percent of the
     page and the card dissolves into it — a 1px inset ring is the least that
     restores the boundary without reintroducing a card surface. */
  box-shadow: var(--shadow-card-rest), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
  /* 240ms, not 400: a hover is watched at its very first frame, and the lift
     read as lag at 400. Both hover rules are gated on a real pointer — on a
     touch screen `:hover` fires on tap and sticks there until the next tap
     elsewhere, so the tile stayed lifted and zoomed after every press. */
  transition: transform 0.24s var(--ease-out-expo), box-shadow 0.24s var(--ease-out-expo);
}
@media (hover: hover) and (pointer: fine) {
  .mix-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-card-hover), inset 0 0 0 1px rgba(255, 255, 255, 0.16);
  }
}
.mix-card-cover {
  position: relative;
  /* taller than the old 1/0.86: the metadata now lives inside this box, so the
     lower third is type rather than picture */
  aspect-ratio: 1 / 1.22;
  background: var(--bg-panel);
  overflow: hidden;
  border-radius: var(--radius-card);
}
/* HOMEPAGE LATEST MIXES: every tile is a perfect square, the picture filling
   it edge to edge (object-fit: cover, below) with the overlay on top. Scoped
   to the homepage grid so other .mix-card uses keep their own proportions. */
#homepage-selectors-grid .mix-card-cover { aspect-ratio: 1 / 1; }

/* Each selector card wears the hero card's look: the soft grey hairline, the
   plain drop shadow (--shadow-frame-soft) and the hero's warm scrim (--bg-panel tones, the
   same two gradients as the hero's .hero-image-clip::after) instead of the
   neutral black cover scrim — so the tiles read as small copies of the stage. */
#homepage-selectors-grid .selector-tile {
  border: 1px solid var(--frame-border-soft);
  border-radius: var(--radius-frame);
  box-shadow: var(--shadow-frame-soft);
}
#homepage-selectors-grid .selector-tile .mix-card-cover::after {
  background:
    linear-gradient(180deg, transparent 8%, color-mix(in srgb, var(--bg-panel) 55%, transparent) 42%, color-mix(in srgb, var(--bg-panel) 94%, transparent) 88%),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-panel) 72%, transparent) 0%, transparent 64%);
}

/* Tile images fade in once they have actually loaded (they are lazy, so they
   arrive while you scroll). The homepage's inline script sets data-fade="on"
   on the grid and .is-loaded on each image; without JS neither exists and the
   images simply show. */
#homepage-selectors-grid[data-fade="on"] .mix-card-cover img:not(.cover-fallback) {
  opacity: 0;
  transition: opacity 0.8s ease, transform 0.42s var(--ease-out-expo);
}
#homepage-selectors-grid[data-fade="on"] .mix-card-cover img.is-loaded { opacity: 1; }

/* Selector tiles: bare icons for play / download / share — no ring, no fill.
   (The share button is added by js/ue-links.js as .ue-links-share-btn.)
   Colour still carries hover/active; everything else keeps its circles. */
#homepage-selectors-grid .mix-card-actions .play-circle,
#homepage-selectors-grid .mix-card-actions .download-btn,
#homepage-selectors-grid .mix-card-actions .ue-links-share-btn {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
}
#homepage-selectors-grid .mix-card-actions .play-circle:hover,
#homepage-selectors-grid .mix-card-actions .download-btn:hover,
#homepage-selectors-grid .mix-card-actions .ue-links-share-btn:hover {
  border-color: transparent;
  background: transparent;
  color: var(--accent-bright);
}
.mix-card-cover img, .mix-card-cover .cover-fallback {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--img-filter);
  transition: transform 0.42s var(--ease-out-expo);
}
@media (hover: hover) and (pointer: fine) {
  .mix-card:hover .mix-card-cover img { transform: scale(1.04); }
}
.mix-card-cover::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--cover-scrim);
  pointer-events: none;
}
.mix-card-number {
  position: absolute;
  top: 10px;
  left: 12px;
  z-index: 2;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--text-primary);
  text-shadow: var(--text-shadow-number);
}
/* the metadata plate, laid over the lower part of the artwork */
.mix-card-body {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: 14px 14px 15px;
  display: flex;
  flex-direction: column;
  /* stretch, not flex-start: the title is the full width of the tile now */
  gap: 12px;
  /* the cover's play anchor sits underneath — let clicks through, and hand
     them back only on the things that are themselves interactive */
  pointer-events: none;
}
.mix-card-body a,
.mix-card-body button { pointer-events: auto; }
/* The title owns its own line, at full card width, with the controls beneath
   it. It used to share a flex row with the buttons, which cost it roughly a
   third of the tile and clamped most real titles to "Love Mani…" on a phone.
   Nothing sits beside it now, so the clamp is a safety net for genuinely long
   titles rather than the normal case. */
.mix-card-title {
  font-size: clamp(19px, 1.6vw, 26px);
  font-weight: 700;
  line-height: 1.15;
  color: var(--text-primary);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
  overflow-wrap: anywhere;
  text-wrap: balance;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.mix-card-title a { transition: color 0.16s ease; }
/* The artist, small, under the title. This line and the big handwritten mark
   across the artwork are two answers to the same question, so the card carries
   one of them at a time — see the cover-script section below for how to swap
   back. Mono/uppercase rather than the old 14px body text: at this size it
   reads as a credit line beside the number, not as a second title. */
.mix-card-artist {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-bright);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
  transition: color 0.16s ease;
}
.mix-card-artist:hover { color: var(--text-primary); }
.mix-card.is-unplayable .play-circle { opacity: 0.3; pointer-events: none; }

/* download buttons (blob download of the mix audio) */
.download-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: 1.5px solid var(--hairline);
  color: var(--text-muted);
  flex: none;
  transition: color 0.3s var(--ease-out-expo), border-color 0.3s var(--ease-out-expo), background 0.3s var(--ease-out-expo), transform 0.3s var(--ease-out-expo);
}
.download-btn:active { transform: scale(0.94); transition-duration: 0.1s; }
.download-btn .icon-svg { width: 16px; height: 16px; }
.download-btn:hover {
  color: var(--on-accent);
  border-color: var(--accent-bright);
  background: var(--accent-veil-strong);
  transform: scale(1.04);
}
.download-btn--sm { width: 34px; height: 34px; }
.download-btn--sm .icon-svg { width: 14px; height: 14px; }
.download-btn.is-loading {
  pointer-events: none;
  color: var(--accent-bright);
  border-color: var(--accent-bright);
  animation: dl-pulse 1s ease-in-out infinite;
}
@keyframes dl-pulse { 50% { opacity: 0.35; } }
/* was a vertical stack, which left a dead column of empty card beside the
   title; on the overlay card the controls read as one row of affordances */
.mix-card-actions {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  flex: none;
}
.catalog-row-actions { display: flex; align-items: center; gap: 10px; }
#featured-download-button { width: 46px; height: 46px; }

/* placeholder cover when an artist/mix has no image — always shown whole,
   never cropped, regardless of the container's own object-fit: cover rule */
.cover-fallback {
  display: block;
  object-fit: contain !important;
  background: var(--bg-panel);
  padding: 18%;
  filter: none !important;
}

/* ==========================================================================
   The handwritten artist mark (mix thumbnails + resident portraits)
   --------------------------------------------------------------------------
   The artist's name in the script face, laid across the middle of the artwork
   and sized to span the tile — short names huge, long ones stepped down, two
   words stacked on two lines. The markup comes from `coverScriptHtml()` in
   lib/WebsitePageGenerator.cjs, which carries the rest of the sizing contract.

   The whole mechanism is one line of maths: the tile is an inline-size
   container, each line carries its own natural width in em as `--script-w`,
   and the font-size is the tile's width divided by that width. Nothing here
   depends on the character count or on a per-name breakpoint.
   ========================================================================== */
.mix-card-cover,
.resident-card-photo { container-type: inline-size; }

.cover-script {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* the cover is wrapped in the play anchor — the mark must never eat a click */
  pointer-events: none;
  font-family: var(--font-script);
  /* ON-ARTWORK ISLAND: the same trick as .mix-card and the light theme's dark
     island. The ground here is the photograph, not the page, so the ink is
     declared on this subtree and the lines inherit it — which is what makes the
     mark read identically in both themes instead of flipping to paper ink over
     a near-black skyline. The `color` line is required, not decorative: tokens
     alone do not reach an element that only inherits its colour. */
  --text-primary: #f6f3ee;
  --accent-bright: #ffffff;
  color: var(--text-primary);
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.5), 0 0 3px rgba(0, 0, 0, 0.4);
  /* the same signature tilt the hero's artist line wears */
  transform: rotate(-2deg);
  transition: color 0.4s var(--ease-out-expo);
}
.mix-card:hover .cover-script,
.resident-card:hover .cover-script { color: var(--accent-bright); }

.cover-script-line {
  display: block;
  /* Two limits, whichever is smaller:
     WIDTH  — --script-w is the line's natural width in em (generator-estimated,
              then refined to the measured value by partials/footer), so
              `100cqi * fit / --script-w` always spans the same fraction of the
              tile whatever the name's length. This is the one that governs.
     HEIGHT — --script-h is the tile's CLEAR height (the part with no metadata
              on it), also expressed in cqi so it tracks the fixed aspect ratio.
              Split between the stacked lines it stops a two- or three-line name
              from growing down into the type below it.
     A one-word name is therefore as big as the tile is wide; a stacked one is
     as big as the clear space allows. */
  font-size: min(
    100cqi * var(--script-fit, 0.82) / var(--script-w, 5),
    var(--script-h, 40cqi) / var(--script-lines, 1)
  );
  line-height: 0.92;
  white-space: nowrap;
}

/* The mix card's metadata is laid over the lower ~60% of its own cover, so the
   mark takes the picture above it: centred there by the padding, bounded by
   --script-h. The resident portrait carries its name in a body block BELOW the
   photo, so the whole photo is clear and the mark gets almost all of it. */
/* The mark is centred in the CLEAR part of the tile — the artwork above the
   plate — so both numbers below have to track the plate's real height.

   ⚠️ That height is very nearly constant in PIXELS (two fixed paddings, one
   title line, one 12px gap, one 34px control row) while the tile itself scales
   with the column. A percentage is the wrong unit twice over: `padding-bottom`
   resolves percentages against the WIDTH, and the width is exactly what the
   plate does not follow. At six columns 46% happened to land near the plate; in
   the one-column phone layout the same 46% was 183px against a ~97px plate and
   pushed the mark 43px too high, leaving a hole above the title.

   So state the plate in the units it is actually made of. 95px = 14 top pad +
   12 gap + 12 gap + 42 control row + 15 bottom pad; then the title's own clamp
   times its 1.15 line-height, and the artist line (13px at 1.2). The control
   row is 42 because that is .play-circle--sm — the download and share buttons
   beside it are smaller (34, and 30 below 480px) and never set the row's
   height. Measured, not assumed: an earlier pass guessed 34 and left the mark
   sitting 4px low at every width. */
.mix-card { --mix-plate-h: calc(95px + 1.15 * clamp(19px, 1.6vw, 26px) + 1.2 * 13px); }

/* ⚠️ THE MARK IS OFF ON MIX CARDS — paused, not deleted.
   The card now credits its artist with the small .mix-card-artist line under
   the title instead. Everything the mark needs is still here and still live on
   the residents page (.resident-card-photo .cover-script, below).

   TO BRING IT BACK:
     1. un-comment the rule immediately below;
     2. lib/WebsitePageGenerator.cjs — re-add `coverScriptHtml(entry.artistName)`
        to mixCardHtml, after the cover anchor's imageHtml(...) (residentCardHtml
        still calls it, so copy that line);
     3. decide whether .mix-card-artist stays. The mark and the link say the
        same thing, and carrying both is the layout the mark was removed from.
   The --mix-plate-h maths above is kept current for exactly this reason: the
   mark is centred in the artwork ABOVE the plate, so it needs the plate's real
   height the moment it returns.

.mix-card-cover .cover-script {
  padding-bottom: var(--mix-plate-h);
  --script-h: calc(122cqi - var(--mix-plate-h) - 36px);
}
*/
.resident-card-photo .cover-script { --script-h: 74cqi; }

/* No container queries (pre-2023 engines): the fit maths cannot run, and a mark
   at an arbitrary size is worse than none, so the tile renders as it did. */
@supports not (font-size: 1cqi) {
  .cover-script { display: none; }
}

/* ==========================================================================
   Catalog rows (Archive page / artist pages)
   ========================================================================== */
.catalog-list {
  display: flex;
  flex-direction: column;
  content-visibility: auto;
  contain-intrinsic-size: 1200px;
}
.catalog-row {
  display: grid;
  grid-template-columns: 58px 64px minmax(0, 1.7fr) minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(10px, 2vw, 22px);
  padding: 12px 6px;
  border-bottom: 1px solid var(--hairline);
  transition: background 0.2s ease;
}
.catalog-row:hover { background: var(--veil-hover); }
/* artist pages: the play button leads the row (generator catalogRowHtml
   playFirst) — one extra column at the far left */
.catalog-row--play-first { grid-template-columns: 42px 58px 64px minmax(0, 1.7fr) minmax(0, 1fr) auto; }
.catalog-row-lead { display: flex; align-items: center; justify-content: center; }
.catalog-row-number {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--accent-bright);
}
.catalog-row-cover {
  width: 64px;
  height: 64px;
  border-radius: 0;
  overflow: hidden;
  background: var(--bg-panel);
  flex: none;
}
.catalog-row-cover img, .catalog-row-cover .cover-fallback { width: 100%; height: 100%; object-fit: cover; font-size: 20px; }
.catalog-row-main { min-width: 0; }
.catalog-row-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.catalog-row-title a:hover { color: var(--accent-bright); }
.catalog-row-artist { font-size: 13.5px; color: var(--accent-bright); margin-top: 2px; display: inline-block; }
.catalog-row-artist:hover { color: var(--text-primary); }
.catalog-row-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  font-size: 12.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.catalog-row.is-unplayable .play-circle { opacity: 0.3; pointer-events: none; }
.unplayable-note {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  border: 1px solid var(--hairline);
  border-radius: 3px;
  padding: 2px 6px;
}
@media (max-width: 700px) {
  /* narrow number column + stacked action buttons free ~80px for the
     title column so mix titles stop breaking mid-word */
  .catalog-row { grid-template-columns: 28px 52px minmax(0, 1fr) auto; gap: 10px 12px; }
  .catalog-row--play-first { grid-template-columns: 36px 28px 52px minmax(0, 1fr) auto; }
  .catalog-row--play-first .catalog-row-lead .play-circle { width: 36px; height: 36px; }
  .catalog-row-number { font-size: 13px; }
  .catalog-row-cover { width: 52px; height: 52px; }
  .catalog-row-meta-cell { display: none; }
  .catalog-row-meta--inline { margin-top: 4px; }
  .catalog-row-actions { flex-direction: column; gap: 8px; }
}
@media (min-width: 701px) {
  .catalog-row-meta--inline { display: none; }
}

/* ==========================================================================
   Residents grid
   ========================================================================== */
#residents-grid {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(14px, 1.8vw, 26px);
}
@media (max-width: 1400px) { #residents-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 1020px) { #residents-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 640px)  { #residents-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.resident-card {
  background: var(--bg-card);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card-rest);
  overflow: hidden;
  transition: transform 0.4s var(--ease-out-expo), border-color 0.4s var(--ease-out-expo), box-shadow 0.4s var(--ease-out-expo);
}
.resident-card:hover {
  transform: translateY(-4px);
  border-color: var(--hairline-accent);
  box-shadow: var(--shadow-card-hover);
}
.resident-card-photo {
  aspect-ratio: 1 / 1.05;
  background: var(--bg-panel);
  position: relative;
  overflow: hidden;
}
.resident-card-photo img, .resident-card-photo .cover-fallback {
  width: 100%; height: 100%; object-fit: cover;
  filter: var(--img-filter);
  transition: transform 0.6s var(--ease-out-expo);
}
.resident-card:hover .resident-card-photo img { transform: scale(1.04); }
.resident-card-photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--portrait-scrim);
  pointer-events: none;
}
.resident-card-body { padding: 14px 14px 16px; }
.resident-card-name { font-size: 16px; font-weight: 700; letter-spacing: 0.04em; }
.resident-card-sub {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  margin-top: 6px;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.resident-card-sub .resident-genre { color: var(--accent-bright); }

/* ==========================================================================
   Page headers (Residents / Archive / About / artist detail)
   ========================================================================== */
.page-title-block { padding: clamp(28px, 4vw, 52px) 0 clamp(22px, 3vw, 38px); }
.page-title-block h1 {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(30px, 4vw, 52px);
  line-height: 1.08;
  margin-top: 14px;
}
.page-title-sub { color: var(--text-secondary); margin-top: 10px; max-width: 640px; font-size: 14.5px; }

/* Artist detail header */
#artist-detail-header {
  display: grid;
  grid-template-columns: minmax(240px, 380px) 1fr;
  gap: clamp(24px, 3.5vw, 56px);
  align-items: center;
  padding: clamp(28px, 4vw, 52px) 0 clamp(24px, 3vw, 40px);
}
#artist-detail-portrait {
  position: relative;
  border-radius: var(--radius-frame);
  border: 1px solid var(--hairline-accent);
  box-shadow: var(--shadow-portrait);
}
#artist-detail-portrait .hero-image-clip {
  position: relative;
  border-radius: var(--radius-frame);
  overflow: hidden;
  aspect-ratio: 1 / 1.05;
  background: var(--bg-panel);
}
#artist-detail-portrait img:not(#artist-detail-portrait-logo), #artist-detail-portrait .cover-fallback {
  width: 100%; height: 100%; object-fit: cover;
  filter: var(--img-filter-hero);
}
/* Centred and sized to fit WHOLE. This sits inside .hero-image-clip, which is
   overflow:hidden, so the old `left:-4%; width:108%` bled 4% past each edge and
   the clip ate it — measured 16.1px off the left, which cropped the leading "U"
   of UNDERGROUND on every artist page.
   The mark is 1773×198 (8.95:1), and rotating it 4° widens its footprint to
   W·cos4° + (W/8.95)·sin4° ≈ W·1.005 — so 92% leaves ~14px of air on each side
   at the 378px rendered size and the full lockup always lands inside. */
#artist-detail-portrait-logo {
  position: absolute;
  top: 8%;
  left: 50%;
  width: 92%;
  height: auto;
  transform: translateX(-50%) rotate(4deg);
  filter: var(--shadow-logo);
  pointer-events: none;
  z-index: 2;
}
#artist-detail-name {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(34px, 4.4vw, 60px);
  line-height: 1.06;
  margin: 16px 0 4px;
}
#artist-detail-signature {
  font-family: var(--font-script);
  font-size: clamp(34px, 3.6vw, 50px);
  color: var(--accent-bright);
  display: inline-block;
  transform: rotate(-2deg);
  margin-bottom: 10px;
}
#artist-detail-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  font-size: 13.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
@media (max-width: 860px) {
  #artist-detail-header { grid-template-columns: 1fr; }
  #artist-detail-portrait { max-width: 420px; }
}

/* ==========================================================================
   About page
   ========================================================================== */
#about-content-panel {
  position: relative;
  isolation: isolate;
  max-width: 1040px;
  margin: 0 auto;
  padding: clamp(28px, 4vw, 56px) 0 clamp(40px, 5vw, 64px);
}
/* Street backdrop: full-bleed behind the hero. The radial mask is both the
   vignette and the spotlight: fully lit at the top right, fading to nothing
   toward the left and bottom, so it never needs a theme-specific colour. */
#about-hero-backdrop {
  --about-hero-opacity: 0.62;
  position: absolute;
  top: 0;
  left: 50%;
  width: 100vw;
  margin-left: -50vw;
  height: min(86vh, 760px);
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  -webkit-mask-image: radial-gradient(ellipse 62% 96% at 90% 2%, #000 0%, rgba(0,0,0,0.62) 30%, rgba(0,0,0,0.2) 55%, transparent 78%);
  mask-image: radial-gradient(ellipse 62% 96% at 90% 2%, #000 0%, rgba(0,0,0,0.62) 30%, rgba(0,0,0,0.2) 55%, transparent 78%);
}
:root[data-theme="light"] #about-hero-backdrop { --about-hero-opacity: 0.34; }
#about-hero-backdrop-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 100% 0%;
  opacity: var(--about-hero-opacity);
  filter: grayscale(0.45) contrast(1.05);
}
#about-lead-block { max-width: 860px; }
#about-content-panel .about-lead {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(24px, 3.4vw, 40px);
  line-height: 1.25;
  color: var(--text-primary);
  text-wrap: balance;
}
#about-content-panel #about-signature {
  margin: 18px 0 0;
  max-width: none;
  font-family: var(--font-script);
  font-size: clamp(56px, 8vw, 96px);
  line-height: 0.9;
  color: var(--accent-bright);
}
#about-receipt-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 28px;
  align-items: center;
  margin: clamp(28px, 4vw, 48px) 0;
  padding: 16px 20px;
  background: var(--paper);
  color: var(--paper-ink);
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  clip-path: polygon(0 8%, 3% 0, 8% 5%, 14% 0, 21% 7%, 29% 2%, 37% 7%, 46% 0, 54% 5%, 62% 0, 70% 7%, 78% 2%, 86% 7%, 93% 0, 100% 5%, 100% 92%, 96% 100%, 89% 95%, 81% 100%, 72% 93%, 63% 100%, 54% 95%, 45% 100%, 36% 93%, 28% 100%, 20% 95%, 12% 100%, 5% 93%, 0 100%);
}
#about-receipt-strip .receipt-strip-script { font-family: var(--font-script); font-size: 22px; text-transform: none; letter-spacing: 0; }
#about-story-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(32px, 5vw, 72px) clamp(32px, 5vw, 72px);
}
#about-story-columns section { min-width: 0; }
.about-h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(20px, 2.2vw, 26px);
  line-height: 1.2;
  color: var(--text-primary);
  margin-bottom: 14px;
  text-wrap: balance;
}
#about-content-panel p {
  color: var(--text-secondary);
  margin-bottom: 16px;
  font-size: 15.5px;
  line-height: 1.7;
  max-width: 62ch;
  text-wrap: pretty;
}
#about-content-panel em { color: var(--text-primary); font-style: italic; }
#about-tapes-list {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
  border-top: 1px solid var(--hairline);
}
#about-tapes-list li {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 14px;
  align-items: baseline;
  padding: 11px 0;
  border-bottom: 1px solid var(--hairline);
  font-size: 15px;
  color: var(--text-secondary);
}
#about-tapes-list .tape-artist { color: var(--text-primary); min-width: 8.5em; }
#about-actions-section {
  margin-top: clamp(40px, 6vw, 72px);
  padding-top: clamp(24px, 3vw, 36px);
  border-top: 1px solid var(--hairline-accent);
}
#about-actions-row { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 8px; }
#about-play-cta,
#about-contribute-cta {
  display: inline-flex;
  align-items: center;
  padding: 13px 26px;
  border: 2px solid var(--accent-bright);
  border-radius: 999px;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.08em;
  transition: background 0.2s ease, color 0.2s ease, transform 0.16s ease-out;
}
#about-play-cta { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
#about-contribute-cta { color: var(--accent-bright); }
#about-play-cta:active,
#about-contribute-cta:active { transform: scale(0.97); }
@media (hover: hover) and (pointer: fine) {
  #about-play-cta:hover { background: var(--accent-bright); border-color: var(--accent-bright); }
  #about-contribute-cta:hover { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
}
@media (max-width: 760px) {
  #about-story-columns { grid-template-columns: 1fr; }
  #about-tapes-list .tape-artist { min-width: 0; flex-basis: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  #about-play-cta, #about-contribute-cta { transition: none; }
}

#about-flyers-section { padding: 0 0 clamp(48px, 6vw, 80px); }
#about-flyers-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: clamp(14px, 1.8vw, 24px);
  margin-top: 20px;
}
.flyer-grid-item {
  width: 100%;
  aspect-ratio: 604 / 775;
  object-fit: cover;
  border-radius: var(--radius-card);
  border: 1px solid var(--hairline);
  background: var(--bg-panel);
  transition: transform 0.4s var(--ease-out-expo), border-color 0.4s var(--ease-out-expo);
}
.flyer-grid-item:hover { transform: translateY(-4px); border-color: var(--hairline-accent); }

/* ==========================================================================
   Footer
   ========================================================================== */
#site-footer {
  border-top: 1px solid var(--hairline-accent);
  margin-top: clamp(30px, 5vw, 64px);
  padding: 30px 0 40px;
}
#site-footer-inner {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 0 var(--page-pad);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px 32px;
}
/* The footer's barcode lockup was removed; the tagline is the brand block's
   only child now, so it carries no top offset of its own. */
.footer-tagline {
  font-size: 12.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.footer-links { display: flex; flex-wrap: wrap; gap: 10px 22px; }
.footer-links a {
  font-size: 12.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.footer-links a:hover { color: var(--accent-bright); }
@media (max-width: 760px) {
  #site-footer { padding: 36px 0 44px; }
  #site-footer-inner { flex-direction: column; gap: 32px; }
  #footer-brand-block { display: flex; flex-direction: column; align-items: center; width: 100%; }
  .footer-links { width: 100%; justify-content: center; gap: 12px 22px; }
}

/* ==========================================================================
   Persistent bottom player bar
   ========================================================================== */
#persistent-player-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 1000;
  height: var(--player-bar-height);
  background: var(--scrim-player);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid var(--hairline-accent);
  box-shadow: inset 0 1px 0 var(--veil-inset-strong);
  display: grid;
  grid-template-columns: minmax(180px, 320px) auto minmax(200px, 1fr) auto auto;
  align-items: center;
  gap: clamp(12px, 2vw, 30px);
  padding: 0 var(--page-pad);
}
#player-bar-now-playing { display: flex; align-items: center; gap: 12px; min-width: 0; }
#player-bar-thumb {
  width: 52px; height: 52px;
  border-radius: 0;
  overflow: hidden;
  background: var(--bg-panel);
  border: 1px solid var(--hairline);
  flex: none;
}
#player-bar-thumb img, #player-bar-thumb .cover-fallback { width: 100%; height: 100%; object-fit: cover; font-size: 16px; }
#player-bar-track-meta { min-width: 0; }
#player-bar-title {
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#player-bar-artist {
  font-size: 13px;
  /* exempt, same as #featured-artist-signature: this is the same mix title,
     just repeated in the bar */
  color: #e01e2b;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#player-bar-transport { display: flex; align-items: center; gap: clamp(10px, 1.4vw, 20px); }
.transport-btn {
  color: var(--text-secondary);
  display: inline-flex;
  padding: 6px;
  transition: color 0.2s ease, transform 0.2s var(--ease-out-expo);
}
.transport-btn:hover { color: var(--text-primary); }
.transport-btn:active { transform: scale(0.9); transition-duration: 0.1s; }
.transport-btn .icon-svg { width: 17px; height: 17px; }
.transport-btn.is-on { color: var(--accent-bright); }
/* Rectangle, solid colour, simple icon, white border — the first obvious play
   cue on the page (the hero carries none), styled to echo the badge border on
   the site's own logo mark rather than a quiet ghost circle.
   Hardcoded, not var(--accent-bright): this is one of the two elements
   exempt from the site-wide accent-to-white recolor (the other is the mix
   title), so it must not move when that token changes. */
#player-bar-play {
  width: 144px;
  height: 46px;
  border-radius: 0;
  background: #470f0f;
  border: 1px solid var(--on-accent);
  color: var(--on-accent);
}
#player-bar-wave-wrap { display: flex; align-items: center; gap: 14px; min-width: 0; }
#player-bar-waveform { flex: 1; min-width: 0; height: 36px; position: relative; cursor: pointer; touch-action: none; }
#player-bar-waveform canvas { width: 100%; height: 100%; display: block; }
.player-bar-time { font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.05em; white-space: nowrap; }
/* fixed width so "--:--" -> "1:00:00" never nudges the waveform */
.player-bar-time { min-width: 7ch; font-variant-numeric: tabular-nums; }
.player-bar-time.time-current { text-align: right; }

/* Bar boot fade (partials/player-bar.html script): the parts the player fills
   in — waveform, what's playing, the film/cover/artist chips — stay invisible
   until it has, then fade in left to right. Transport and volume are static,
   so they show from the start. */
#persistent-player-bar[data-bar-boot] #player-bar-wave-wrap,
#persistent-player-bar[data-bar-boot] #player-bar-now-playing,
#persistent-player-bar[data-bar-boot] #player-bar-source-slot {
  transition: opacity 0.6s ease;
}
#persistent-player-bar[data-bar-boot] #player-bar-now-playing { transition-delay: 0.1s; }
#persistent-player-bar[data-bar-boot] #player-bar-source-slot { transition-delay: 0.2s; }
#persistent-player-bar[data-bar-boot="pending"] #player-bar-wave-wrap,
#persistent-player-bar[data-bar-boot="pending"] #player-bar-now-playing,
#persistent-player-bar[data-bar-boot="pending"] #player-bar-source-slot { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  #persistent-player-bar[data-bar-boot] #player-bar-wave-wrap,
  #persistent-player-bar[data-bar-boot] #player-bar-now-playing,
  #persistent-player-bar[data-bar-boot] #player-bar-source-slot { transition-duration: 0.2s; transition-delay: 0s; }
}
.player-bar-time.time-current { color: var(--accent-bright); }
.player-bar-time.time-total { color: var(--text-muted); }
#player-bar-volume { display: flex; align-items: center; gap: 10px; }
#player-bar-volume .icon-svg { width: 18px; height: 18px; color: var(--text-secondary); }
#volume-slider {
  -webkit-appearance: none;
  appearance: none;
  width: clamp(70px, 8vw, 120px);
  height: 3px;
  border-radius: 2px;
  background: var(--track-empty-strong);
  outline: none;
}
#volume-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--accent-bright);
  cursor: pointer;
}
#volume-slider::-moz-range-thumb {
  width: 12px; height: 12px;
  border: none;
  border-radius: 50%;
  background: var(--accent-bright);
  cursor: pointer;
}
#player-bar-menu-btn { color: var(--text-secondary); display: inline-flex; padding: 6px; }
#player-bar-menu-btn:hover { color: var(--text-primary); }
#persistent-player-bar.is-idle #player-bar-title { color: var(--text-muted); font-weight: 500; }

/* --- primed state -------------------------------------------------------
   The bar carries the "resume at 0:52" / "shared link from 1:23" offer that
   used to be a floating slab over the grid. Nothing is added to the page: the
   bar is already here, and its idle slot ("Select a mix to play") is replaced
   by the queued track until the visitor takes it or plays something else.
   The whole affordance is one muted line plus a slow ring on the play button —
   an indicator, not an announcement. */
.player-bar-primed-label {
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
/* Primed state no longer touches #player-bar-play at all — it used to get a
   pulsing ring + border-colour swap, removed because the button must never
   animate or change its border. The muted .player-bar-primed-label line above
   is still the cue for "resume at 0:52" / a shared link. */

/* breathing live-dot while audio is playing */
#player-bar-title { position: relative; }
#persistent-player-bar.is-playing #player-bar-title::before {
  content: '';
  display: inline-block;
  width: 7px; height: 7px;
  margin-right: 8px;
  border-radius: 50%;
  background: var(--accent-bright);
  vertical-align: 1px;
  animation: live-dot-breathe 1.8s ease-in-out infinite;
}
@keyframes live-dot-breathe {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.35; transform: scale(0.75); }
}

@media (max-width: 900px) {
  #persistent-player-bar {
    /* columns: now-playing | play btn | waveform | volume | menu.
       Play button + icons are auto (never crushed); now-playing and
       waveform are the flexible columns and clamp/truncate — the old
       auto-sized wave column let the play button overflow onto it */
    grid-template-columns: minmax(0, 1.1fr) auto minmax(0, 1fr) auto auto;
    height: 68px;
    gap: 12px;
  }
  #player-bar-transport .transport-btn { display: none; }
  #player-bar-transport #player-bar-play { display: inline-flex; width: 128px; height: 42px; border-radius: 0; }
  #player-bar-wave-wrap .player-bar-time { display: none; }
  #player-bar-waveform { height: 30px; }
  #player-bar-volume #volume-slider { display: none; }
  #player-bar-thumb { width: 44px; height: 44px; }
  body { padding-bottom: 92px; }

  /* Idle-only full-width PLAY CTA: before a track is chosen there is no other
     play affordance on the page, so the bar collapses to one obvious red
     rectangle spanning the full width instead of sharing the row. Any track
     load removes .is-idle (player.js setBarTrack), restoring the normal
     compact control row above. */
  #persistent-player-bar.is-idle { padding: 0; }
  #persistent-player-bar.is-idle #player-bar-wave-wrap,
  #persistent-player-bar.is-idle #player-bar-now-playing,
  #persistent-player-bar.is-idle #player-bar-source-slot,
  #persistent-player-bar.is-idle #player-bar-volume,
  #persistent-player-bar.is-idle #player-bar-menu-btn {
    display: none;
  }
  #persistent-player-bar.is-idle #player-bar-transport { flex: 1 1 100%; }
  #persistent-player-bar.is-idle #player-bar-play {
    width: 100%;
    height: 100%;
    border-radius: 0;
  }
  #persistent-player-bar.is-idle #player-bar-play .icon-svg { width: 22px; height: 22px; }
}

/* Bar order: play + waveform on the LEFT, then what's playing (thumbnail +
   title) and the film / cover / artist chips on the RIGHT, then volume + menu.
   Flex rather than the grid above, so the chip slot (homepage only) simply
   takes no room on every other page. The markup order is unchanged; `order`
   does the rearranging. */
#persistent-player-bar { display: flex; }
#player-bar-transport   { order: 1; flex: none; }
#player-bar-transport #player-bar-play { order: -1; } /* play leads, far left */
#player-bar-wave-wrap   { order: 2; flex: 1 1 auto; min-width: 120px; }
#player-bar-now-playing { order: 3; flex: 0 1 300px; min-width: 0; }
#player-bar-source-slot { order: 4; flex: none; display: flex; align-items: center; }
#player-bar-source-slot:empty { display: none; }
#player-bar-volume      { order: 5; flex: none; }
#player-bar-menu-btn    { order: 6; flex: none; }
@media (max-width: 900px) {
  #player-bar-wave-wrap   { min-width: 60px; }
  #player-bar-now-playing { flex-basis: 220px; }
}
@media (max-width: 540px) {
  /* phones: the title block gives way first, the chips stay tappable */
  #player-bar-now-playing { flex-basis: 0; flex-grow: 0.6; }
  #player-bar-volume { display: none; }
}

/* ==========================================================================
   Search overlay
   ========================================================================== */
#search-overlay {
  position: fixed;
  inset: 0;
  z-index: 1500;
  background: var(--scrim-search);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: none;
  overflow-y: auto;
  padding: 10vh var(--page-pad) 15vh;
}
#search-overlay.is-open { display: block; }
#search-overlay-inner { max-width: 760px; margin: 0 auto; }
#search-overlay-close {
  position: absolute;
  top: 26px;
  right: var(--page-pad);
  font-size: 14px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
#search-overlay-close:hover { color: var(--accent-bright); }
#search-input {
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 2px solid var(--accent);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: clamp(20px, 3vw, 30px);
  padding: 12px 2px;
  outline: none;
  letter-spacing: 0.04em;
}
#search-input::placeholder { color: var(--text-muted); }
.search-results-heading {
  font-size: 12.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 30px 0 8px;
}
.search-result-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 11px 4px;
  border-bottom: 1px solid var(--hairline);
  color: var(--text-primary);
  font-size: 15.5px;
}
.search-result-row:hover { color: var(--accent-bright); }
.search-result-row .search-result-sub { color: var(--text-muted); font-size: 13px; }

/* ==========================================================================
   Motion (restrained) — GSAP adds .motion-ready; CSS handles fallback
   ========================================================================== */
.motion-item { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ==========================================================================
   Playback status / retry / error surface (audio streaming remediation)
   DOM for every selector below is created at runtime by player.js — see
   UI-CONTRACT.md for the exact node tree, ids, copy, and JS build recipe.
   Two instances share this block: the persistent player bar
   (#player-bar-status-region) and the featured hero waveform
   (#featured-waveform-status-region). Reuses existing tokens/patterns only
   — no new fonts or colors. The sr-only live announcer nodes reuse
   .visually-hidden (see Reset / base, above); the download action reuses
   .download-btn / .download-btn--sm (see Mix cards, above) unmodified; the
   buffering/seeking/reconnecting dot reuses the live-dot-breathe keyframe
   already defined in the Persistent bottom player bar section, above.
   ========================================================================== */

/* additive position only — neither selector has an existing `position`
   declaration, so this does not alter current layout */
#player-bar-wave-wrap { position: relative; }
#featured-waveform { position: relative; }

.player-status-region {
  display: none;
  position: absolute;
  z-index: 5;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  border-radius: 7px;
  border: 1px solid var(--hairline);
  background: var(--scrim-panel);
  box-shadow: var(--shadow-panel);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
  white-space: nowrap;
  /* informational states never intercept pointer events over the
     waveform/page beneath them — only the terminal failed state (with a
     real Retry/Download action) opts back in, below */
  pointer-events: none;
}
/* TRANSIENT STATES SHOW NO BOX.
   Starting / buffering / seeking / reconnecting used to float a pill — over the
   hero waveform, and over the page above the player bar, where it covered the
   card grid. They are now expressed by the thing they are about: the waveform
   breathes while it waits. Nothing is added to the page, nothing moves, and no
   layout shifts when the state clears.
   This costs screen readers NOTHING: the announcements live in separate
   `aria-live` nodes (#player-bar-status-live, #featured-waveform-status-live)
   that sit OUTSIDE this element, so hiding the pill never touches them.
   Only `.is-failed` — the terminal state with a real decision — still renders,
   and it is backed by the dialog. */
.player-status-region.is-failed {
  display: inline-flex;
}

@keyframes wave-waiting {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.42; }
}
#featured-waveform:has(.player-status-region.is-starting) canvas,
#featured-waveform:has(.player-status-region.is-buffering) canvas,
#featured-waveform:has(.player-status-region.is-seeking) canvas,
#featured-waveform:has(.player-status-region.is-reconnecting) canvas,
#player-bar-wave-wrap:has(.player-status-region.is-starting) canvas,
#player-bar-wave-wrap:has(.player-status-region.is-buffering) canvas,
#player-bar-wave-wrap:has(.player-status-region.is-seeking) canvas,
#player-bar-wave-wrap:has(.player-status-region.is-reconnecting) canvas {
  animation: wave-waiting 1.5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  /* the global reduced-motion rule kills the animation, which would leave no
     cue at all — hold the waveform at the dimmed end instead */
  #featured-waveform:has(.player-status-region.is-starting) canvas,
  #featured-waveform:has(.player-status-region.is-buffering) canvas,
  #featured-waveform:has(.player-status-region.is-seeking) canvas,
  #featured-waveform:has(.player-status-region.is-reconnecting) canvas,
  #player-bar-wave-wrap:has(.player-status-region.is-starting) canvas,
  #player-bar-wave-wrap:has(.player-status-region.is-buffering) canvas,
  #player-bar-wave-wrap:has(.player-status-region.is-seeking) canvas,
  #player-bar-wave-wrap:has(.player-status-region.is-reconnecting) canvas {
    opacity: 0.55;
  }
}
.player-status-region.is-failed {
  border-color: var(--accent-bright);
  background: var(--accent-veil-strong);
  pointer-events: auto;
}

/* player bar: pill floats just above the bar and spans the waveform
   column's own width — never widens the fixed 5-column bar grid, never
   changes the bar's fixed height, no layout shift on any state change */
/* Sits INSIDE the bar's waveform column. It used to be `bottom: calc(100% + 8px)`
   — i.e. floating above the whole player bar, full width, directly over the card
   grid, which is exactly the "extra element parked on the page" pattern being
   removed. Playback has failed at this point, so covering the waveform it
   belongs to costs nothing. */
#player-bar-status-region {
  left: auto;
  right: 6px;
  top: 50%;
  bottom: auto;
  transform: translateY(-50%);
  max-width: calc(100% - 12px);
}

/* hero: compact corner badge inside the waveform's own box — the row's
   play/download buttons are separate flex siblings, untouched */
#featured-waveform-status-region {
  top: 8px;
  right: 8px;
}

.player-status-icon {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-secondary);
  flex: none;
}
.is-buffering .player-status-icon,
.is-seeking .player-status-icon {
  animation: live-dot-breathe 1.8s ease-in-out infinite;
}
.is-reconnecting .player-status-icon {
  background: var(--accent);
  animation: live-dot-breathe 1.1s ease-in-out infinite;
}
.is-failed .player-status-icon {
  /* static, not pulsing — a resolved/terminal state reads calmer as a
     steady mark than as another "in progress" pulse */
  background: var(--accent-bright);
}
.is-failed .player-status-text { color: var(--accent-bright); }

.player-status-actions { display: none; align-items: center; gap: 8px; margin-left: 2px; }
.is-failed .player-status-actions { display: inline-flex; }

.player-status-retry-button {
  border: 1px solid var(--accent-bright);
  border-radius: 999px;
  padding: 3px 11px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-bright);
  background: transparent;
  transition: background 0.2s ease, color 0.2s ease, transform 0.15s var(--ease-out-expo);
}
.player-status-retry-button:hover { background: var(--accent); color: var(--on-accent); }
.player-status-retry-button:active { transform: scale(0.95); }

/* non-seekable waveform affordance — applied by the integrator directly to
   the existing scrubber elements whenever no range-capable source is
   confirmed for the active track (independent of the status pill above) */
#player-bar-waveform.is-seek-disabled,
.waveform-track.is-seek-disabled,
.waveform-progress-line.is-seek-disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

@media (max-width: 900px) {
  .player-status-region {
    font-size: 10px;
    padding: 4px 8px;
    gap: 6px;
  }
  #player-bar-status-region { right: 4px; }
  .player-status-retry-button { padding: 3px 9px; font-size: 10px; }
}

@media (max-width: 540px) {
  /* hero waveform becomes its own full-width row above the play/download
     row at this breakpoint (see Featured hero, above) — corner badge
     still fits, just tightened in a touch */
  #featured-waveform-status-region { top: 6px; right: 6px; }
}

@media (prefers-reduced-motion: reduce) {
  /* belt-and-suspenders: the file's global reduced-motion rule above
     already neutralizes all animation/transition, this makes the intent
     explicit and self-contained for this section specifically */
  .player-status-icon,
  .player-status-retry-button {
    animation: none !important;
    transition: none !important;
  }
}

/* ==========================================================================
   Playback status — cold-start affordance (gateway-warming phase)
   Appended on top of the "Playback status / retry / error surface" block
   above — nothing above this line was changed. Adds ONE new status code,
   `starting` ("Starting mix" copy, `.is-starting` class per
   applySurfaceStatus() in player.js), on the SAME
   #player-bar-status-region / #featured-waveform-status-region surface.
   Exists because a cold gateway can take 2-12s to the first byte (see
   PROBE_TIMEOUT_MS in player.js) — with nothing painted the instant Play
   is pressed, that reads as broken, not merely slow. Same tokens and the
   same pulsing-dot treatment as .is-buffering/.is-seeking above — no new
   colors, no new fonts. The existing @media (max-width: 900px) block and
   the reduced-motion block immediately above both already target the base
   .player-status-region / .player-status-icon selectors, so both apply to
   this state automatically; nothing to add for either.
   ========================================================================== */
/* `starting` is a transient state, so it follows the same rule as
   buffering/seeking/reconnecting: no pill. It is announced to screen readers by
   #player-bar-status-live, and shown visually by the waveform's `wave-waiting`
   breathe (see "Transient states show no box" above). This rule previously set
   `display: inline-flex` and, sitting later in the file, silently won over that
   block — which is why the pill kept appearing after it was supposedly hidden. */
.player-status-region.is-starting {
  display: none;
}

/* ==========================================================================
   Shared dialog — the ONLY interruption left
   Markup is static in templates/partials/modal.html (never injected), so it is
   styled here like everything else. Reserved for decisions the visitor has to
   make: a mix that would not load (retry / download) and a blocked clipboard
   (here is the link, copy it). Transient playback states stay as the inline
   pill on the waveform, and the resume / shared-link offers live in the player
   bar — see "primed state". Nothing else is allowed to interrupt.

   Note: padding lives on #ue-dialog-panel, never on #ue-dialog. The controller
   treats "press and release on the dialog element" as a backdrop click, so
   padding on the dialog would become a dead strip that looks like the panel
   but closes it.
   ========================================================================== */
#ue-dialog {
  padding: 0;
  border: none;
  background: transparent;
  max-width: min(440px, calc(100vw - 32px));
  max-height: calc(100dvh - 32px);
  color: var(--text-primary);
  overflow: visible;
}
#ue-dialog::backdrop {
  background: var(--scrim-backdrop);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
#ue-dialog-panel {
  position: relative;
  background: var(--bg-elevated);
  border: 1px solid var(--hairline-accent);
  border-radius: var(--radius-frame);
  box-shadow: var(--shadow-float);
  padding: clamp(22px, 4vw, 30px);
  font-family: var(--font-body);
}
#ue-dialog-title {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(20px, 3vw, 26px);
  line-height: 1.15;
  color: var(--text-primary);
  padding-right: 28px; /* clear of the ✕ */
  text-wrap: balance;
}
#ue-dialog-message {
  margin-top: 10px;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--text-secondary);
  max-width: 48ch;
}
#ue-dialog-close {
  position: absolute;
  top: 12px;
  right: 14px;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--text-muted);
  transition: color 0.2s ease, background 0.2s ease;
}
#ue-dialog-close:hover { color: var(--text-primary); background: var(--veil-hover); }
#ue-dialog-close:active { transform: scale(0.92); transition-duration: 0.1s; }
.ue-dialog-close-glyph { font-size: 15px; line-height: 1; }

/* the copy fallback's field — monospace, because what it holds is a URL the
   visitor is about to select and copy */
#ue-dialog-copy-input {
  /* deliberately NO `display` here. The partial hides this field with the
     `hidden` attribute, and an id selector setting `display: block`
     out-specifies the UA's `[hidden] { display: none }` — which printed an
     empty input in every dialog that has no copyText. Only the
     [data-copy="on"] rule below is allowed to make it visible. */
  width: 100%;
  margin-top: 18px;
  padding: 11px 12px;
  /* --bg-base, NOT --bg-panel: in light theme --bg-panel is the dark image
     well (#1c1714), which inside a white panel would print dark ink on a dark
     field. --bg-base reads as a recess against --bg-elevated in both themes. */
  background: var(--bg-base);
  border: 1px solid var(--hairline);
  border-radius: 8px;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 12.5px;
  letter-spacing: 0.02em;
}
#ue-dialog[data-copy="on"] #ue-dialog-copy-input { display: block; }

/* image slot — a mix's original artwork (artist page "View original artwork").
   The dialog widens for it; no `display` on the img itself, for the same
   [hidden] reason as the copy field above. */
#ue-dialog[data-image="on"] { max-width: min(720px, calc(100vw - 32px)); }
#ue-dialog[data-image="on"] #ue-dialog-image {
  display: block;
  margin-top: 14px;
  width: 100%;
  height: auto;
  max-height: calc(100dvh - 160px);
  object-fit: contain;
  border-radius: var(--radius-frame);
}

#ue-dialog-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 22px;
}
#ue-dialog[data-actions="0"] #ue-dialog-actions { display: none; margin-top: 0; }
.ue-dialog-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 11px 22px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-family: var(--font-body);
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s var(--ease-out-expo);
}
.ue-dialog-action:active { transform: scale(0.97); transition-duration: 0.1s; }
.ue-dialog-action--primary {
  background: var(--accent);
  border: 1px solid var(--accent);
  color: var(--on-accent);
}
.ue-dialog-action--primary:hover { background: var(--accent-bright); border-color: var(--accent-bright); }
.ue-dialog-action--quiet {
  background: transparent;
  border: 1px solid var(--hairline-accent);
  color: var(--text-secondary);
}
.ue-dialog-action--quiet:hover { color: var(--text-primary); border-color: var(--accent-bright); }

@media (max-width: 540px) {
  /* full-width panel on phones, actions stack and fill so neither is a
     hard-to-hit pill at the bottom of a small screen */
  #ue-dialog { max-width: calc(100vw - 20px); }
  #ue-dialog-actions { flex-direction: column-reverse; align-items: stretch; }
  .ue-dialog-action { width: 100%; }
}

/* ==========================================================================
   Share modal — one share widget for the whole site
   Markup is static in templates/partials/share-modal.html (never injected),
   opened by every share affordance: the icon on a mix card, the icon on a
   catalog row, the hero's share button, the player bar.

   Reading order IS priority order, and the layout enforces it: the film first
   (it is the thing that gets posted), then the two primary actions, then the
   link itself, then the file downloads, and the platform chips last because
   they are the least-used path.

   Same backdrop-click rule as #ue-dialog: padding lives on #ue-share-panel,
   never on #ue-share-dialog.
   ========================================================================== */
#ue-share-dialog {
  padding: 0;
  border: none;
  background: transparent;
  /* Desktop is a real two-column panel (the 720px rule below); the narrow
     single column is the PHONE layout, not the default everywhere. */
  max-width: min(800px, calc(100vw - 48px));
  max-height: calc(100dvh - 24px);
  color: var(--text-primary);
  overflow: visible;
}
#ue-share-dialog::backdrop {
  background: var(--scrim-backdrop);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

/* The controller shows and hides most of this panel's controls with the
   `hidden` attribute (no video, no timestamp, no native share sheet). An
   author `display` rule beats the UA's `[hidden] { display: none }` no matter
   how specific it is — the exact trap #ue-dialog-copy-input documents above —
   so this one rule re-asserts it for the whole subtree, and every rule below
   is then free to set whatever display it needs. */
#ue-share-dialog [hidden] { display: none !important; }

#ue-share-panel {
  position: relative;
  background: var(--bg-elevated);
  border: 1px solid var(--hairline-accent);
  border-radius: var(--radius-frame);
  box-shadow: var(--shadow-float);
  padding: clamp(18px, 4vw, 26px);
  font-family: var(--font-body);
  max-height: calc(100dvh - 24px);
  overflow-y: auto;
}

#ue-share-close {
  position: absolute;
  top: 12px;
  right: 14px;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--text-muted);
  z-index: 2;
  transition: color 0.2s ease, background 0.2s ease;
}
#ue-share-close:hover { color: var(--text-primary); background: var(--veil-hover); }
#ue-share-close:active { transform: scale(0.92); transition-duration: 0.1s; }
.ue-share-close-glyph { font-size: 15px; line-height: 1; }

#ue-share-eyebrow {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding-right: 34px; /* clear of the ✕ */
}

/* The preview. Square because every generated promo is 720x720; the still sits
   underneath, so a refused autoplay, Save-Data or reduced-motion leaves the
   artwork rather than a black box. */
#ue-share-media {
  position: relative;
  width: 100%;
  max-width: 300px;
  margin-top: 14px;
  margin-inline: auto;
  aspect-ratio: 1 / 1;
  max-height: min(42vh, 300px);
  border-radius: 12px;
  overflow: hidden;
  background: var(--bg-base);
  border: 1px solid var(--hairline);
}

#ue-share-art,
#ue-share-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
#ue-share-video { z-index: 1; }
#ue-share-sound {
  position: absolute;
  right: 8px;
  bottom: 8px;
  z-index: 2;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--hairline);
  background: var(--scrim-panel);
  color: var(--text-primary);
}
#ue-share-sound .icon-svg { width: 14px; height: 14px; }
#ue-share-sound .icon-unmuted,
#ue-share-sound[aria-pressed="true"] .icon-muted { display: none; }
#ue-share-sound[aria-pressed="true"] .icon-unmuted { display: inline-block; }

#ue-share-title {
  margin-top: 16px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(18px, 3vw, 22px);
  line-height: 1.15;
  text-wrap: balance;
}
#ue-share-artist {
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* Primary actions. "Share…" is first and filled because on a phone it is the
   one-tap route to Instagram, TikTok and Messages; it is absent on desktop,
   where "Copy link" then takes the lead position on its own. */
#ue-share-primary {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 18px;
}
.ue-share-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 1 1 auto;
  padding: 11px 18px;
  border-radius: 999px;
  border: 1px solid var(--hairline-accent);
  background: transparent;
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s var(--ease-out-expo);
}
.ue-share-action:hover { color: var(--text-primary); border-color: var(--accent-bright); }
.ue-share-action:active { transform: scale(0.97); transition-duration: 0.1s; }
.ue-share-action--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.ue-share-action--primary:hover {
  background: var(--accent-bright);
  border-color: var(--accent-bright);
  color: var(--on-accent);
}
.ue-share-action.is-loading { opacity: 0.6; pointer-events: none; }

/* Which address gets shared: the permanent Arweave one, or the host the
   listener is on. A two-option switch rather than a checkbox, because neither
   is a modifier of the other — they are two different links. */
#ue-share-host {
  display: inline-flex;
  gap: 2px;
  margin-top: 14px;
  padding: 2px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--bg-base);
}
.ue-share-host-opt {
  padding: 7px 16px;
  border-radius: 999px;
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.2s ease, background 0.2s ease;
}
.ue-share-host-opt:hover { color: var(--text-primary); }
.ue-share-host-opt[aria-pressed="true"] {
  background: var(--accent-veil-strong);
  color: var(--text-primary);
  box-shadow: inset 0 0 0 1px var(--hairline-accent);
}

#ue-share-note {
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

/* The link, always visible. It says what is about to be sent, and it is the
   recovery path when the clipboard is blocked — nothing to open, just select. */
#ue-share-url {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 10px 12px;
  background: var(--bg-base);
  border: 1px solid var(--hairline);
  border-radius: 8px;
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
}

#ue-share-at {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-top: 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
  cursor: pointer;
}
#ue-share-at-input { accent-color: var(--accent-bright); width: 15px; height: 15px; }
#ue-share-at-time { color: var(--text-primary); }

#ue-share-files {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}

#ue-share-hint {
  margin-top: 10px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-secondary);
}

/* Platform chips, deliberately last and deliberately quiet: they convert far
   worse than the share sheet, and on a phone they are the wrong answer. */
#ue-share-targets {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--hairline);
}
.ue-share-target {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 9px 6px;
  border-radius: 8px;
  border: 1px solid var(--hairline);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.ue-share-target:hover {
  color: var(--text-primary);
  border-color: var(--accent-bright);
  background: var(--accent-veil);
}

#ue-share-status {
  margin-top: 12px;
  min-height: 1em;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

/* Two columns from 720px: the film on the left at a size worth looking at,
   every control on the right. Below that the grid collapses to one column and
   the media re-centres — the phone layout the panel started as.

   This block sits AFTER every base rule above on purpose: the overrides here
   are same-specificity id selectors, so source order is the only thing that
   decides them. Moved earlier in the file, the single-column rules would
   silently win at desktop width. */
@media (min-width: 720px) {
  #ue-share-panel {
    display: grid;
    grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
    gap: clamp(22px, 3vw, 34px);
    align-items: start;
    padding: clamp(26px, 3vw, 34px);
  }
  #ue-share-media {
    margin-top: 0;
    max-width: none;
    max-height: none;
  }
  #ue-share-body { min-width: 0; }
  #ue-share-eyebrow { padding-right: 34px; }
  #ue-share-title { margin-top: 8px; font-size: clamp(22px, 2.2vw, 28px); }
  #ue-share-primary .ue-share-action { flex: 0 1 auto; min-width: 148px; }
  /* the platform chips stay three-up: six across this column would shrink
     TELEGRAM and WHATSAPP past the point of reading */
}

@media (max-width: 540px) {
  #ue-share-dialog { max-width: calc(100vw - 16px); }
  #ue-share-media { max-width: 240px; max-height: 34vh; }
  /* the two primaries fill the width and stack: a phone share is a thumb
     reaching the bottom of the panel, not a pair of half-width pills */
  #ue-share-primary { flex-direction: column; }
  .ue-share-action { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .ue-share-action,
  .ue-share-target,
  #ue-share-close { transition: none; }
  .ue-share-action:active { transform: none; }
}

/* ==========================================================================
   Light theme — warm ink on paper, same brand red, opt-in
   Activated by `<html data-theme="light">`, set pre-paint by the inline
   script in templates/partials/head.html from localStorage['ue-theme'].
   Absence of the attribute is dark, so no-JS and first-visit both get the
   brand default and there is no flash.

   This is the same identity re-grounded on paper, NOT a monochrome inversion
   of it. Dark is three things — near-black, warm ink, oxblood — and light is
   the same three: four paper tones carrying dark's own surface relationship
   (wells recess below the page, cards rise above it), warm-biased inks, and
   the identical brand red as the highlight. What inverts is the DIRECTION of
   emphasis, not the palette: on black, emphasis is lighter; on paper, deeper.
   Imagery alone stays greyscale, so the red is the only chroma on the page and
   the skyline thumbnails (already monochrome, treatment v2) read as one
   material with it; the thumbnails' own near-black mass is the point here — on
   paper they finally have somewhere to sit, which they never did on #0a0606.

   The CRT texture is keyed off the thumbnails' own treatment spec
   (config/skyline-thumbnail-treatment.json: scanlinePitch 8, scanlineDepth
   0.38, grainStrength 12, contrast 1.3) so page texture and image texture
   agree instead of competing. Page pitch is 3px, not 8: the thumbnail's 8px
   pitch is at source resolution and lands near 2-3 CSS px once a 1200px-wide
   image is drawn into a ~300px card.
   ========================================================================== */
:root[data-theme="light"] {
  /* THE TONAL LADDER — five steps, not four whites.
       #ffffff  elevated   popovers, the highest plane
       #f8f6f4  card       raised above the page
       #e8e4e0  page       the ground
       #1c1714  well       image wells — actual holes, not grey squares
       #0a0606  chrome     header / player bar / overlay (see the dark island;
                           these are dark theme's own chrome values verbatim)
     Two earlier passes failed here: all four surfaces within 3% of each other,
     so nothing could sit on top of anything. Depth on paper comes from having
     genuinely dark values in the palette, not from more shades of off-white.
     The image wells going dark is the load-bearing move — the skyline
     thumbnails are near-black, so a dark well is the same material as the
     photograph instead of a light frame fighting it. */
  --bg-base: #e8e4e0;
  --bg-panel: #1c1714;
  --bg-card: #f8f6f4;
  --bg-elevated: #ffffff;

  /* THE RED, AND WHERE IT IS ALLOWED TO LIVE.
     Red over paper is the pink problem: every low-alpha red wash (a 7% veil, a
     13% hover, a 16% ring, a 12% hero gradient) composites toward pink on a
     light ground, and enough of them together tint the whole page. So in this
     theme red NEVER touches paper as a wash. It appears two ways only:
       1. at full strength on the DARK chrome, where it is simply red (see the
          dark island block below — those surfaces use the brand values);
       2. as deep oxblood TYPE on paper, dark enough to read as ink-with-blood
          rather than as a pink accent.
     Every wash token below is therefore neutral ink, not red. */
  --frame-border-soft: rgba(0, 0, 0, 0.1);
  --shadow-frame-soft: 0 14px 34px rgba(30, 20, 14, 0.18);
  /* NOT whitened like the dark-chrome islands: this is on-paper text, and
     white-on-paper would be invisible. Kept at the oxblood this site already
     uses for "deep oxblood TYPE on paper" (see comment above). */
  --accent: #470f0f;
  --accent-bright: #470f0f;
  --accent-dim: #7b6a64;
  --on-accent: #f6f3f0;
  /* the about-page receipt strip inverts with everything else: a near-black
     ticket on paper, rather than paper-on-paper, which would vanish */
  --paper: #1a1310;
  --paper-ink: #ece6df;
  /* inks carry the same warm bias dark's do (#f2ece4 / #b7aaa3 / #8a7d77) —
     pure neutral greys on pure neutral paper were half the flatness */
  --text-primary: #14100e;
  --text-secondary: #4a423e;
  --text-muted: #655c57;
  --hairline: rgba(22, 17, 14, 0.2);
  --hairline-accent: rgba(22, 17, 14, 0.5);

  /* CHROME IS DARK IN BOTH THEMES. The header, player bar and search overlay
     stay near-black on the paper page — which is where most of the new depth
     comes from, and what lets the brand marks and the red keep their native
     colours instead of being inverted or greyscaled into something else. */
  --scrim-header: rgba(10, 6, 6, 0.94);
  --scrim-player: rgba(12, 7, 7, 0.96);
  --scrim-search: rgba(8, 5, 5, 0.97);
  --scrim-panel: rgba(248, 246, 244, 0.96);
  /* the dialog dims paper toward the page's own brown-black, not toward grey */
  --scrim-backdrop: rgba(22, 17, 14, 0.5);

  --veil-hover: rgba(22, 17, 14, 0.05);
  --veil-inset: rgba(255, 255, 255, 0.06);
  --veil-inset-strong: rgba(255, 255, 255, 0.07);
  --track-empty: rgba(22, 17, 14, 0.2);
  --track-empty-strong: rgba(22, 17, 14, 0.24);

  /* neutral ink washes — see the note above on why these are not red */
  --accent-veil: rgba(22, 17, 14, 0.05);
  --accent-veil-strong: rgba(22, 17, 14, 0.1);
  --accent-glow: rgba(22, 17, 14, 0.3);
  --accent-glow-soft: rgba(22, 17, 14, 0.16);

  /* Shadows carry the page's own brown-
     black (22,17,14), never pure black, and every one is a CONTACT shadow plus
     a soft one — the 1-2px contact layer is what makes an element sit on the
     paper rather than float above a grey cloud. The hover ring is ink now, not
     red: a 16% red ring on white was one of the pink sources. */
  --shadow-card-rest: 0 1px 2px rgba(22, 17, 14, 0.07), 0 8px 20px rgba(22, 17, 14, 0.08);
  --shadow-card-hover: 0 2px 6px rgba(22, 17, 14, 0.1), 0 18px 38px rgba(22, 17, 14, 0.18), 0 0 0 1px rgba(22, 17, 14, 0.14);
  --shadow-frame: 0 0 0 1px rgba(22, 17, 14, 0.1), 0 2px 8px rgba(22, 17, 14, 0.1), 0 26px 56px rgba(22, 17, 14, 0.2);
  --shadow-portrait: 0 2px 8px rgba(22, 17, 14, 0.1), 0 20px 48px rgba(22, 17, 14, 0.21);
  --shadow-panel: 0 1px 2px rgba(22, 17, 14, 0.1), 0 10px 24px rgba(22, 17, 14, 0.16);
  --shadow-float: 0 2px 6px rgba(22, 17, 14, 0.11), 0 18px 40px rgba(22, 17, 14, 0.2);
  --shadow-logo: drop-shadow(0 3px 8px rgba(22, 17, 14, 0.32));
  --text-shadow-title: none;

  /* artwork: greyscale + a contrast lift. The mix thumbnails measure ~56/255
     mean luminance by design; against paper that mass is the composition, so
     it is lifted rather than scrimmed. */
  --img-filter: grayscale(1) contrast(1.14);
  --img-filter-hero: grayscale(1) contrast(1.08);
  /* 0.6, against dark's 0.32: the mix number printed over this is forced white
     (it sits on artwork), and this scrim is the only thing guaranteeing it a
     ground when a thumbnail is missing or still loading. */
  /* the card's ink is light in BOTH themes (it sits on artwork, not on paper),
     so this scrim stays dark here too — see the overlay-card note */
  --cover-scrim: linear-gradient(180deg, rgba(12, 9, 8, 0.5) 0%, transparent 22%, transparent 50%, rgba(8, 6, 5, 0.86) 100%);
  /* no red gradient here — dark can wash its hero in 14% red because the
     result stays dark; over a light-keyed image the same wash reads pink */
  --hero-scrim: linear-gradient(0deg, rgba(12, 9, 8, 0.34), transparent 46%);
  --portrait-scrim: linear-gradient(180deg, transparent 50%, rgba(12, 9, 8, 0.55));

  /* dark draws the waveform as bright cream on near-black (0.82/0.55 alpha);
     the mirror on paper needs comparable ink weight or the scrubber reads as
     an empty box — measured on the featured hero at 1440px, 0.28/0.14 was
     invisible past arm's length */
  /* these are the HERO waveform's values, on paper. The player-bar waveform
     sits on the dark chrome and gets the brand values from the dark-island
     block below — player.js resolves these per canvas, not off :root. */
  --wave-played: #470f0f;
  --wave-unplayed: rgba(20, 15, 13, 0.6);
  --wave-remaining: rgba(20, 15, 13, 0.5);

  --grain-opacity: 0.11;
  --grain-blend: multiply;
  --scanline-opacity: 0.07;
}

/* --- the dark island ----------------------------------------------------
   The chrome stays dark in light theme, so it re-declares the colour tokens on
   its own subtree. Because every rule in this stylesheet reads tokens rather
   than literals, that single block flips the header, mobile menu, player bar
   and search overlay wholesale — text, icons, hairlines, waveforms, hover
   washes — with no per-rule overrides. These are dark's own values, which is
   the point: on these surfaces the brand red is simply red, the white wordmark
   needs no inversion, and nothing composites toward pink.
   (Custom properties inherit, so a canvas inside the player bar resolves the
   --wave-* values from here — player.js reads the canvas, not :root.) */
:root[data-theme="light"] #site-header,
:root[data-theme="light"] #mobile-menu,
:root[data-theme="light"] #persistent-player-bar,
:root[data-theme="light"] #search-overlay,
:root[data-theme="light"] #featured-mix-hero {
  /* Re-declaring the tokens is not enough on its own. `color` INHERITS, and
     elements that never declare their own (e.g. #player-bar-title) inherit the
     value computed on <body> — outside this island — so the token said
     #f2ece4 while the text painted in paper ink on the dark bar. Setting
     `color` here gives the island its own inheritance root. Any future
     re-grounded region needs this line as well as the tokens. */
  color: var(--text-primary);
  --text-primary: #f2ece4;
  --text-secondary: #b7aaa3;
  --text-muted: #8a7d77;
  --accent: #470f0f;
  --accent-bright: #ffffff;
  --on-accent: #ffffff;
  --hairline: rgba(255, 255, 255, 0.1);
  --hairline-accent: rgba(255, 255, 255, 0.45);
  --veil-hover: rgba(255, 255, 255, 0.04);
  --track-empty: rgba(255, 255, 255, 0.18);
  --track-empty-strong: rgba(255, 255, 255, 0.22);
  --accent-veil: rgba(255, 255, 255, 0.08);
  --accent-veil-strong: rgba(255, 255, 255, 0.14);
  --accent-glow: rgba(255, 255, 255, 0.7);
  --bg-elevated: #1c1212;
  --wave-played: #ffffff;
  --wave-unplayed: rgba(242, 236, 228, 0.82);
  --wave-remaining: rgba(154, 143, 138, 0.55);
}

/* The paper itself is lit, not a flat fill: light falls from the top and the
   ground deepens toward the bottom edge, so the page has a direction before
   any element is drawn on it. Two stops, ~4% total range — at the point where
   you can consciously see it as a gradient it has gone too far. Fixed
   attachment so the lighting belongs to the viewport, not to the scroll
   position, which is what stops it from banding as you scroll a long catalog. */
:root[data-theme="light"] body {
  background-image:
    radial-gradient(115% 70% at 50% 0%, rgba(255, 255, 255, 0.55), transparent 62%),
    linear-gradient(180deg, transparent 55%, rgba(22, 17, 14, 0.05));
  background-attachment: fixed;
}

/* --- light-theme structural corrections ---------------------------------
   Rules whose dark-theme value is not a token, or whose token flips the
   wrong way because the element sits on artwork rather than on the page. */

/* mix number and any cover-borne label sit ON a near-black thumbnail in BOTH
   themes, so they stay paper-white — following --text-primary here would
   print black-on-black */
:root[data-theme="light"] .mix-card-number { color: #ffffff; }

/* The UE placeholder mark keeps its oxblood in both themes — it was greyscaled
   while this theme had no hue at all, and now that red IS the highlight, a
   missing cover showing the red mark is correct rather than a leak. */

/* The HEADER wordmark now needs no filter at all — it is a white-on-transparent
   PNG sitting on the dark header, exactly as in dark theme, red badge intact.
   Removing that filter also removed a pink source: invert() sends the badge's
   red to cyan, and the hue-rotate(180deg) that brought it back landed on a
   dusty rose, not the brand red.
   The footer lockup used to be the one mark left on paper, and the only one
   that inverted; it has since been removed from the footer, so no mark does.
   NOT #artist-detail-portrait-logo: that lockup is laid OVER the portrait
   photograph, not over paper, so it stays white in both themes for the same
   reason .mix-card-number does — inverting it prints dark type on a dark
   image. Same trap, different element. */
:root[data-theme="light"] #artist-detail-portrait-logo { filter: var(--shadow-logo); }

/* The download button is the one place --on-accent met a WASH rather than a
   fill: paper-coloured icon on a 10% ink veil is 1.17:1 — the icon, which is
   the button's entire content, disappeared on hover. Fill it like
   .play-circle:hover already does (16.85:1), which is also this theme's own
   language: active = a solid black block with paper type. */
:root[data-theme="light"] .download-btn:hover { background: var(--accent); }

/* the playing indicator pulses red in BOTH themes now — it inherits
   --accent-bright and needs no light-specific rule */

/* Theme flip: a short crossfade on the large surfaces only, so switching
   reads as the room's lights changing rather than as a page reload. Deliberately
   NOT a global `* { transition: background-color }` — that would also re-time
   every hover state, which already owns its own curve. Colour properties only:
   nothing here animates layout. The file's global prefers-reduced-motion rule
   already neutralises all of it with !important, so a reduced-motion visitor
   gets the instant cut. */
body,
#site-header,
#persistent-player-bar,
#search-overlay,
.mix-card,
.resident-card,
.catalog-row,
.play-circle,
.download-btn {
  transition-property: background-color, border-color, color, box-shadow;
  transition-duration: 0.22s;
  transition-timing-function: var(--ease-out-expo);
}
/* the two texture layers carry their own, slightly slower fade so the CRT
   ruling arrives just after the ground has changed, not with it */
body::before, body::after {
  transition: opacity 0.32s var(--ease-out-expo);
}
/* the hover/press transitions declared on these elements earlier in the file
   are longer and more specific in intent; re-assert them so the theme-flip
   timing above cannot flatten them */
.mix-card, .resident-card {
  transition-property: transform, border-color, box-shadow, background-color;
  transition-duration: 0.4s;
}
.play-circle, .download-btn {
  transition-property: background-color, color, border-color, transform;
  transition-duration: 0.3s;
}

@media (prefers-reduced-motion: reduce) {
  /* the scanline layer is static, so it stays; nothing here animates */
  :root[data-theme="light"] body::before { opacity: var(--scanline-opacity); }
}

@media (prefers-reduced-motion: reduce) {
  #player-bar-source-slot .hero-thumb:not([hidden]) { animation: none; }
  #homepage-selectors-grid[data-fade="on"] .mix-card-cover img:not(.cover-fallback) { transition: none; }
}
