:root {
  /* Native chrome — the per-track <audio controls> on release pages,
     scrollbars, form controls — follows this, not --bg. Without it the
     browser renders them to the OS preference, so a light-mode visitor got
     pale grey players sitting on the near-black dark skin. Declared on both
     skins so it tracks the toggle rather than the system. */
  color-scheme: light;

  --accent: #e83d0f;
  /* Small-text accent. Every rule using it sets a text colour, never a border
     or fill, so it can be darkened for contrast without touching the brand
     orange. On the cream --bg the brand #e83d0f is 3.30:1 — fine for large
     text, short of AA at the 0.72-0.85rem this is used at. #b8300b is 4.86:1.
     The dark theme keeps the exact brand colour; it already passes there. */
  --accent-2: #b8300b;
  --ink: #191713;
  --ink-soft: #66625a;   /* 4.87:1 on --bg; was #6f6b60 at 4.26:1 */
  --bg: #e9e6de;
  --line: #cec9bc;
  --card: #fff;
  --radius: 4px;
  /* Mobile menu scrim. Same colour as --bg but with alpha, which a hex var
     can't supply — the blur behind it needs something to see through. */
  --overlay: rgba(233, 230, 222, 0.82);

  /* Layout. --main-pad is .site-main's horizontal padding; .contact-stage-inner
     cancels it with negative margins, so the two must move together.
     --hero-col is the home rotator's grid track, shared by .home-hero and
     .hero-nowcap so the name caption stays aligned under the image. */
  --main-pad: clamp(1.25rem, 3vw, 2.5rem);
  --main-max: clamp(1100px, 85vw, 1500px);
  --hero-col: clamp(300px, 34vw, 560px);
  --spk-size: 125vmin;   /* speaker-grill backdrop; see body.page-home::after */
  /* Light theme: the accent stays genuinely orange over cream. */
  --spk-color: var(--accent);
  --spk-opacity: 0.10;
}

/* Dark theme — Direction B "After Hours" palette. This is the default:
   <html> ships with data-theme="dark". Light is opt-in via the toggle
   (data-theme="light"), which just falls back to the :root light vars. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --accent: #e83d0f;
  --accent-2: #e83d0f;
  --ink: #f4efe6;
  --ink-soft: #928d81;
  --bg: #100e0c;
  --line: #2a2621;
  --card: #1a1714;
  --overlay: rgba(16, 14, 12, 0.82);
  /* Dark theme: low-alpha accent over near-black browns out before it becomes
     visible, so there is no opacity that is both subtle and still orange —
     use ink instead. */
  --spk-color: var(--ink);
  --spk-opacity: 0.07;
}

* { box-sizing: border-box; }
html { transition: background-color 0.25s ease; background: var(--bg); }

/* ===== Speaker-grill backdrop =====
   Fixed to the viewport and centred on the bottom-left corner, so roughly a
   quarter of the grill rises out of it.

   Masked rather than used as a background-image: the source SVG is a flat
   #263238 that would all but vanish on the dark theme, and a mask lets
   background-color supply the colour instead — hence --spk-color, which each
   theme block sets to what actually works there.

   Rotation comes from --spk-rot, written on scroll in base.html.

   Home and the legal pages only (Terms, Privacy Policy, What we do with your
   data — all three carry .page-legal). Elsewhere it competed with the page's
   own content — the editorial backdrops on About/Publishing most of all — so
   those pages sit on a plain --bg. The legal pages are the exception because
   each is a long column of small legal type with nothing else on it, and the
   grill is the only thing giving them any weight. --spk-color stays defined
   for all themes because the Publishing backdrop below borrows it for its
   fill. */
body.page-home::after,
body.page-legal::after {
  content: "";
  position: fixed;
  z-index: -1;
  pointer-events: none;
  width: var(--spk-size);
  height: var(--spk-size);
  left: calc(var(--spk-size) / -2);
  bottom: calc(var(--spk-size) / -2);
  background: var(--spk-color);
  opacity: var(--spk-opacity);
  -webkit-mask-image: url("/assets/vectors/concentric-circles.svg");
  mask-image: url("/assets/vectors/concentric-circles.svg");
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  transform: rotate(var(--spk-rot, 0deg));
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  body.page-home::after,
  body.page-legal::after { transform: none; }
}
body {
  transition: color 0.25s ease;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

html, body {
  margin: 0;
  padding: 0;
  /* Painted on html only — the speaker-grill layer below sits at z-index -1,
     and an opaque background on body would cover it. */
  color: var(--ink);
  font-family: "DM Sans", system-ui, -apple-system, sans-serif;
  font-size: 17px;
  line-height: 1.55;
}

.mono { font-family: "DM Mono", ui-monospace, monospace; }

a { color: var(--ink); text-decoration: none; }
a:hover { color: var(--accent-2); }

/* Global focus ring. Only the theme switch and (on mobile) the nav links had
   one; everything else fell back to the UA default, which the colours here all
   but erase. Keyed to :focus-visible so it shows for keyboard users without
   ringing every mouse click. Individual components may still override it —
   .theme-toggle rounds its own. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
/* <main> only takes focus as the skip link's target; a ring there is noise. */
.site-main:focus,
.site-main:focus-visible { outline: none; }

/* Off-screen until focused, then pinned to the top-left over the header. */
.skip-link {
  position: absolute;
  left: 0.5rem;
  top: -100%;
  z-index: 100;
  padding: 0.6rem 1rem;
  background: var(--accent);
  color: #fff;
  border-radius: var(--radius);
  font-size: 0.85rem;
  font-weight: 500;
}
.skip-link:focus { top: 0.5rem; color: #fff; }

h1 { font-size: clamp(2.2rem, 4.6vw, 3.2rem); line-height: 1.0; margin: 0.15em 0 0.4em; letter-spacing: -0.03em; font-weight: 800; }
h2 {
  font-family: "DM Mono", ui-monospace, monospace;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-soft);
  font-weight: 500;
  margin: 0 0 1rem;
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.78rem;
  color: var(--accent-2);
  margin: 0 0 0.5rem;
}

/* Header */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.2rem 2rem;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
  position: sticky;
  top: 0;
  z-index: 10;
  transition: background-color 0.25s ease, border-color 0.25s ease;
}
.site-header-right { display: flex; align-items: center; gap: 1.4rem; }
/* Theme switch — NIGHT (o——) DAY. The track takes --ink and the knob --bg, so
   the pair inverts with the theme on its own: a cream track holding a near-black
   knob in the dark, exactly the reverse in the light. No per-theme override
   beyond the knob's travel. Knob left = night, knob right = day. */
.theme-toggle {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--ink);
  cursor: pointer;
  /* Adds to .site-header-right's 1.4rem for 3.4rem clear of the nav. The links
     are 1.6rem apart, so anything at or under that read as one more nav item;
     it takes roughly double their spacing to separate into its own group. */
  margin-left: 2rem;
}
.theme-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 999px;
}
.theme-toggle-label {
  font-size: 0.56rem;
  font-weight: 400;
  /* A touch more tracking than the mockup's bold cut: at 400 the letterforms
     lose the weight that was holding them apart at this size. */
  letter-spacing: 0.11em;
  text-transform: uppercase;
  line-height: 1;
  transition: color 0.2s ease;
}
/* Hover lights up the side you'd land on, so the control says where it goes. */
.theme-toggle:hover .theme-toggle-label--day { color: var(--accent-2); }
:root[data-theme="light"] .theme-toggle:hover .theme-toggle-label--day { color: inherit; }
:root[data-theme="light"] .theme-toggle:hover .theme-toggle-label--night { color: var(--accent-2); }
.theme-track {
  position: relative;
  flex: none;
  width: 52px;
  height: 24px;
  border-radius: 999px;
  background: var(--ink);
  transition: background-color 0.25s ease;
}
.theme-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--bg);
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.25s ease;
}
/* 52 − 20 − 2 − 2 = 28px of travel, landing on the mirrored inset. */
:root[data-theme="light"] .theme-knob { transform: translateX(28px); }
@media (prefers-reduced-motion: reduce) {
  .theme-knob { transition: background-color 0.25s ease; }
}
/* Only genuinely narrow phones drop the words — the 900px nav breakpoint is not
   the constraint, since collapsing the nav to a burger frees header space. Below
   this the brand and burger want it back, and the knob's position still carries
   the state on its own. */
@media (max-width: 460px) {
  .theme-toggle-label { display: none; }
}
/* Logo variants: light default, dark swap (double class to beat .brand-logo) */
.brand-logo.brand-logo--dark { display: none; }
:root[data-theme="dark"] .brand-logo.brand-logo--light { display: none; }
:root[data-theme="dark"] .brand-logo.brand-logo--dark { display: block; }
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.brand-mark {
  display: inline-block;
  background: var(--ink);
  color: var(--bg);
  padding: 0.2rem 0.45rem;
  border-radius: 2px;
  font-family: "DM Mono", monospace;
  font-weight: 500;
  font-size: 0.82rem;
}
.brand-text { font-size: 1.05rem; }
.brand-logo { display: block; height: 45px; width: auto; }
.site-nav a {
  margin-left: 1.6rem;
  /* Matches the mobile overlay: DM Sans 800, sentence case. Was DM Mono in
     caps at 0.08em tracking — that tracking existed for the caps, and reads as
     gaps between lowercase letters, so it's pulled back to near zero. */
  font-family: "DM Sans", sans-serif;
  font-weight: 800;
  font-size: 0.9rem;
  letter-spacing: 0.01em;
  color: var(--ink-soft);
}
/* Three states: --ink-soft at rest, --ink on hover, --accent for the page you
   are on. Hover is --ink rather than a literal white so it stays legible on
   the cream skin, where it resolves to near-black. The on-state is the only
   thing wearing the accent, which is what makes it read as a location. */
.site-nav a:hover { color: var(--ink); }
.site-nav a.is-current { color: var(--accent-2); }

/* ===== Mobile menu =====
   Breaks at 900px rather than the 760px used for layout. The nav is ~600px
   wide with four links and each new section adds ~90-100px, so 900px keeps
   the hamburger ahead of the links wrapping up to about seven of them. */
.nav-toggle { display: none; }

@media (max-width: 900px) {
  .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    appearance: none;
    -webkit-appearance: none;
    background: none;
    border: 1px solid var(--line);
    border-radius: 999px;
    color: var(--ink-soft);
    cursor: pointer;
    position: relative;
    z-index: 11;                 /* above the overlay, with the theme toggle */
    transition: border-color 0.15s ease, color 0.15s ease;
  }
  .nav-toggle:hover { border-color: var(--accent); color: var(--accent); }
  /* .site-nav is a descendant of .site-header, so the header's z-index: 10
     stacking context contains both — the overlay competes with the header's
     own children, not with the header. Lift the brand and theme toggle above
     it explicitly, or the overlay paints over them. The scrim then covers the
     header strip too, which is what keeps the blur running edge to edge. */
  .brand,
  .theme-toggle { position: relative; z-index: 11; }
  /* No nav to separate from once it's behind the burger — drop the margin so
     the switch and the burger keep the header-right gap between them. */
  .theme-toggle { margin-left: 0; }
  /* One .bar element plus its two pseudo-elements = three lines that fold
     into an X; the middle one goes transparent rather than scaling away so
     the rotation stays centred. */
  .nav-toggle .bar,
  .nav-toggle .bar::before,
  .nav-toggle .bar::after {
    display: block;
    width: 15px;
    height: 1.5px;
    background: currentColor;
    transition: transform 0.25s ease, background-color 0.2s ease;
  }
  .nav-toggle .bar { position: relative; }
  .nav-toggle .bar::before,
  .nav-toggle .bar::after { content: ""; position: absolute; left: 0; }
  .nav-toggle .bar::before { top: -5px; }
  .nav-toggle .bar::after { top: 5px; }
  [data-nav-open] .nav-toggle .bar { background: transparent; }
  [data-nav-open] .nav-toggle .bar::before { transform: translateY(5px) rotate(45deg); }
  [data-nav-open] .nav-toggle .bar::after { transform: translateY(-5px) rotate(-45deg); }

  .site-nav {
    position: fixed;
    inset: 0;
    z-index: 9;                  /* under the sticky header (z-index 10), so
                                    brand and theme toggle stay live */
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 6rem var(--main-pad) 3rem;   /* 6rem clears the ~85px header */
    background: var(--overlay);
    -webkit-backdrop-filter: blur(20px);
    backdrop-filter: blur(20px);
    opacity: 0;
    visibility: hidden;
    /* Delay visibility until the fade finishes so links aren't tabbable while
       invisible; on open it switches immediately. */
    transition: opacity 0.28s ease, visibility 0s linear 0.28s;
  }
  [data-nav-open] .site-nav {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.28s ease, visibility 0s;
  }
  .site-nav a {
    margin: 0;
    font-family: "DM Sans", sans-serif;
    font-weight: 800;
    font-size: clamp(1.9rem, 8vw, 2.8rem);
    letter-spacing: -0.03em;
    text-transform: none;
    color: var(--ink);
    /* No dividers — spacing alone separates the items, so the padding does the
       work the rules used to. */
    padding: 0.65rem 0;
    text-align: center;
  }
  .site-nav a:hover,
  .site-nav a:focus-visible { color: var(--accent-2); }

  html[data-nav-open],
  html[data-nav-open] body { overflow: hidden; }
}

@media (prefers-reduced-motion: reduce) {
  .site-nav,
  .nav-toggle .bar,
  .nav-toggle .bar::before,
  .nav-toggle .bar::after { transition: none; }
}

.site-main {
  width: 100%;
  /* Holds at 1100px up to a ~1294px viewport (unchanged on laptops), then
     tracks 85vw, then caps at 1500. A bare 85vw would shrink mid-size screens.
     Shared with .footer-inner via --main-max so the two stay aligned. */
  max-width: var(--main-max);
  margin: 0 auto;
  padding: 2.5rem var(--main-pad) 4rem;
  border-left: 1px solid var(--line);
  border-right: 1px solid var(--line);
  flex: 1 0 auto;
}

.site-footer {
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 0.85rem;
}
.footer-inner {
  width: 100%;
  max-width: var(--main-max);
  margin: 0 auto;
  padding: 1.4rem var(--main-pad);
  display: flex;
  align-items: start;
  gap: 1.6rem;
  flex-wrap: wrap;
}
.footer-left { display: flex; flex-direction: column; gap: 0.75rem; }
/* Footer links read as navigation, so they take the header nav's DM Sans 800
   rather than the mono used for the copyright line below them. */
.footer-nav { display: flex; flex-direction: column; align-items: flex-start; gap: 0.3rem; }
.footer-nav a {
  font-family: "DM Sans", sans-serif;
  font-weight: 800;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
  color: var(--ink-soft);
}
.footer-nav a:hover { color: var(--accent-2); }
.footer-copy { font-size: 0.76rem; }
.footer-socials { display: flex; align-items: center; gap: 1rem; }
/* Above the phone breakpoint the icons sit against the right edge, copyright
   left. On phones the auto margin is dropped so the copyright wraps first and
   the icons stay left-aligned on their own row beneath it. */
@media (min-width: 761px) {
  .footer-socials { margin-left: auto; }
}
.footer-social { display: flex; align-items: center; color: var(--ink-soft); }
.footer-social svg { width: 1.15rem; height: 1.15rem; display: block; }
.footer-social:hover { color: var(--accent); }

/* Hero / page heads */
.hero { padding: 2rem 0 3rem; max-width: 700px; }
.hero .lede { font-size: 1.2rem; color: var(--ink-soft); margin: 0 0 1.6rem; }
.hero-cta { display: flex; gap: 0.6rem; flex-wrap: wrap; }

.page-head { margin-bottom: 2rem; }

.btn {
  display: inline-block;
  padding: 0.7rem 1.2rem;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  font-weight: 500;
}
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { background: #c8330d; border-color: #c8330d; color: #fff; }
.btn:hover { color: var(--accent); border-color: var(--accent); }

/* Blocks */
.block { margin: 2.6rem 0 0; border-top: 1px solid var(--line); padding-top: 1.4rem; }
.block-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 1rem;
}

/* Grids */
.grid { display: grid; gap: 1rem; }
.grid-artists { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
.grid-releases { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }

.release-card img,
.release-card .photo-placeholder {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  display: block;
}
.release-card-body {
  padding: 0.7rem 0.8rem 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.release-card-body .catalog { color: var(--accent-2); font-size: 0.8rem; }
.release-card-body .release-title { font-weight: 500; line-height: 1.25; }
.release-card-body .release-artist { color: var(--ink-soft); font-size: 0.9rem; }

.card { display: block; }
/* Artist index tiles — ledger: grayscale, colorize on hover */
.artist-card img,
.artist-card .photo-placeholder {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: 3px;
  filter: grayscale(1) contrast(1.02);
  transition: filter 0.3s ease;
}
.artist-card:hover img { filter: none; }
.photo-placeholder {
  width: 100%;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--line);
  font-family: "DM Mono", monospace;
  font-size: 2.5rem;
  color: var(--ink-soft);
}
.card-label {
  display: block;
  padding: 0.55rem 0 0;
  font-weight: 700;
  letter-spacing: -0.01em;
  font-size: 0.95rem;
}

/* Release list */
.release-list { list-style: none; padding: 0; margin: 0; border-top: 1px solid var(--line); }
.release-list li { border-bottom: 1px solid var(--line); }
.release-list a {
  display: grid;
  grid-template-columns: 120px 1fr 1fr auto;
  gap: 1rem;
  padding: 0.9rem 0;
  align-items: baseline;
}
.release-list .catalog { color: var(--accent-2); font-size: 0.9rem; }
.release-list .release-title { font-weight: 500; }
.release-list .release-artist { color: var(--ink-soft); }
.release-list .release-date { color: var(--ink-soft); font-size: 0.85rem; }

@media (max-width: 700px) {
  .release-list a { grid-template-columns: 90px 1fr; }
  .release-list .release-artist, .release-list .release-date { grid-column: 2; }
}

/* Artist detail */
.artist-hero {
  display: grid;
  grid-template-columns: minmax(220px, 320px) 1fr;
  gap: 2rem;
  align-items: end;
  margin-bottom: 2rem;
}
.artist-photo {
  width: 100%;
  border-radius: var(--radius);
  display: block;
}
.prose p { max-width: 70ch; color: var(--ink); margin: 0 0 1rem; }

@media (max-width: 700px) {
  .artist-hero { grid-template-columns: 1fr; }
  h1 { font-size: 2rem; }
}

/* Release detail */
.release-hero {
  display: grid;
  grid-template-columns: minmax(240px, 360px) 1fr;
  gap: 2.5rem;
  align-items: end;
  margin: 1rem 0 2rem;
}
.release-cover {
  width: 100%;
  display: block;
  border-radius: var(--radius);
  box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.release-head h1 { margin-top: 0.4rem; }
@media (max-width: 700px) {
  .release-hero { grid-template-columns: 1fr; gap: 1.2rem; }
  .release-cover { max-width: 300px; }
}
.release-detail .catalog { color: var(--accent-2); margin: 0; }
.release-detail .release-artist.large { font-size: 1.2rem; color: var(--ink-soft); margin: 0 0 0.6rem; }
.tracklist { list-style: decimal; padding-left: 1.5rem; }
.tracklist li { margin: 0.6rem 0; }
.tracklist .track-name { font-weight: 500; margin-bottom: 0.15rem; }
.tracklist .track-artist { font-size: 0.9rem; color: var(--accent-2); margin-bottom: 0.15rem; }
.tracklist .track-mix {
  font-family: "DM Mono", monospace;
  font-size: 0.8rem;
  color: var(--ink-soft);
  margin-bottom: 0.4rem;
}
/* The mix line owns the gap above the player, so give it back when an ISRC
   line follows and takes over that job. */
.tracklist .track-mix:has(+ .track-isrc) { margin-bottom: 0.15rem; }
.tracklist .track-isrc {
  font-family: "DM Mono", monospace;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  opacity: 0.75;
  margin-bottom: 0.4rem;
}
.tracklist audio { width: 100%; max-width: 500px; }

/* Missing audio. The note ships with every track and stays hidden until the
   <audio> element reports it can't load the file — see release.html. Without
   JS nothing is marked and the player simply won't play, which is the same
   thing that happened before this existed. */
.tracklist .track-unavailable { display: none; }
.tracklist li.is-unavailable .track-unavailable {
  display: block;
  font-family: "DM Mono", monospace;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  margin: 0.3rem 0 0;
}
.tracklist li.is-unavailable audio {
  opacity: 0.3;
  filter: grayscale(1);
  pointer-events: none;
}

.release-meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.4rem 1.2rem;
  margin: 1.5rem 0;
  max-width: 600px;
}
.release-meta dt {
  color: var(--ink-soft);
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.release-meta dd { margin: 0; }

code {
  font-family: "DM Mono", monospace;
  background: var(--line);
  padding: 0.1rem 0.3rem;
  border-radius: 3px;
  font-size: 0.9em;
}

/* ===== Home — Groove Ledger ===== */
.home-hero {
  display: grid;
  grid-template-columns: 1fr var(--hero-col);
  gap: 2rem;
  align-items: stretch;
  /* Height derived from the column so the box aspect stays ~1.02 at every
     size. That keeps every cutout height-bound under object-fit: contain, so
     they all render at the same height instead of jumping size between slides.
     A fixed taller value would make the wide cutouts (~1.02 aspect) width-bound
     and ~20% shorter than the narrow ones. */
  min-height: calc(var(--hero-col) - 20px);
  /* Cancel .site-main's right padding so the rotator runs flush to the right
     rule. The cutouts are cropped at their own right edge, so landing that
     crop on the rule reads as the frame cutting the subject rather than a
     sliced edge floating in space. The freed width goes to the 1fr text
     column, which also buys the headline more room. */
  margin-right: calc(var(--main-pad) * -1);
}
.home-hero-l { padding: 1.5rem 0; display: flex; flex-direction: column; justify-content: center; }
.home-hero-l h1 {
  /* Sized so "Live the rhythm." holds one line. It's 2 chars longer than any
     line the old headline carried, and the tightest column-to-font ratio across
     the range sits near 1300px wide — where --main-max stops tracking the
     viewport but --hero-col keeps growing — so that width governs this value. */
  font-size: clamp(2.4rem, 5.6vw, 5.6rem);
  line-height: 0.92;
  letter-spacing: -0.035em;
  font-weight: 800;
  margin: 0 0 1.3rem;
  text-wrap: balance;
}
.home-hero-l h1 em { font-style: normal; color: var(--accent); }
/* Deck under the headline. Sized to hold one line on desktop: all 56 characters
   have to clear the 1fr text column, and the tightest column-to-font ratio is
   near 1300px wide — the same width that governs the h1 above. No max-width,
   since a measure cap is what forced the wrap. Below the 900px breakpoint it
   wraps freely, which is fine — the single-column layout has the room. */
.home-hero-sub {
  font-size: clamp(1rem, 1.42vw, 1.4rem);
  line-height: 1.45;
  font-weight: 700;
  color: var(--ink-soft);
  margin: 0;
}
.home-hero-sub em { font-style: normal; color: var(--accent); }

/* rotating cutout gallery */
/* Deliberately no background: the cutouts are transparent, so the page shows
   through on its own. Painting --bg here made the box visible mid-theme-switch,
   because html transitions its background over 0.25s and this didn't — for that
   quarter second the rotator was a hard rectangle of the incoming colour. */
.hero-rot { position: relative; overflow: hidden; }
/* The <a> carries the crossfade so each slide is clickable through to its
   artist; the <img> inside only handles fit. Duration, delay and the
   heroCyc/heroCap keyframes all depend on how many artists are featured, so
   home.html emits them inline. See hero_timing(). */
.hero-rot a {
  position: absolute; inset: 0;
  opacity: 0;
  pointer-events: none;        /* the keyframe enables the visible slide */
  animation-name: heroCyc;
  animation-iteration-count: infinite;
  transform: scale(1.03);
}
.hero-rot img {
  width: 100%; height: 100%;
  object-fit: contain; object-position: right bottom;
  padding: 1.4rem 0 0 1rem;   /* no right padding — the crop meets the rule */
  filter: grayscale(1) contrast(1.05);
  display: block;
}

/* rotating name caption under the image */
.hero-nowcap {
  display: grid;
  grid-template-columns: 1fr var(--hero-col);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  /* Rules run rail to rail. Cancelling both paddings keeps the second track's
     right edge level with .home-hero's, so the name stays under the image. */
  margin-inline: calc(var(--main-pad) * -1);
}
.hero-nowcap .cell { grid-column: 2; padding: 0.85rem 0; text-align: center; }
/* Stack the names in one grid cell so they overlap for the crossfade while
   keeping the caption in normal flow (border sits below the text, not through it). */
.hero-nowcap .names { display: grid; justify-items: center; }
.hero-nowcap .names a {
  grid-area: 1 / 1;
  opacity: 0;
  pointer-events: none;        /* only the visible name is clickable (see keyframe) */
  font-weight: 800; letter-spacing: -0.01em; font-size: 1.05rem;
  white-space: nowrap;
  animation-name: heroCap;
  animation-iteration-count: infinite;
}
.hero-nowcap .names a:hover { color: var(--accent-2); }
.hero-nowcap .names a::after {
  content: "→";
  color: var(--accent);
  display: inline-block;
  margin-left: 0.35em;
  transition: transform 0.18s ease;
}
.hero-nowcap .names a:hover::after { transform: translateX(0.3em); }
/* Reduced motion keeps the rotation and loses the zoom.
   This used to stop the rotator dead on the first artist, which meant anyone
   with the setting on — and it is a common one — saw one of the seven featured
   artists and had no way of knowing the other six existed. The setting is there
   to suppress movement that can trigger vestibular discomfort: the
   scale(1.05 -> 1) qualifies, a dissolve doesn't. So swap in the flat keyframes
   instead of switching the animation off.
   heroCycFlat is emitted inline by home.html next to heroCyc. heroCap needs no
   variant — the caption never had a transform. */
@media (prefers-reduced-motion: reduce) {
  .hero-rot a { animation-name: heroCycFlat; transform: none; }
}

/* releases as sleeves */
.rel-block { margin: 2.4rem 0 0; }
.rel-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 1.2rem; }
.rel-head h2 {
  font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--ink-soft); margin: 0; font-weight: 500;
}
.rel-head a { font-size: 0.76rem; color: var(--accent-2); }
.rel-sleeves { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.3rem; }
.rel-sleeve .art {
  border-radius: 3px; overflow: hidden; aspect-ratio: 1;
  border: 1px solid var(--line);
  box-shadow: 0 10px 22px -14px rgba(0, 0, 0, 0.4);
}
.rel-sleeve img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; transition: transform 0.4s ease; }
.rel-sleeve:hover img { transform: scale(1.04); }
.rel-sleeve .cat {
  display: block; color: var(--accent-2); font-size: 0.72rem;
  font-variant-numeric: tabular-nums; margin: 0.6rem 0 0.15rem;
}
.rel-sleeve .ti { display: block; font-weight: 700; letter-spacing: -0.01em; line-height: 1.2; }
.rel-sleeve .ar { display: block; color: var(--ink-soft); font-size: 0.85rem; }

@media (max-width: 760px) {
  /* min-height/margin-right are reset so the rotator sizes purely from its own
     height and carries the full-bleed margins itself (the text column must keep
     its padding, so the bleed can't live on .home-hero here). */
  .home-hero { grid-template-columns: 1fr; margin-right: 0; min-height: 0; }
  /* The base clamp's 5.6vw term is tiny down here (21px at 375), so its floor
     governs and the headline stops scaling. Track the viewport instead. The
     ceiling is what "Live the rhythm." can take at 375 while holding one line;
     the old headline's longest was 2 chars shorter and took 3rem. */
  .home-hero-l h1 { font-size: clamp(2rem, 11.4vw, 2.65rem); }
  .hero-rot {
    /* Full width: cancel .site-main's padding on both sides. Every cutout is
       1224x1200, so matching that ratio lets object-fit: contain fill the box
       edge to edge — no letterboxing, and nothing cropped. */
    margin-inline: calc(var(--main-pad) * -1);
    min-height: 0;
    /* Height is stated outright rather than via aspect-ratio. .hero-rot is a
       grid item whose children are all position: absolute, so it has no
       in-flow content; WebKit stretches it to an auto row that measures 0 and
       the images vanish, while Blink resolves the ratio. The bleed makes the
       box exactly 100vw, so 1200/1224 of that is the same result. */
    height: calc(100vw * 1200 / 1224);
    border-top: 1px solid var(--line);
  }
  .hero-rot img { padding: 0; }
  .hero-nowcap { grid-template-columns: 1fr; }
  .hero-nowcap .cell { grid-column: 1; }
  .rel-sleeves { grid-template-columns: 1fr 1fr; }
}

/* ===== About page — accent lede + mentioned-artist roll call ===== */
.about-lede-h em { font-style: normal; color: var(--accent); }
.about-signoff { color: var(--accent-2); font-weight: 700; }
.prose a { text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line); }
.prose a:hover { text-decoration-color: var(--accent); }

/* ===== Music Publishing ===== */
.pub-list { list-style: none; margin: 0; padding: 0; }
.pub-list li {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--line);
}
.pub-name { font-weight: 800; letter-spacing: -0.01em; font-size: 1.05rem; }
.pub-pro {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent-2);
}
.pub-mail { font-weight: 800; font-size: clamp(1.1rem, 2.6vw, 1.5rem); letter-spacing: -0.02em; }
.pub-mail:hover { color: var(--accent); }

/* ===== Legal pages ===== */
.legal h2 {
  font-size: 1.15rem;
  letter-spacing: -0.02em;
  margin: 2.2rem 0 0.7rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--line);
  max-width: 70ch;
}
.legal h3 {
  font-size: 0.95rem;
  letter-spacing: -0.01em;
  margin: 1.5rem 0 0.4rem;
}
/* Anchor jumps must clear the sticky header (~86px) or the heading lands
   underneath it. */
.legal h2[id] { scroll-margin-top: 6.5rem; }
.legal > h2:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
.legal ul { max-width: 70ch; margin: 0 0 1rem; padding-left: 1.1rem; }
.legal li { color: var(--ink); margin: 0 0 0.45rem; }
.legal p { font-size: 0.95rem; }
/* Effective date sits above the first paragraph as a standalone stamp, so it
   gets the mono/accent treatment rather than reading as body copy. */
.legal .legal-date {
  color: var(--accent-2);
  font-size: 0.82rem;
  margin: 0 0 1.6rem;
}
.legal .legal-contact { line-height: 1.7; }
.about-rollcall { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
.about-rollcall .card-label { font-size: 0.82rem; }

/* ===== Contact page ===== */
/*
  Catalog cover graphic parked for now — revisit later.
  When re-enabled, put a transparent PNG at
  assets/brand/catalog-cover-bg.png and restore the body.page-contact::before
  block (fixed, cover, center right; opacity ~0.55 light / ~0.9 dark).
*/
body.page-contact > .site-main {
  background: transparent;
}

/* Content stage: transparent so the body graphic shows; journal rules stay. */
.contact-stage-inner {
  position: relative;
  margin: -2.5rem calc(var(--main-pad) * -1) -4rem;
  padding: 2.5rem var(--main-pad) 4rem;
  min-height: calc(100vh - 120px);
  color: var(--ink);
  background: transparent;
}
.contact-stage-inner > * { position: relative; z-index: 2; }
.contact-stage-copy { max-width: 720px; }
.contact-stage-inner .eyebrow { color: var(--accent-2); }
.contact-lede {
  color: var(--ink-soft);
  font-size: 1.1rem;
  max-width: 36ch;
  margin: 0 0 2rem;
}
.contact-label {
  display: block;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-soft);
  margin-bottom: 0.25rem;
}
.contact-email {
  color: var(--ink);
  font-weight: 600;
  letter-spacing: -0.01em;
  font-size: 0.92rem;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.contact-email:hover { color: var(--accent-2); }

/* Option A — Directory list */
.contact-dir {
  list-style: none;
  padding: 0;
  margin: 2rem 0 0;
  border-top: 1px solid var(--line);
  max-width: 560px;
}
.contact-dir li {
  border-bottom: 1px solid var(--line);
  padding: 1rem 0;
  display: grid;
  gap: 0.2rem;
}
.contact-dir .contact-email { font-size: 0.95rem; }

/* Option B — Catalog cover homage */
.contact-b .contact-stage-copy { max-width: 640px; }
.contact-b-eyebrow {
  font-size: clamp(1.4rem, 3vw, 2.2rem);
  font-weight: 800;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 0.4rem;
  letter-spacing: -0.02em;
}
.contact-b-title {
  font-size: clamp(4rem, 12vw, 7.5rem);
  font-weight: 900;
  line-height: 0.85;
  letter-spacing: -0.03em;
  margin: 0 0 0.6rem;
  text-transform: uppercase;
}
.contact-b-sub {
  font-style: italic;
  color: var(--ink-soft);
  margin: 0 0 1.2rem;
}
.contact-b-rule {
  width: 3.5rem;
  height: 2px;
  background: var(--accent);
  margin-bottom: 1.8rem;
}
.contact-b-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}
.contact-b-list .contact-email { font-size: 1rem; }

/* Option C — Channel cards (theme-aware glass panels) */
.contact-c .contact-stage-copy { max-width: none; }
.contact-channels {
  display: grid;
  /* Prefer 2 cols so long emails fit; 3 only when there's room. */
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1rem;
  margin-top: 0.5rem;
}
.contact-channel {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  padding: 1.1rem 1.15rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--card) 82%, transparent);
  backdrop-filter: blur(10px);
  transition: border-color 0.15s ease, transform 0.2s ease;
}
.contact-channel:hover {
  border-color: var(--accent);
  color: inherit;
  transform: translateY(-2px);
}
.contact-channel .contact-email {
  font-family: "DM Mono", ui-monospace, monospace;
  font-size: 0.82rem;
  font-weight: 500;
}

/* Option D — Split desk */
.contact-d .contact-d-grid {
  display: grid;
  grid-template-columns: 1fr minmax(300px, 420px);
  gap: 2.5rem;
  align-items: stretch;
}
.contact-d-intro h1 { margin-bottom: 1rem; }
.contact-d-panel {
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.4rem 1.4rem;
  align-self: start;
}
.contact-d-panel .contact-dir { margin-top: 0; border-top: none; max-width: none; }
.contact-d-panel .contact-dir li { border-bottom-color: var(--line); }
.contact-d-panel .contact-label { color: var(--ink-soft); }
.contact-d-panel .contact-email { color: var(--ink); }

@media (max-width: 760px) {
  .contact-stage-inner {
    margin: -2.5rem -2rem -4rem;
    padding: 2rem 1.25rem 3rem;
  }
  .contact-d .contact-d-grid { grid-template-columns: 1fr; gap: 1.5rem; }
  .contact-b-title { font-size: clamp(3.2rem, 18vw, 5rem); }
  .contact-channels { grid-template-columns: 1fr; }
}

/* ===== Releases — category filter pills ===== */
.rel-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0 0 1.8rem;
  padding-bottom: 1.2rem;
  border-bottom: 1px solid var(--line);
}
.rel-filter a {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-soft);
  white-space: nowrap;
  position: relative;
  isolation: isolate;               /* keeps the ::before fill behind the label */
  overflow: hidden;                 /* clips the fill to the pill radius */
  transition: border-color 0.25s ease, color 0.25s ease;
}
/* The accent fill wipes in from the left on hover, and is simply held open on
   the active pill — same mechanic, so hovering reads as "this is what you'd get". */
.rel-filter a::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.32s cubic-bezier(0.22, 0.7, 0.3, 1);
}
.rel-filter a:hover::before,
.rel-filter a.on::before { transform: scaleX(1); }
.rel-filter a:hover,
.rel-filter a.on { color: #fff; border-color: var(--accent); }
.rel-filter a.on { font-weight: 500; }
.rel-filter .n {
  font-size: 0.68rem;
  opacity: 0.55;
  font-variant-numeric: tabular-nums;
  transition: opacity 0.25s ease;
}
.rel-filter a:hover .n,
.rel-filter a.on .n { opacity: 0.8; }

/* Sleeves rise in on load, staggered across the grid, so changing filter reads
   as a new set arriving rather than a hard repaint. --i is the capped grid
   position (set in releases.html) — without a cap the tail of a 73-item grid
   would still be animating seconds later. */
.rel-sleeves--stagger .rel-sleeve {
  animation: relSleeveIn 0.5s cubic-bezier(0.22, 0.7, 0.3, 1) both;
  animation-delay: calc(var(--i, 0) * 45ms);
}
@keyframes relSleeveIn {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}
.rel-filter a { animation: relFilterIn 0.4s ease both; }
.rel-filter a:nth-child(1) { animation-delay: 0ms; }
.rel-filter a:nth-child(2) { animation-delay: 40ms; }
.rel-filter a:nth-child(3) { animation-delay: 80ms; }
.rel-filter a:nth-child(4) { animation-delay: 120ms; }
.rel-filter a:nth-child(5) { animation-delay: 160ms; }
@keyframes relFilterIn {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .rel-sleeves--stagger .rel-sleeve,
  .rel-filter a { animation: none; }
  .rel-filter a::before { transition: none; }
}

/* ===== Editorial backdrop (About / Publishing) =====
   A photo pinned to the top-right of .site-main, flush against its right rule
   — same convention as the home rotator, which also meets that rule. Hanging
   it off body instead put the art past the rail, which then drew a line down
   the middle of it.

   Dark skin only. The source PNGs are *glow* plates: their alpha is opaque
   where the photo is bright and clear where it is dark (corr(alpha, luminance)
   is +0.4 nightclub, +0.58 dj). They paint highlights and let the near-black
   --bg stand in for the shadows, which is a treatment the cream skin has no
   equivalent for — every light-skin version tried either vanished or read as a
   negative, so the light skin simply goes without.

   Two different treatments:

   About       the colour plate as-is. Its own alpha does the fading, so no
               mask is needed and it keeps the club's colour.
   Publishing  flat grey rather than colour, using the same trick as the
               speaker grill above: the plate becomes a mask and --spk-color
               supplies the fill. That lets it run much larger and pass under
               the body copy without competing with it.

   isolation on the parent gives the z-index:-1 pseudo a stacking context to
   sit in, so it lands behind the page copy but above the page background. */
:root[data-theme="dark"] body.page-about > .site-main,
:root[data-theme="dark"] body.page-publishing > .site-main {
  position: relative;
  isolation: isolate;
}
:root[data-theme="dark"] body.page-about > .site-main::before,
:root[data-theme="dark"] body.page-publishing > .site-main::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 0;
  right: 0;
  pointer-events: none;
}

/* About — the colour plate as-is. Portrait, to fill the tall right-hand gap
   this page's copy column leaves. */
:root[data-theme="dark"] body.page-about > .site-main::before {
  width: clamp(280px, 38vw, 620px);
  aspect-ratio: 900 / 1477;
  background: url("/assets/background-images/nightclub-tall-900.webp")
              top right / contain no-repeat;
}

/* Publishing — monotone grey, at roughly speaker-grill weight, large enough to
   run under the copy column. */
:root[data-theme="dark"] body.page-publishing > .site-main::before {
  width: clamp(420px, 58vw, 980px);
  aspect-ratio: 1000 / 640;
  background: var(--spk-color);
  opacity: 0.16;
  -webkit-mask: url("/assets/background-images/dj-1000-color.webp")
                top right / contain no-repeat;
          mask: url("/assets/background-images/dj-1000-color.webp")
                top right / contain no-repeat;
}

/* Below the breakpoint the copy runs full width and the image would sit behind
   the body text rather than beside it. */
@media (max-width: 760px) {
  :root[data-theme="dark"] body.page-about > .site-main::before,
  :root[data-theme="dark"] body.page-publishing > .site-main::before { display: none; }
}

/* ===== Error pages ===== */
.error-links { display: flex; flex-wrap: wrap; gap: 1.2rem; margin-top: 1.5rem; }
.error-links a {
  color: var(--accent-2);
  font-family: "DM Mono", ui-monospace, monospace;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.error-links a:hover { color: var(--ink); }

/* ===== Site-wide player =====
   Docked to the bottom of the viewport on every page. Markup and behaviour are
   in base.html.

   The bar is translucent over a blur, the same treatment as the mobile nav
   overlay: --overlay is --bg with alpha, which is the only reason there is
   anything for backdrop-filter to see. Where blur isn't supported the alpha
   still leaves it readable, just less pretty.

   Its height is a variable because the body's bottom padding depends on it, so
   the footer can be scrolled clear of the bar. Change it here only.

   92px is set by the contents, not chosen: three lines of type (song, credit,
   mix) against a 64px sleeve, whichever is taller, plus 14px of air. The mix
   line is why it isn't the 68px it started at — see .player-now. */
:root { --player-h: 92px; }

.player {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 12;               /* over the mobile nav overlay (9) and header (10) */
  /* column-reverse, so the queue opens *upward* out of the bar while the bar
     itself stays pinned to the bottom edge. Source order keeps the transport
     first, which is the right reading order — this only flips the painting. */
  display: flex;
  flex-direction: column-reverse;
  border-top: 1px solid var(--line);
  background: var(--overlay);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  backdrop-filter: blur(20px) saturate(140%);
}
/* Reserve the bar's height at the end of the page so the last line of the
   footer can still be read. Only when a player is actually rendered.

   Reserved from first paint whether or not the entrance below runs, so the
   slide never moves the page — the bar animates into space that was already
   set aside for it. */
body:has(.player) { padding-bottom: var(--player-h); }

/* Entrance: the bar slides up once per session — see base.html for why once,
   and not on every navigation. Parked off-screen by the attribute the template
   ships, so there is no frame of it painting in place first. 110% clears the
   top border.

   Only the "in" state carries a transition, so clearing the attribute (a
   visitor who has already seen it) puts the bar in place instantly rather than
   animating it there. */
.player[data-enter="pending"] { transform: translateY(110%); }
.player[data-enter="in"] {
  transform: translateY(0);
  transition: transform 0.5s cubic-bezier(0.2, 0.75, 0.25, 1);
}

/* A single ring leaving the play button, twice. The slide is half a second and
   easy to miss on a busy page; this gives the eye somewhere to land. It's
   box-shadow rather than a pseudo-element so it can't affect layout, and it
   stops on its own — and on the first click, which base.html handles. */
@keyframes player-hint {
  from { box-shadow: 0 0 0 0 rgba(232, 61, 15, 0.5); }
  to   { box-shadow: 0 0 0 15px rgba(232, 61, 15, 0); }
}
.player[data-hint] .player-play { animation: player-hint 1.7s ease-out 2; }

/* The bar is information, not decoration, so reduced motion keeps it — it just
   arrives already in place, with no slide and no ring. */
@media (prefers-reduced-motion: reduce) {
  .player[data-enter="pending"] { transform: none; }
  .player[data-enter="in"] { transition: none; }
  .player[data-hint] .player-play { animation: none; }
}

.player-bar {
  position: relative;   /* the phone layout pins the scrubber to this row */
  width: 100%;
  max-width: var(--main-max);
  margin: 0 auto;
  height: var(--player-h);
  padding: 0 var(--main-pad);
  display: flex;
  align-items: center;
  gap: 0.9rem;
}

.player-art {
  flex: none;
  width: 64px;
  height: 64px;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--card);
}
.player-art img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Three stacked lines, in the release page's tracklist voice: the song in DM
   Sans bold, the credit in --accent-2, the mix in DM Mono --ink-soft. Wide
   enough for a real mix name — "Richard Earnshaw Extended Rework" is 31
   characters and they get longer.

   min-width: 0 is what actually lets the ellipsis happen — without it the flex
   item refuses to shrink below its text and pushes the seek bar off the end. */
.player-now { min-width: 0; flex: 0 1 20rem; }
.player-title,
.player-artist,
.player-mix {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.35;
}
.player-title { font-weight: 700; font-size: 0.92rem; letter-spacing: -0.01em; }
.player-title:hover { color: var(--accent-2); }
.player-artist { font-size: 0.8rem; color: var(--accent-2); }
.player-mix { font-size: 0.74rem; color: var(--ink-soft); }

.player-transport { flex: none; display: flex; align-items: center; gap: 0.15rem; }
.player-btn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink);
  cursor: pointer;
}
.player-btn svg { width: 20px; height: 20px; display: block; }
.player-btn:hover { color: var(--accent); }
.player-play {
  width: 38px;
  height: 38px;
  background: var(--accent);
  color: #fff;
}
.player-play:hover { background: #c8330d; color: #fff; }
/* One button, two icons: which one shows is read off the state on .player, so
   nothing in the script touches className. The play triangle is nudged right
   because its optical centre sits left of its bounding box. */
.player-icon-play { margin-left: 2px; }
.player[data-playing="false"] .player-icon-pause,
.player[data-playing="true"] .player-icon-play { display: none; }

/* Capped rather than filling the row: on a 1500px canvas a scrubber that wide
   moves ~4px per second of a seven-minute mix, which is unusable to drag and
   reads as an empty rule. The leftover space goes to the queue button, which
   takes the right edge on its own. */
.player-progress { flex: 1 1 auto; max-width: 34rem; display: flex; align-items: center; gap: 0.6rem; }
.player-time { font-size: 0.72rem; color: var(--ink-soft); flex: none; font-variant-numeric: tabular-nums; }
/* Fully custom rather than accent-color on a native range. The phone layout
   needs a 3px hairline with no handle, and a native thumb can't be removed —
   it renders bigger than its own track and sticks out over the page above.
   Once appearance is off, the played portion has to be drawn too: --played is
   a percentage the script writes on timeupdate, and the gradient's two stops
   sit on it so the fill has a hard edge rather than a ramp.
   Firefox ignores all of that and uses ::-moz-range-progress instead. */
.player-seek {
  flex: 1 1 auto;
  min-width: 0;
  -webkit-appearance: none;
  appearance: none;
  height: 14px;               /* pointer target; the visible track is 4px */
  background: none;
  cursor: pointer;
}
.player-seek::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(to right,
              var(--accent) 0 var(--played, 0%), var(--line) var(--played, 0%) 100%);
}
.player-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -3.5px;         /* centres the 11px handle on the 4px track */
  border-radius: 50%;
  background: var(--accent);
}
.player-seek::-moz-range-track { height: 4px; border-radius: 2px; background: var(--line); }
.player-seek::-moz-range-progress { height: 4px; border-radius: 2px; background: var(--accent); }
.player-seek::-moz-range-thumb {
  width: 11px; height: 11px; border: 0; border-radius: 50%; background: var(--accent);
}
.player-seek:disabled { cursor: default; opacity: 0.45; }

.player-queue-toggle { flex: none; margin-left: auto; }
.player[data-playing] .player-queue-toggle[aria-expanded="true"] { color: var(--accent); }

/* Queue: a panel that grows upward out of the bar. Bottom-anchored and capped
   so a longer queue scrolls rather than covering the page. */
.player-queue {
  list-style: none;
  margin: 0 auto;
  padding: 0.5rem var(--main-pad) 0.7rem;
  width: 100%;
  max-width: var(--main-max);
  max-height: min(50vh, 380px);
  overflow-y: auto;
  border-bottom: 1px solid var(--line);
}
.player-queue-head {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-soft);
  padding: 0.35rem 0 0.5rem;
}
.player-queue-item { display: flex; align-items: center; gap: 0.6rem; }
.player-queue-play {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.4rem 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.player-queue-play img {
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 3px;
  object-fit: cover;
  border: 1px solid var(--line);
}
.player-queue-text { min-width: 0; display: block; }
.player-queue-title,
.player-queue-sub {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.player-queue-title { font-size: 0.85rem; font-weight: 500; }
.player-queue-sub { font-size: 0.72rem; color: var(--ink-soft); }
.player-queue-play:hover .player-queue-title { color: var(--accent-2); }
.player-queue-cat { flex: none; font-size: 0.7rem; color: var(--ink-soft); }
.player-queue-cat:hover { color: var(--accent-2); }
.player-queue-item.is-current .player-queue-title { color: var(--accent-2); font-weight: 700; }
/* Same convention as the release page tracklist: a track whose MP3 isn't in
   the bucket yet goes quiet rather than disappearing. */
.player-queue-item.is-unavailable { opacity: 0.4; }
.player-queue-item.is-unavailable .player-queue-play { cursor: default; }

/* Phones: the transport keeps play only, and the scrubber leaves the row to
   become a hairline across the top edge of the bar — there isn't room for a
   title, three buttons, two timestamps and a slider at 360px. */
@media (max-width: 700px) {
  :root { --player-h: 80px; }
  /* Every pixel here is bought back for the text column. At 390px the row is
     the sleeve, three lines, play and queue; with the page's usual --main-pad
     and 0.7rem gaps there are ~186px left for type, which cuts a plain
     "Groove Disciple Extended Mix" short. Tightening the frame gets it to
     ~215px, enough for a normal mix name. Long credits still ellipsis — at this
     width something has to, and the release page carries the full billing. */
  .player-bar { gap: 0.6rem; padding: 0 0.9rem; }
  .player-art { width: 52px; height: 52px; }
  .player-title { font-size: 0.86rem; }
  .player-artist { font-size: 0.76rem; }
  .player-mix { font-size: 0.7rem; }
  .player-prev,
  .player-next,
  .player-time { display: none; }
  .player-now { flex: 1 1 auto; }
  .player-progress {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    flex: none;
  }
  /* A hairline across the top edge with no handle: at this size a handle would
     stick out over the page above, and a 3px drag target is a fiction anyway.
     It reads as progress; seeking is a desktop affordance here. */
  .player-seek { height: 3px; width: 100%; margin: 0; }
  .player-seek::-webkit-slider-runnable-track { height: 3px; border-radius: 0; }
  .player-seek::-webkit-slider-thumb { width: 0; height: 0; margin: 0; }
  .player-seek::-moz-range-track,
  .player-seek::-moz-range-progress { height: 3px; border-radius: 0; }
  .player-seek::-moz-range-thumb { width: 0; height: 0; }
}
