/* ═══════════════════════════════════════════════════════════════════════════
   7 MINUTES FROM MARS — the site's design system.

   ⚠️ This file is OURS. css/style.css belongs to EchoHex's demo site and is
   deliberately excluded from the update manifest; this one is not in the
   manifest either, so no release can ever repaint the label.

   THE LAWS THIS FOLLOWS (ETCH, and the ones the fleet paid for):
   · Draw it, don't download it. There is not one image in this background.
   · Flat lines. Every shaded, "realistic" thing tried on the Mars pages failed
     — mush at low contrast, leopard print at real contrast. Lines survived.
   · Decoration may not imply a state it cannot know. So: no numerals, no
     signal bars, no telemetry readouts, no counters. Those would be claims.
     Abstract motion is honest; a fake number is a lie with a font.
   · A fault is loud, a cosmetic absence is quiet.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── ground — TRUE NEUTRAL ────────────────────────────────────────────────
     ETCH's neutrals carry a blue-violet bias that exists to flatter House
     gold. Against ember that cast goes muddy, so these are greys, not tinted
     greys. Hue moved; luminance is the design. */
  --bg:        #070707;
  --bg-lift:   #0d0d0d;
  --panel:     #131313;
  --panel-2:   #1a1a1a;

  /* ── the accent ───────────────────────────────────────────────────────────
     ONE accent. Ember — entry heat, not fire-engine red, which reads as an
     error state. An artist page may override --accent per artist; everything
     downstream derives from it, so nothing has to be restated. */
  --accent:      #e0623c;
  --accent-lift: #f08a63;
  --accent-deep: #8f3319;

  /* ⛔ DERIVED FROM THE ACCENT, never hardcoded. ETCH learned this the hard
     way: literal rgba() left every hairline gold on a themed account — the
     accent changing everywhere except the device the design was built from. */
  --etch:      rgba(255, 255, 255, 0.08);
  --etch-warm: color-mix(in srgb, var(--accent) 22%, transparent);
  --etch-soft: color-mix(in srgb, var(--accent) 10%, transparent);
  --glow:      color-mix(in srgb, var(--accent) 38%, transparent);

  --text:  #ecebe9;
  --dim:   #9a9895;
  --muted: #625f5c;

  --font-body: "IBM Plex Sans", "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;

  /* eight steps, nothing between them */
  --t-micro: 0.6875rem; --t-label: 0.75rem;  --t-sm: 0.875rem; --t-base: 1rem;
  --t-lead:  1.25rem;   --t-h3:    1.75rem;  --t-h2: 2.5rem;   --t-h1:  3.75rem;

  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px; --s5: 24px;
  --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;

  --swift: 160ms cubic-bezier(0.4, 0, 0.2, 1);
  --glide: 320ms cubic-bezier(0.22, 0.61, 0.36, 1);

  --track: 0.09em; --track-wide: 0.14em; --track-max: 0.2em;
  --max-width: 1180px;

  /* ⛔ ONE SOURCE OF TRUTH FOR THE BAR'S HEIGHT. The topbar is sticky, which
     means it is IN FLOW - so an artist page is 50px + 100svh and the band's
     bottom content sat 14px above the fold. It fitted on a desktop and would
     have dropped below the fold on any phone where the safe-area inset makes
     the bar taller. The bar SETS its height from this and the first band
     SUBTRACTS it, so the two cannot drift apart. */
  --topbar-h: 50px;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { color-scheme: dark; -webkit-text-size-adjust: 100%; }

body {
  min-height: 100svh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--t-base);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }
[hidden] { display: none !important; }
a { color: inherit; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE BACKGROUND — three drawn layers.

   All of it is fixed, behind everything, and pointer-events:none. ⚠️ That last
   part is not optional: a full-screen layer that swallows taps silently turns
   the page into a modal, which is a behaviour change nobody asked for.

   Everything animates TRANSFORM and OPACITY only — the two properties the
   compositor can handle without relayout or repaint. No background-position
   animation, no filters, no box-shadow pulsing.
   ═══════════════════════════════════════════════════════════════════════════ */

.sky { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }

/* ── 1. THE GRATICULE ──────────────────────────────────────────────────────
   An instrument ground, drifting down forever. Oversized and transform-moved
   rather than background-position-animated, so it rides the compositor.
   The element is 200% tall and travels exactly one tile (56px), so the loop
   is seamless — a drift that jumps at the seam reads as a rendering fault. */
.sky-grid {
  position: absolute;
  inset: -50% 0 -50% 0;
  background-image:
    linear-gradient(to right,  var(--etch) 1px, transparent 1px),
    linear-gradient(to bottom, var(--etch) 1px, transparent 1px);
  background-size: 56px 56px;
  opacity: 0.30;
  animation: grid-drift 14s linear infinite;
  mask-image: radial-gradient(ellipse 120% 80% at 50% 30%, #000 30%, transparent 78%);
}
@keyframes grid-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, 56px, 0); }
}

/* ── 2. THE DESCENT ────────────────────────────────────────────────────────
   Seven traces falling. Seven because that is the name, and because a count
   that means something is free to get right.

   Each is a hairline with an ember head, falling the full height at its own
   speed. This is the one piece of motion on the site that is genuinely ABOUT
   something: the whole identity is seven minutes of falling with nobody able
   to help. ⚠️ It carries no numbers and claims no state — it is a mood, and a
   mood is allowed to be abstract. */
.sky-fall { position: absolute; inset: 0; }
.trace {
  position: absolute;
  top: -22vh;
  width: 1px;
  height: 20vh;
  background: linear-gradient(to bottom, transparent, var(--etch-warm) 55%, var(--glow));
  opacity: 0;
  animation: fall linear infinite;
  will-change: transform, opacity;
}
/* The ember head. Small, and it is the only lit thing in the layer. */
.trace::after {
  content: '';
  position: absolute;
  left: -1px; bottom: -1px;
  width: 3px; height: 3px;
  background: var(--accent);
  border-radius: 50%;
}

@keyframes fall {
  0%          { transform: translate3d(0, 0, 0);      opacity: 0; }
  8%          {                                       opacity: 1; }
  88%         {                                       opacity: 1; }
  100%        { transform: translate3d(0, 128vh, 0);  opacity: 0; }
}

/* Staggered by hand rather than by a loop, so each trace can be placed where
   it looks right instead of where the arithmetic puts it. Nothing lands on a
   column of the graticule — a trace riding a grid line looks like a glitch. */
.trace:nth-child(1) { left:  7%; animation-duration: 15s; animation-delay:  0s;   }
.trace:nth-child(2) { left: 21%; animation-duration: 22s; animation-delay: -6s;   }
.trace:nth-child(3) { left: 35%; animation-duration: 18s; animation-delay: -12s;  }
.trace:nth-child(4) { left: 52%; animation-duration: 26s; animation-delay: -3s;   }
.trace:nth-child(5) { left: 67%; animation-duration: 20s; animation-delay: -16s;  }
.trace:nth-child(6) { left: 81%; animation-duration: 29s; animation-delay: -9s;   }
.trace:nth-child(7) { left: 93%; animation-duration: 17s; animation-delay: -21s;  }

/* ── 3. THE HORIZON ────────────────────────────────────────────────────────
   Entry heat at the bottom edge. It breathes very slowly — slow enough that
   you notice it only if you sit still, which is the right amount for a thing
   behind text. Opacity only. */
.sky-horizon {
  position: absolute;
  left: -20%; right: -20%; bottom: -34vh;
  height: 62vh;
  background: radial-gradient(ellipse at 50% 100%,
              color-mix(in srgb, var(--accent) 26%, transparent) 0%,
              color-mix(in srgb, var(--accent) 7%, transparent) 38%,
              transparent 68%);
  animation: breathe 13s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 0.95; }
}

/* Keeps text legible over all of it. ⛔ The legibility lives in this flat
   wash, NOT in a backdrop-filter — a blur that is unsupported degrades to
   nothing, where an unsupported flat layer still darkens. And eight
   full-screen filter passes per frame is what we removed from Mars Deck. */
.sky-scrim {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 90% 70% at 50% 40%,
              transparent 0%, rgba(7,7,7,0.55) 62%, rgba(7,7,7,0.88) 100%);
}

/* ⛔ REDUCED MOTION IS NOT "LESS MOTION", IT IS NONE.
   The layers stay — the design is the same drawing — they simply stop. The
   traces are frozen mid-fall at a readable opacity rather than hidden, so the
   composition does not change shape for anyone. */
@media (prefers-reduced-motion: reduce) {
  .sky-grid, .trace, .sky-horizon { animation: none !important; }
  .trace { opacity: 0.5; top: 14vh; }
  .sky-horizon { opacity: 0.75; }
  /* ⛔ The breath stops, the DOT STAYS — and it stays at full strength, not
     at whatever opacity the animation happened to be holding. Stopping an
     animation must never remove information, only movement. */
  .stamp.soon::before { animation: none; opacity: 1; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CHROME
   ═══════════════════════════════════════════════════════════════════════════ */

.shell { position: relative; z-index: 1; }
.wrap  { width: 100%; max-width: var(--max-width); margin-inline: auto; padding-inline: var(--s5); }

.topbar {
  border-bottom: 1px solid var(--etch);
  /* ⛔ OPAQUE, AND IT WAS NOT BEFORE. This sat at 78% with no blur behind it,
     so whatever scrolled underneath stayed crisply readable THROUGH the bar —
     "6 RELEASES" printed over the wordmark, the vertical cue's last two letters
     over the nav. Frosted glass needs a blur to be frosted; without one, 78% is
     just a window. And per the house rule the legibility belongs in the
     BACKGROUND ALPHA, never in a filter, so that a browser without
     backdrop-filter degrades to a flat wash rather than to nothing.
     Scrolled, this is the black bar he asked for. */
  background: var(--bg);
  /* ⛔ FIXED, NOT STICKY, AND THAT IS THE WHOLE CHANGE. Sticky sits IN FLOW:
     a transparent sticky bar does not reveal the photograph behind it, it
     reveals 50px of page background, which is the same black strip with the
     border taken off. Only taking the bar out of flow lets the hero start at
     y=0 and run under it. */
  position: fixed; top: 0; left: 0; right: 0; z-index: 20;
  transition: background var(--glide), border-color var(--glide);
  /* Height declared rather than implied by padding, so --topbar-h is a fact
     instead of an estimate of one. */
  height: calc(var(--topbar-h) + env(safe-area-inset-top, 0px));
  padding-top: env(safe-area-inset-top, 0px);
  display: flex; align-items: center;
}
.topbar > .wrap { width: 100%; }

/* ── the bar on a phone ─────────────────────────────────────────────
   ⛔ MEASURED, NOT GUESSED: at 390px the brand is 199px and the nav is 170px,
   which with the gap needs 393px of the 375px available. So the bar wrapped to
   two rows while its height stayed declared at 50px — and the nav rendered
   2px BELOW the bar's own bottom edge, outside its background entirely. That
   was true before this bar ever went transparent; making it transparent is
   only what finally made it visible.

   ⚠️ THE NAME IS STILL ANNOUNCED. The words are clipped, never `display:
   none`, so the link's accessible name remains "7 Minutes from Mars" for a
   screen reader while the eye gets the label's own mark. Hiding them properly
   would leave a home link called "7". */
@media (max-width: 520px) {
  .brand { gap: 0; }
  .brand-rest {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ── at rest: no bar, just enough ground to read by ───────────────────────
   ⛔ A GRADIENT, NOT A FLAT ALPHA. A flat wash is still a bar — it just has a
   softer edge, and the eye finds that edge immediately. A gradient fading to
   nothing reads as the photograph's own vignette, which is what the bands
   already do at their feet.

   ⚠️ IT IS NOT DECORATION, IT IS THE CONTRAST. What sits behind this is an
   arbitrary press shot that changes with every artist, so nothing about the
   text's legibility can be assumed from the page's own colours. The top stop
   is heavy enough that white text clears AA even over a white frame, and the
   text carries the same soft shadow `.band-name` uses on top of that. */
html.at-top .topbar {
  background: linear-gradient(to bottom,
              rgba(7, 7, 7, 0.72) 0%,
              rgba(7, 7, 7, 0.34) 55%,
              rgba(7, 7, 7, 0) 100%);
  border-bottom-color: transparent;
}
html.at-top .topbar .brand,
html.at-top .topbar .topnav a { text-shadow: 0 1px 12px rgba(7, 7, 7, 0.95); }
.topbar .wrap { display: flex; align-items: center; gap: var(--s5); flex-wrap: wrap; }

/* ⛔ Every inheritable property that can change this component's box is
   declared on it, whether or not a host is likely to set one. Referencing a
   host variable is one door; INHERITANCE is the other, and it is silent —
   line-height alone once gave one shared bar two different heights across
   eight pages with byte-identical CSS. */
.brand {
  font-family: var(--font-body);
  font-size: var(--t-sm); font-weight: 700; font-style: normal;
  line-height: 1.2; letter-spacing: var(--track-wide); text-transform: uppercase;
  white-space: nowrap; text-decoration: none; color: var(--text);
  display: inline-flex; align-items: center; gap: var(--s2);
}
.brand .seven { color: var(--accent); }
.brand:hover { color: var(--accent-lift); }

.topnav { display: flex; gap: var(--s5); margin-left: auto; flex-wrap: wrap; }
.topnav a {
  font-family: var(--font-mono);
  font-size: var(--t-micro); font-weight: 400; font-style: normal;
  line-height: 1.2; letter-spacing: var(--track-wide); text-transform: uppercase;
  white-space: nowrap; text-decoration: none; color: var(--dim);
  transition: color var(--swift);
}
.topnav a:hover, .topnav a:focus-visible { color: var(--accent); }
.topnav a[aria-current="page"] { color: var(--text); }

/* ═══════════════════════════════════════════════════════════════════════════
   SHARED BITS
   ═══════════════════════════════════════════════════════════════════════════ */

.kicker {
  font-family: var(--font-mono);
  font-size: var(--t-micro); letter-spacing: var(--track-max); text-transform: uppercase;
  color: var(--accent); margin-bottom: var(--s3);
}
.kicker.quiet { color: var(--muted); }

.rule { height: 1px; background: var(--etch); border: 0; margin-block: var(--s7); }

/* ═══════════════════════════════════════════════════════════════════════════
   ARTIST PAGE
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── THE ARTIST INTRO ─────────────────────────────────────────────────────
   Stephan: "under the artist name, one paragraph about the artist, and then
   their Spotify, Apple Music, Instagram and X as links."

   It sits directly under the band so it IS under the name, and the links moved
   out of the band to get here - four buttons laid over a full-bleed photograph
   were clutter on the one element that should stay cinematic. */
.artist-intro { padding-block: var(--s8) 0; }

.artist-bio {
  font-size: clamp(1.05rem, 2.2vw, 1.35rem);
  font-weight: 300; line-height: 1.6; color: var(--dim);
  max-width: 62ch;
}

/* ── the longer piece ────────────────────────────────────────────────
   Three voices down the page, each quieter and smaller than the last: the
   tagline states, the bio introduces, this one explains. Set any two of them
   at the same size and the page reads as one undifferentiated block of text
   and nobody finishes it.

   ⚠️ A NARROWER measure than the bio above it, not a wider one. A long read
   needs a SHORTER line than a short read — the eye has to find the start of
   the next line many more times, and every extra character makes that harder.
   Widening the measure because there is more text is the instinct, and it is
   backwards. */
.artist-story { margin-top: var(--s6); max-width: 58ch; }
.artist-story p {
  font-size: var(--t-body);
  font-weight: 300; line-height: 1.75; color: var(--dim);
}
.artist-story p + p { margin-top: var(--s5); }

/* ── TO THE MUSIC ────────────────────────────────────────────────────
   The artist page's own cue, bottom right of the hero. Same falling-ember
   language as the homepage's, and the same `cue-fall` keyframes — one idea,
   one animation, defined once.

   ⛔ THIS ONE IS A LINK AND THE HOMEPAGE'S IS NOT, WHICH CHANGES ITS DUTIES.
   Decoration can be `pointer-events: none` and aria-hidden. A control cannot:
   it needs a hover state, a visible focus ring for anyone arriving by keyboard,
   and a tap target big enough to actually hit. The padding below is the tap
   target — the ink is small, the hit area is not. */
.drop-cue {
  position: absolute;
  right: var(--s5); bottom: var(--s7);
  display: flex; flex-direction: column; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s4);   /* the hit area, not the look */
  text-decoration: none;
  color: var(--dim);
  transition: color .22s ease;
}
.drop-cue-label {
  /* Top-to-bottom, so the reading direction matches the movement it names.
     See the note on .scroll-cue-label: vertical-rl alone is top-to-bottom and
     adding a 180deg turn silently reverses it. */
  writing-mode: vertical-rl;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  white-space: nowrap;
  color: inherit;
  /* ⛔ THIS CUE SITS ON A PHOTOGRAPH, NOT ON THE FLAT GROUND. The homepage's
     measured 5.85:1 against `--bg` and that number means nothing here, where
     whatever is behind it changes with every artist's press shot. Same soft
     ground `.band-name` already uses, and for the same reason: the letterforms
     have to hold on ANY frame underneath, including a bright one. */
  text-shadow: 0 1px 14px rgba(7,7,7,0.9);
}
.drop-cue-line {
  position: relative;
  width: 1px; height: 46px;
  background: linear-gradient(to bottom, transparent, var(--etch-warm));
  overflow: visible;
}
.drop-cue-line::after {
  content: '';
  position: absolute; left: -1.5px; bottom: -2px;
  width: 4px; height: 4px; border-radius: 50%;
  background: var(--accent);
  animation: cue-fall 2.6s ease-in-out infinite;
}
.drop-cue:hover, .drop-cue:focus-visible { color: var(--accent); }

/* ⚠️ A focus ring that only an outline draws. The hover state is a COLOUR
   change, and colour alone is not a focus indicator — it fails for anyone who
   cannot distinguish it and it disappears entirely against the ember accent. */
.drop-cue:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .drop-cue-line::after { animation: none; transform: none; opacity: 1; }
}
/* On a phone the words go first and the falling head stays: it still says
   "there is more below", and the words are the affordable loss. */
@media (max-width: 720px) {
  .drop-cue { right: var(--s3); bottom: var(--s6); }
  .drop-cue-label { display: none; }
}
.artist-links { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s6); }

/* ── the featured release ──────────────────────────────────────────────────
   The shop window. Cover on the left at real size, everything known about the
   record on the right. */
.feature {
  display: grid;
  grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
  gap: var(--s7);
  align-items: start;
  padding: var(--s6);
  border: 1px solid var(--etch);
  background: color-mix(in srgb, var(--panel) 72%, transparent);
}
.feature-cover {
  position: relative;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--etch);
  background: var(--panel-2);
  overflow: hidden;
}
.feature-cover img { width: 100%; height: 100%; object-fit: cover; }

/* ⛔ An unwired cover is QUIET, not a torn-page glyph. A decorative absence
   that fails loudly is the exact inversion of the rule — a working site that
   looks broken. So a missing cover renders as a drawn plate, on purpose. */
.cover-empty {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  background:
    repeating-linear-gradient(45deg, transparent 0 11px, var(--etch-soft) 11px 12px);
}
.cover-empty span {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--muted);
}

.feature-meta { min-width: 0; }
/* ⛔ THE TITLE CARRIES THE COLUMN. Stephan: "the title should be bigger and
   bold to fill out almost that area." The cover is 420px and the column beside
   it is ~700px at full width, so the type is sized to own that space rather
   than sit politely at the top of it.
   line-height under 1 is safe here because a release title is one or two
   words; the archive below clamps instead, where lengths are unpredictable. */
.feature-meta h2 {
  /* 4rem, down from 4.4rem on 2026-09-18. Measured against the artist name in
     the band one screen above, which renders at ~90px: at 4.4rem this title was
     0.78 of it and read as competing with the artist's own name; at 4rem it is
     0.71 and steps down cleanly. The block loses only 12px of height (270 ->
     258), so the balance against the 360px cover does not move.
     ⚠️ The 90px of space above this block is AIR, not a gap - it sits over a
     bottom-aligned stack. Sizing type to close it would be solving the wrong
     problem, which is what my first reading of the measurement did. */
  font-size: clamp(2rem, 5.4vw, 4rem);
  font-weight: 700; line-height: 0.95;
  margin-bottom: var(--s4);
  text-transform: uppercase;
  /* ⚠️ TRACKING LOOSENED WITH THE CAPS, from -0.045em. Negative tracking is
     tuned for mixed case, where lowercase letters carry their own side
     bearings; capitals are wider and flatter-sided, so the same value jams
     them together. -0.015em keeps the display weight without the collision. */
  letter-spacing: -0.015em;
}
.stamp-row { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); margin-bottom: var(--s5); }
.stamp {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--muted);
  border: 1px solid var(--etch); padding: 3px var(--s2);
}
.stamp.lit { color: var(--accent); border-color: var(--etch-warm); }

/* ── NOT OUT YET — a STATE, drawn differently from a FACT ───────────────────
   ⛔ IT DOES NOT USE `lit`. Ember here means "a specification we can stand
   behind" — the type stamp, the artist links. A release's availability is a
   different kind of claim, and two different meanings sharing one colour is
   how an accent stops meaning anything; EchoBorn's palette already paid for
   that lesson once. So the WORDS go bright (this is the most important thing
   in the row, and `--text` outranks the muted metadata beside it) and the
   ember is spent on one 5px dot.

   ⚠️ THE DOT MAY BREATHE BECAUSE THE STATE IS KNOWN. The rule it would
   otherwise break — decoration may not imply a state it cannot know — is
   about animation inventing a fact, like a level meter dancing on a dead
   channel. Here the fact is in the database: this record is coming. The
   motion says the one true thing. */
.stamp.soon {
  color: var(--text);
  border-color: var(--etch-warm);
  display: inline-flex; align-items: center; gap: var(--s2);
}
.stamp.soon::before {
  content: '';
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent); flex: none;
  /* ⛔ Floors at 0.35, never 0. A mark that animates to invisible is a mark
     that is MISSING for part of every cycle, and on a slow screenshot or a
     frame-stepped capture it reads as a rendering fault. The breath is the
     decoration; the dot itself is information and it never leaves. */
  animation: soon-breathe 2.8s ease-in-out infinite;
}
@keyframes soon-breathe {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}

/* The kicker above the title, and the scroll cue that points at it. Both go
   BRIGHTER than their released wording rather than dimmer — the whole point
   is that this is the loudest thing on the page, not a footnote. */
.kicker.soon { color: var(--accent-lift); }
.drop-cue.soon { color: var(--text); }

/* One line over the service row. Muted on purpose: it is a caption for the
   slots under it, not a second announcement competing with the stamp. */
.listen-note {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--muted); margin-bottom: var(--s3);
}

/* In the archive list the mark sits ABOVE the metadata line, because it is
   the thing that changes what the row means. */
.arc-soon {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--accent-lift); margin-top: 3px;
}

/* ── REPRESENTATION + CONTACT ───────────────────────────────────────────────
   Plain, legible blocks. ⛔ Deliberately undesigned next to the bands: the
   reader here is somebody checking a fact, and a fact they have to decode is
   a fact they may get wrong. The 56ch cap is the same reading measure the
   feature notes use. */
.rep { max-width: 62ch; }
.rep-line { color: var(--dim); font-weight: 300; margin-bottom: var(--s4); }
.rep-line strong { color: var(--text); font-weight: 600; }
.rep-line a { color: var(--accent); }
.rep-line a:hover, .rep-line a:focus-visible { color: var(--accent-lift); }
.rep-line:last-child { margin-bottom: 0; margin-top: var(--s5); }

.contact-list { list-style: none; border-top: 1px solid var(--etch); max-width: 72ch; }
.contact-row { padding-block: var(--s4); border-bottom: 1px solid var(--etch); }
.contact-kind {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--accent); margin-bottom: var(--s2);
}
.contact-note { color: var(--dim); font-weight: 300; max-width: 62ch; }
.contact-note a { color: var(--text); border-bottom: 1px solid var(--etch-warm); }
.contact-note a:hover, .contact-note a:focus-visible { color: var(--accent); }
.contact-cta { margin-top: var(--s6); }

/* The address in the contact hero sits on a photograph, so it takes the same
   soft ground .band-name uses rather than relying on the picture being dark. */
.band-meta .lit a { color: inherit; text-shadow: 0 1px 14px rgba(7, 7, 7, 0.9); }

.feature-notes { color: var(--dim); font-weight: 300; margin-bottom: var(--s5); max-width: 56ch; }

/* ── the player ────────────────────────────────────────────────────────────
   ⛔ Our own audio element, never a third-party embed. You cannot style inside
   a cross-origin iframe, and a streaming embed for a track that is not out yet
   or is region-locked renders somebody else's empty state in the middle of our
   layout. Streaming services are LINKS. */
.player { width: 100%; margin-bottom: var(--s5); }
.player audio { width: 100%; height: 40px; }

.links { display: flex; flex-wrap: wrap; gap: var(--s3); }
.link-btn {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  text-decoration: none; color: var(--text);
  border: 1px solid var(--etch); padding: var(--s3) var(--s4);
  transition: border-color var(--swift), color var(--swift), background-color var(--swift);
}
.link-btn:hover, .link-btn:focus-visible {
  color: var(--accent-lift); border-color: var(--etch-warm);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}

/* ── the archive ───────────────────────────────────────────────────────────
   Proof there is a catalogue behind the shop window. */


.rel-links { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s2); }
.rel-links a {
  font-family: var(--font-mono); font-size: var(--t-micro);
  color: var(--dim); text-decoration: none;
  border-bottom: 1px solid var(--etch); transition: color var(--swift), border-color var(--swift);
}
.rel-links a:hover { color: var(--accent); border-color: var(--etch-warm); }

/* ⛔ An empty catalogue says so plainly and does NOT invent a placeholder
   record. An empty state hides itself; it never fabricates content to fill a
   grid. */
.empty-note {
  border: 1px dashed var(--etch); padding: var(--s6);
  color: var(--muted); font-family: var(--font-mono); font-size: var(--t-sm);
}

/* ═══════════════════════════════════════════════════════════════════════════
   ROSTER (index)
   ═══════════════════════════════════════════════════════════════════════════ */



/* ═══════════════════════════════════════════════════════════════════════════
   FOOTER
   ═══════════════════════════════════════════════════════════════════════════ */

.site-foot {
  border-top: 1px solid var(--etch);
  margin-top: var(--s9);
  padding-block: var(--s6);
  padding-bottom: calc(var(--s6) + env(safe-area-inset-bottom, 0px));
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track); text-transform: uppercase; color: var(--muted);
}
.site-foot .wrap { display: flex; flex-wrap: wrap; gap: var(--s3) var(--s5); align-items: center; }
.site-foot a { color: var(--dim); text-decoration: none; }
.site-foot a:hover { color: var(--accent); }
.site-foot .spacer { margin-left: auto; }

/* ═══════════════════════════════════════════════════════════════════════════
   NARROW
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 860px) {
  /* ⛔ `.feature` IS NOT STYLED HERE ANY MORE, AND THAT IS THE POINT.
     Its columns, gap and padding were all declared in this block and all
     three were overridden by the unconditional `.feature` rules in the
     artist-page section further down — same specificity, later in the
     file. Two of the three happened to be re-stated down there; the
     column count was not, and the block silently stopped stacking on
     phones. The live rule is in the `max-width: 860px` block beside those
     declarations, where the cascade cannot step over it. */
  .feature-cover { max-width: 420px; }
}
@media (max-width: 520px) {
  .wrap { padding-inline: var(--s4); }
  .site-foot .spacer { margin-left: 0; }
  .topnav { gap: var(--s4); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE LABEL HOMEPAGE
   Identity first, roster second, catalogue as proof. No news block — there are
   zero posts, and a "Latest news" heading over nothing is a homepage
   advertising its own emptiness.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Sized DOWN 2026-09-17 on Stephan's call: the hero was pushing the first
   artist band below the fold, and on this site the band IS the product.
   The wordmark still reads as the loudest thing on the page - it just no
   longer costs a whole screen to say it. */
/* ── THE TITLE CARD ──────────────────────────────────────────────────────
   Stephan, 2026-09-17: "when the page loads at 100vh I want only to see
   7 Minutes from Mars and then Sophia there."

   So the first screen carries the wordmark, the mark, and NOTHING else. The
   lede and the counts moved to a closing statement at the foot of the page —
   open with the name, close with what it means. A title card that also argues
   its case is not a title card.

   svh, not vh: on a phone `vh` is the LARGE viewport, so a 100vh hero is
   taller than the screen until the URL bar retracts and the first thing a
   visitor does is scroll past a gap. */
.hero {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: var(--s8);
  position: relative;
}

.hero h1 {
  /* Big again - it was sized DOWN earlier today only because it was crowding
     the first artist band off the screen. Now it owns a screen of its own and
     there is nothing to crowd. */
  font-size: clamp(2.6rem, 11vw, 7.5rem);
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 0.92;
  margin-bottom: var(--s6);
}
.hero h1 .seven { color: var(--accent); }
.hero h1 .line2 { display: block; }

/* ── the EDL trace ─────────────────────────────────────────────────────────
   Seven ticks, touchdown lit. The same mark as the holding page, full width
   here. Drawn in the markup as SVG so it scales without a raster and needs no
   request of its own. */
.hero-mark { display: block; width: 100%; height: auto; margin-bottom: var(--s5); }
.hero-mark .trace-line { stroke: var(--etch); stroke-width: 1; fill: none; }
.hero-mark .tk      { stroke: var(--muted); stroke-width: 1; }
.hero-mark .tk-last { stroke: var(--accent); stroke-width: 1.5; }
.hero-mark .land    { fill: var(--accent); }

.hero-lede {
  font-size: clamp(0.95rem, 2.1vw, 1.1rem);
  font-weight: 300; line-height: 1.6; color: var(--dim);
  max-width: 54ch; margin-bottom: var(--s5);
}
.hero-lede strong { color: var(--text); font-weight: 400; }

.hero-foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s4) var(--s5); }
/* ⛔ A real count, read from the database. Never decoration, never rounded up. */
.hero-count {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--muted);
}

/* ── section headings ─────────────────────────────────────────────────────── */
.sec { padding-block: var(--s8) 0; }
.sec-head { display: flex; align-items: baseline; gap: var(--s4); margin-bottom: var(--s5); flex-wrap: wrap; }
.sec-head h2 {
  font-family: var(--font-mono); font-size: var(--t-label); font-weight: 500;
  letter-spacing: var(--track-max); text-transform: uppercase; color: var(--dim);
}
.sec-head .more {
  margin-left: auto; font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--muted); text-decoration: none;
  border-bottom: 1px solid var(--etch); padding-bottom: 2px;
  transition: color var(--swift), border-color var(--swift);
}
.sec-head .more:hover { color: var(--accent); border-color: var(--etch-warm); }

/* ── roster cards carrying artwork ─────────────────────────────────────────
   The card uses that artist's top cover. ⛔ An artist with NO artwork at all
   (MarsDeck has seven releases and none) gets the drawn plate — never a
   borrowed cover from somebody else, which would be an invented fact that
   looks deliberate. */
.roster-card.has-art { padding: 0; overflow: hidden; }
.roster-card.has-art .rc-body { padding: var(--s5) var(--s5) var(--s6); }

/* ── the catalogue band ───────────────────────────────────────────────────── */
/* ⛔ FIXED COLUMN COUNTS, NOT auto-fill.
   auto-fill with a 150px minimum gives whatever count happens to fit the
   width — which produced SIX tiles then TWO, a last row nearly empty.
   6 / 3 / 2 all divide 12, so with the item count trimmed to a multiple of 6
   (see releases_recent's caller) every row is full at every breakpoint. */
.cat-grid {
  list-style: none; display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* Tight, because the homepage grid is now pure artwork with no captions and
     a small gutter makes twelve covers read as ONE block rather than twelve
     floating squares. Stephan: "make it look more solid." */
  gap: var(--s3);
}
.cat-item { min-width: 0; }
.cat-item a { text-decoration: none; display: block; }
.cat-art {
  position: relative; aspect-ratio: 1 / 1;
  border: 1px solid var(--etch); background: var(--panel-2);
  overflow: hidden; margin-bottom: var(--s2);
  transition: border-color var(--swift);
}
.cat-item a:hover .cat-art, .cat-item a:focus-visible .cat-art { border-color: var(--etch-warm); }
.cat-art img { width: 100%; height: 100%; object-fit: cover; }
/* ⛔ THE CAPTION IS A FIXED BOX, and this is the other half of the fault.
   A two-line title made its whole grid ROW taller and left holes under every
   one-line tile beside it — Stephan: "the typo under it kind of breaks the
   layout." Two lines are reserved whether or not they are used, so no title
   can ever push a row.
   line-clamp truncates rather than wrapping to three: a cover with its title
   cut is readable, a grid that jumps is not. */
.cat-t {
  font-size: var(--t-sm); font-weight: 600; line-height: 1.3; color: var(--text);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  min-height: calc(2 * 1.3em);
}
.cat-a {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track); text-transform: uppercase; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

@media (max-width: 520px) {
  .hero { padding-block: var(--s7) var(--s6); }
  .cat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4) var(--s3); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE BAND — cinematic, full-bleed, and NOT A CARD.

   Stephan, 2026-09-17: "I don't want everything boxed into cards… more
   cinematic." He is right, and the reason is worth naming: the bordered panel
   is ETCH's OPS-DASHBOARD habit, which is correct for an instrument and wrong
   for a record label. A label sells the artwork; a border around artwork is a
   frame saying "this is a data field".

   ⛔ THE DRAWN LAWS STILL HOLD — they just never applied here. "Flat lines,
   draw it don't download it" governs the INSTRUMENT furniture: the background,
   the hairlines, the EDL mark. Cover art and press photography are the
   product. Those go big and bleed off the edge.
   ═══════════════════════════════════════════════════════════════════════════ */

.band {
  position: relative;
  display: flex;
  align-items: flex-end;
  /* A full screen each: you scroll INTO an artist and their name
     arrives at the bottom of the frame, rather than meeting a name
     and a photograph at once. */
  min-height: 100svh;
  overflow: hidden;
  isolation: isolate;
}

.band-art { position: absolute; inset: 0; z-index: -1; }
.band-art img { width: 100%; height: 100%; object-fit: cover; object-position: center 28%; }

/* ⛔ THE LEGIBILITY LIVES IN THIS FLAT GRADIENT, never in a filter.
   A backdrop-filter that is unsupported degrades to NOTHING and the type lands
   unreadable on a bright photo; an unsupported flat layer still darkens. It is
   also one paint instead of a full-screen blur pass per frame — the trade that
   took eight filter passes off Mars Deck.
   Dark at the bottom where the type sits, and dark again at the very top so the
   sticky bar stays readable over a light image. */
.band-art::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    rgba(7,7,7,0.82) 0%,
    rgba(7,7,7,0.34) 22%,
    rgba(7,7,7,0.42) 48%,
    rgba(7,7,7,0.88) 82%,
    rgba(7,7,7,0.97) 100%);
}

/* A hairline of ember along the bottom edge — the one piece of instrument
   language allowed into the photograph, and it is a boundary, which is what a
   line honestly is. */
.band::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: linear-gradient(to right, transparent, var(--etch-warm) 22%, var(--etch-warm) 78%, transparent);
}

.band-body { padding-block: var(--s8) var(--s8); max-width: 50rem; }
/* Rhythm without ragged type: the COLUMN moves, the text stays left-aligned.
   Right-aligning a paragraph to alternate would cost more than it buys. */
.band.flip .band-body { margin-left: auto; }
.band.flip .band-art img { object-position: center 24%; }

/* ⛔ WHEN TWO SUBJECTS SIT AT OPPOSITE ENDS, A PHONE KEEPS NEITHER.
   .band-art is object-fit: cover, so the band is never really 16:9 - it is
   whatever shape the viewport is, and a 390px phone shows roughly a QUARTER
   of the width, taken from the middle. On /about/ the two people stand at the
   far ends of the frame with a monitor between them, so that centred quarter
   held a window, a plant and nobody at all: a hero about the people, with no
   people in it, on the device most visitors use.
   ⚠️ The fix is NOT a tighter crop - that would throw away the wide
   composition which is the whole point of the picture on a desktop. It is to
   tell the narrow crop WHICH subject to keep.
   Measured at 390x844 and at 1440x900 before and after: the phone gains his
   face, and the desktop frame moves 48px and loses nobody. */
.band.subject-left .band-art img { object-position: 20% 28%; }

.band-kicker {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-max); text-transform: uppercase;
  color: var(--accent); margin-bottom: var(--s3);
}

.band-name {
  /* ⛔ Sized down from clamp(2.8rem,10vw,8rem) after LOOKING at it. At 144px on
     a 1440 viewport "Synth Sophia" wrapped onto two lines inside the 44rem
     column, and those two lines covered a slab of the photograph - working
     directly against the reason the band exists. A name that fits on one line
     shows more of the artist, which is the whole point. */
  font-size: clamp(2.4rem, 6.6vw, 5.6rem);
  font-weight: 700; letter-spacing: -0.04em; line-height: 0.94;
  margin-bottom: var(--s4);
  /* Not a glow — a soft ground so the letterforms hold on any frame of the
     photograph underneath, including a bright one. */
  text-shadow: 0 2px 40px rgba(7,7,7,0.75);
}

/* ── THE SCRAMBLE ────────────────────────────────────────────────────
   The artist's name resolving out of noise as the band enters. The site is
   named for a signal that arrives slower than the fall finishes, so a name
   ARRIVING rather than simply being there is the site's own language.

   ⛔ THE BOX CANNOT MOVE, AND THAT IS THE WHOLE DESIGN OF THIS EFFECT.
   These names are set in IBM Plex Sans — a PROPORTIONAL face — at up to 90px.
   Cycling random glyphs through "Synth Sophia" changes the word's width on
   every frame, which at that size drags the whole band around and can reflow
   the name onto a second line mid-animation. So the real name STAYS in the
   flow and keeps sizing the box; the scrambling text is painted OVER it,
   absolutely positioned, where it cannot affect layout no matter how wide its
   glyphs happen to be. Nothing reflows. Nothing jumps.

   ⛔ THE ACCESSIBLE NAME NEVER SCRAMBLES. `.scr-ink` holds the real text and
   is only made invisible — never removed, never rewritten — so a screen
   reader announces "Synth Sophia" throughout. The painted layer is
   aria-hidden. This is the same trap that produced "7 Minutesfrom Mars".

   ⚠️ THE FAIL-SAFE DIRECTION IS DELIBERATE. The real name is in the markup
   and visible by default; the script only ever ADDS the effect. No script, no
   IntersectionObserver, an error higher up — and the name simply sits there,
   which is exactly today's behaviour. The opposite arrangement would hide
   every artist's name by default and trust JS to bring it back. */
.scr { position: relative; display: inline-block; }

/* visibility, NOT opacity or color: it keeps the element's box, so the name
   goes on reserving its exact space while the painted layer runs. */
.scr.is-scrambling > .scr-ink { visibility: hidden; }

.scr-fx {
  position: absolute; inset: 0;
  white-space: nowrap;   /* a painted layer may never wrap — it would be the
                            one thing that could still change the look */
  pointer-events: none;
}
/* resolved characters are the real ink; unresolved ones are quieter, so the
   name reads as arriving left-to-right rather than flickering as one block. */
.scr-fx .off { color: var(--muted); }
.scr-fx .on  { color: inherit; }

/* ⚠️ Reduced motion is handled in the SCRIPT, which never starts — rather
   than here, where a rule could only hide the effect after it had already
   begun rewriting text. Nothing to switch off if it never runs. */

.band-line {
  font-size: clamp(1rem, 2.3vw, var(--t-lead));
  font-weight: 300; color: var(--text); opacity: 0.9;
  max-width: 40ch; margin-bottom: var(--s5);
}

/* ── the artist paragraph ────────────────────────────────────────────
   The tagline is the HEADLINE and this is the paragraph under it — two
   different jobs, so they get two different sizes and two different colours.
   Set them alike and the band reads as one run-on sentence.

   ⚠️ max-width in `ch`, not px: a measure is a count of CHARACTERS, and a
   px width that reads well at 1440 becomes a 110-character line on a wide
   monitor, which is where people stop reading.

   ⛔ THE LENGTHS ARE UNEVEN AND THAT IS THE CONTENT, NOT A BUG. DEEPFIELD's
   paragraph is 343 characters and MarsDeck's is 95, because that is what was
   written about each of them. Clamping every band to the shortest would throw
   away real copy; padding the short ones would mean inventing some. */
.band-bio {
  font-size: var(--t-body);
  font-weight: 300; line-height: 1.65;
  color: var(--dim);
  max-width: 52ch;
  margin-bottom: var(--s5);
}
@media (max-width: 720px) { .band-bio { font-size: 0.94rem; } }

/* ── /artists/ ON A PHONE: THE NAME ALONE ───────────────────────────────
   His call, 2026-10-26, after looking at the roster on his phone: the
   tagline and the paragraph come off and the headline stays — "it keeps
   it more clean".

   ⛔ SCOPED TO .band.roster, WHICH ONLY /artists/ WRITES. The same
   component is the landing card, the artist hero and the three site
   pages, and on those .band-line is the page's LEDE — "Small on purpose"
   is the whole opening of /label/. An unscoped rule here would delete it
   on every phone and nothing would report it.

   ⚠️ display:none, NOT the clip pattern, and that is honest here rather
   than lazy: neither line is unique to this band. The tagline is printed
   in full in the cast list higher up the SAME screen, and the paragraph is
   the opening of the artist's own page, one tap away through the Enter
   link, which stays. Hiding text that exists nowhere else would be taking
   it off a phone reader for the look of it.

   ⭐ It also closes five of the six AA failures this page has carried at
   390px since it was built — the bio on all four bands and DEEPFIELD's
   tagline. ⚠️ That is a consequence, not the reason, and it fixes NOTHING
   structural: the scrim is still positioned by percentage of the band
   while the text block is sized by content, DEEPFIELD's 90px NAME still
   reads 2.47:1, and the landing cards still carry both lines. The row in
   CLAUDE.md stays open. */
@media (max-width: 720px) {
  .band.roster .band-line,
  .band.roster .band-bio { display: none; }

  /* ── THE LANDING CARDS ────────────────────────────────────────────────
     His call, 2026-10-27, browsing the live site on his phone: the artist
     paragraph comes off the front-page cards too.

     ⚠️ ONLY THE PARAGRAPH HERE, WHERE THE ROSTER ALSO LOSES ITS TAGLINE.
     That is his wording, not an oversight — on /artists/ he asked for "only
     the headline", here he named "the paragraph of the little description
     text". The two pages do different jobs: the front door introduces an
     artist in one line, the roster is a list of names.

     ⛔ `display:none` rather than the clip pattern, and the reason is the
     same one it was on the roster: this text is not unique to this card.
     It is the opening of the artist's own page, one tap away through the
     "Enter …" button, which stays. Hiding text that exists nowhere else
     would be taking it off a phone reader for the look of it.

     ⚠️ IT ALSO MOVES EVERY OTHER LINE, AND THAT IS THE POINT. The band is
     bottom-anchored and the scrim is drawn bottom-up, so removing a block
     slides the rest DOWN into the 0.88-alpha zone. On the roster that took
     DEEPFIELD's 90px name from 2.47:1 to 13.32:1 with no declaration
     touching it. Measured here too — never assumed. */
  .band.landing .band-bio { display: none; }
}

/* ── the hero paragraph, artist page ─────────────────────────────────────
   Name, tagline, this. One introduction rather than three announcements.

   ⛔ VERTICAL ROOM IS THE WHOLE CONSTRAINT HERE, AND IT IS THE ONLY PLACE ON
   THIS SITE WHERE IT IS SCARCE. The band is `min-height: 100svh` with its
   contents bottom-aligned, so every line this paragraph gains pushes the name
   UP toward the top of the frame and the links DOWN toward the fold. On a 13"
   laptop there is no slack at all. Hence a measure of 46ch, not the 52-62ch
   used in the calmer blocks further down the page: here a narrower column is
   not about reading comfort, it is about how many LINES the block becomes.

   ⚠️ Which is also why the copy itself is kept to two or three sentences.
   That is a layout constraint expressed as an editing rule, and it belongs
   written down next to the thing it constrains — the next person to paste six
   beautiful sentences in here will wonder why the name slid off the top.

   ⛔ The same soft ground `.band-name` carries. This sits on a press
   photograph that changes with every artist, so nothing about the contrast can
   be assumed from the page's own colours. */
.band-story {
  font-size: clamp(0.95rem, 1.35vw, 1.05rem);
  font-weight: 300; line-height: 1.7;
  color: var(--text); opacity: 0.82;
  max-width: 46ch;
  margin-bottom: var(--s5);
  text-shadow: 0 1px 18px rgba(7,7,7,0.9);
}
@media (max-width: 720px) {
  .band-story { font-size: 0.92rem; line-height: 1.65; max-width: 100%; }
}

.band-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3) var(--s5);
  margin-bottom: var(--s5);
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--dim);
}
.band-meta .lit { color: var(--accent); }
/* The release state, beside the count and louder than it. Bright rather than
   ember: this line lies on a press photograph, where the ember at this size
   was measured failing AA, and it carries its own hairline so it reads as a
   mark rather than as more of the same sentence. */
.band-meta .soon {
  color: var(--text);
  border: 1px solid var(--etch-warm);
  padding: 3px var(--s2);
  text-shadow: 0 1px 14px rgba(7, 7, 7, 0.9);
}

/* ⛔ An artist with NO artwork at all gets type on the bare ground — never a
   borrowed photograph. MarsDeck has seven releases and no art; filling that
   hole with somebody else's image is an invented fact that looks deliberate. */
.band.no-art { min-height: 46svh; border-top: 1px solid var(--etch); }
.band.no-art .band-body { padding-block: var(--s7); }

/* ═══════════════════════════════════════════════════════════════════════════
   DE-BOXING — the panels come off the artwork.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The catalogue strip: the cover IS the object. No frame around it. */
.cat-art { border: 0; background: transparent; }
.cat-item a:hover .cat-art, .cat-item a:focus-visible .cat-art { border: 0; }
.cat-art img { transition: transform var(--glide), opacity var(--glide); opacity: 0.88; }
.cat-item a:hover .cat-art img, .cat-item a:focus-visible .cat-art img { opacity: 1; transform: scale(1.04); }

/* The artist page's lead release: no border, no panel, more air. The cover
   carries it. */
/* ⛔ A SHARED EDGE, NOT A CENTRE. Stephan: "it looks a little bit uneven —
   I'm not sure if maybe we are centering this." He was right and the centring
   was the cause: a ~150px text block centred against a 420px cover touches
   NEITHER edge and hangs in the middle of the column, so nothing on the right
   lines up with anything on the left.

   Bottom-aligned instead, so the last line of type sits on the cover's base
   line. The leftover space collects at the TOP, where it reads as air above a
   title rather than as a gap around floating text.

   ⚠️ The honest cause underneath: the right column is thin because the DATA
   is thin - a title and a type stamp, with no date, no notes and no links yet.
   Layout can make that read as deliberate; it cannot make it read as full. */
.feature {
  border: 0;
  background: transparent;
  padding: 0;
  gap: var(--s8);
  align-items: end;
}

/* The cover comes down from 420 so the two columns are closer in weight. At
   420 against a four-line text block the left side simply outweighed the
   right, whatever the alignment. */
.feature { grid-template-columns: minmax(0, 360px) minmax(0, 1fr); }

/* ⚠️ One column when there is no artwork, so the title block is the
   composition instead of sitting beside an empty square. */
.feature.no-cover { grid-template-columns: minmax(0, 1fr); }

/* The label moves INTO the column. It was a separate section heading competing
   with the release title for the same job, and moving it here gives the right
   side four elements to stack instead of two. */
.feature-meta .kicker { margin-bottom: var(--s3); }
.feature-cover { border: 0; background: transparent; }

@media (max-width: 860px) {
  .band { min-height: 62svh; }
  .band-body { padding-block: var(--s7); }
  /* ⛔ THE STACK LIVES HERE, NOT IN THE `NARROW` BLOCK 450 LINES ABOVE.
     That one says `grid-template-columns: 1fr` at this same width and has
     been DEAD since this section was written: `.feature` is re-declared
     unconditionally below it, same specificity, later in the file, so the
     two-column rule won at every width. Reported 2026-10-26 — on a 390px
     phone the cover took 334px of a 358px row and `.feature-meta`
     measured **0px wide**, so the title, the stamp and the Spotify button
     were painted outside their own column and off the side of the screen.
     ⚠️ Moving it rather than raising its specificity: a `.feature.feature`
     or an !important would have fixed the symptom and left the next
     responsive rule in that block to die the same quiet death. */
  .feature { gap: var(--s5); grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 520px) {
  .band { min-height: 56svh; }
  .band-name { line-height: 0.95; }
}

/* The artists not on the front page. ⛔ Named, not hidden — the homepage shows
   two by Stephan's call, and anyone left off is one honest line away rather
   than a roster you have to discover. */
.rest-line {
  font-size: var(--t-sm); color: var(--muted); line-height: 2;
  border-top: 1px solid var(--etch); padding-top: var(--s5);
}
.rest-line a { color: var(--dim); text-decoration: none; border-bottom: 1px solid var(--etch); transition: color var(--swift), border-color var(--swift); }
.rest-line a:hover { color: var(--accent); border-color: var(--etch-warm); }
.rest-line .rest-all {
  display: inline-block; margin-left: var(--s3);
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
}

/* The counter beside a section heading — "5 more". It had NO rule of its own:
   it was only ever styled as a descendant of .roster-card, which the cinematic
   pass deleted. So it was rendering at full body size in the middle of a
   monospace label row. Found by auditing declared-vs-used, not by looking. */
.sec-head .count {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE MASTHEAD — a section page's title card.

   Stephan, 2026-09-17: the /artists/ page opened with a small "Roster /
   Artists" line while the homepage opened with the wordmark over the falling
   traces, so the section page read as a lesser page. It should open like a
   title card: the label's name very large, the drawn EDL mark under it, and
   the animated sky given enough height to actually be seen.

   ⛔ THE WORDMARK IS NOT THE <h1>. On this page the heading must say "Artists"
   or the page announces itself as the homepage to Google and to every screen
   reader. The h1 is the small ember label; the wordmark is a <p> rendered
   enormous. Visual hierarchy and semantic hierarchy are allowed to disagree,
   and this is the case where they should.
   ═══════════════════════════════════════════════════════════════════════════ */

.masthead {
  /* Tall enough that the descent traces have room to fall through it — the
     animation is the point, and a short block would crop it to a flicker. */
  min-height: 74svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: var(--s9) var(--s8);
}


.wordmark {
  font-size: clamp(3rem, 13vw, 8.5rem);
  font-weight: 700; letter-spacing: -0.045em; line-height: 0.9;
  color: var(--text);
  margin-bottom: var(--s6);
}
.wordmark .seven { color: var(--accent); }
.wordmark .line2 { display: block; }

.masthead .hero-mark { margin-bottom: var(--s6); }
/* ⛔ THE COUNT IS THE TITLE CARD FOR THE CAST LIST BELOW IT, not a stray
   sentence. As body prose under a drawn rule it read as a caption nobody
   had placed; as a tracked mono label it reads as the heading of the
   roster that follows — which is what it has always actually been.
   ⚠️ Scoped to .masthead on purpose: .hero-lede also carries the "that
   address does not match anyone on the roster" line on a missing artist
   page, and shouting that in uppercase mono would be wrong. */
.masthead .hero-lede {
  margin-bottom: 0;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--dim);
}

/* ⛔ VISUALLY HIDDEN, NOT DISPLAY:NONE. Stephan asked for the "Artists" label
   above the wordmark to go, but the page still needs an <h1> that says what it
   is - without one, /artists/ announces itself to crawlers and screen readers
   as a second homepage. display:none and visibility:hidden remove it from the
   accessibility tree too, which would defeat the whole point. This is the
   standard clip pattern: present, announced, not drawn.
   Not cloaking: it is the same word the page is about and the same word in the
   <title>. It is simply not part of the composition. */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* With the label gone the wordmark is the first thing in the block, so it
   carries the top of the masthead itself. */
.masthead .wordmark { margin-top: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE WORDMARK IS A DOOR

   On /artists/ the wordmark is a <p>, not the <h1> — the h1 is the hidden
   word "Artists", so the page does not announce itself as a second homepage.
   That stays true; a link changes where it GOES, not what it IS.

   ⛔ display:block, because .line2 inside it is block-level. An inline <a>
   wrapping a block child is legal, but it generates two line boxes and the
   8.5rem wordmark would reflow. Measured before and after: the box does not
   move.
   ══════════════════════════════════════════════════════════════════════════ */
.wordmark a {
  display: block;
  color: inherit;
  text-decoration: none;
  transition: color var(--swift);
}
.wordmark a:hover { color: var(--accent-lift); }
.wordmark a:hover .seven { color: var(--text); }
.wordmark a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: var(--s3);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE ROSTER INDEX — the cast list

   His ask, 2026-10-26: the line under the wordmark should feel like "okay,
   THESE are the artists", with more layout and a little cinema. The page had
   a small grey sentence and then dropped straight into a full-bleed
   photograph, so the roster began without ever announcing itself.

   This is a title-sequence cast list: a number, a name, the artist's own
   tagline, a rule between each. It is DRAWN — type, hairlines and one arrow,
   no new image and no decoration that claims anything.

   ⛔ EVERY FIELD IS REAL. The number is the artist's position in the roster,
   the name and the tagline are theirs, and an artist with no tagline simply
   renders none. Nothing here is written to fill a row.
   ⛔ AND IT IS NAVIGATION, NOT ORNAMENT. Each row jumps to that artist's band
   further down a page that is four full screens tall — [id] already carries
   scroll-margin-top, so a jump lands clear of the sticky bar.
   ══════════════════════════════════════════════════════════════════════════ */
.roster-index {
  /* ⚠️ No top margin: the masthead already ends on var(--s8) of padding,
     and stacking both put a hole between the wordmark and its own cast
     list. The space below is the one that matters — it is the beat before
     the first photograph. */
  margin-bottom: var(--s9);
  border-top: 1px solid var(--etch);
}

.ri-row {
  display: grid;
  align-items: baseline;
  column-gap: var(--s5);
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    "num name go"
    "num line go";
  padding-block: var(--s5);
  border-bottom: 1px solid var(--etch);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--swift), background-color var(--swift);
}

/* ⚠️ The tagline sits BESIDE the name only where there is room for both to
   stay readable. Below this it stacks, which is why the grid is declared in
   areas rather than as a fixed row of columns — the structure does not
   change, only where one cell sits. */
@media (min-width: 760px) {
  .ri-row {
    grid-template-columns: auto minmax(0, auto) minmax(0, 1fr) auto;
    grid-template-areas: "num name line go";
  }
}

.ri-num {
  grid-area: num;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: var(--track-wide);
  /* ⛔ --dim, NOT --muted. The number is CONTENT — it is the artist’s place
     on the roster — and --muted on this ground computes 3.18:1 against a
     4.5 requirement for 12px text. It failed, measured, before it shipped.
     --dim reads 7.00:1 and still sits a clear step below the name. */
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  transition: color var(--swift);
}

.ri-name {
  grid-area: name;
  font-size: var(--t-h3);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--text);
}

.ri-line {
  grid-area: line;
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--dim);
  margin-top: var(--s1);
}
@media (min-width: 760px) {
  .ri-line { margin-top: 0; text-align: right; }
}

/* ⛔ DECORATION, SO IT FAILS QUIET. aria-hidden, and it carries no state: it
   does not say whether the artist has music out, because this row cannot
   know that without repeating a claim the band below already makes. */
.ri-go {
  grid-area: go;
  font-family: var(--font-mono);
  /* Decoration, and aria-hidden, so 3:1 would be the bar it has to clear —
     but it is still a drawn glyph, and matching --dim costs nothing and
     ends the argument. */
  color: var(--dim);
  transition: transform var(--swift), color var(--swift);
}

.ri-row:hover,
.ri-row:focus-visible {
  border-bottom-color: var(--etch-warm);
  background-color: var(--etch-soft);
}
.ri-row:hover .ri-num,
.ri-row:focus-visible .ri-num,
.ri-row:hover .ri-go,
.ri-row:focus-visible .ri-go { color: var(--accent); }
.ri-row:hover .ri-go,
.ri-row:focus-visible .ri-go { transform: translateX(var(--s2)); }

.ri-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: calc(var(--s1) * -1);
}

/* ⛔ THE MOVEMENT IS THE ONLY THING THAT GOES. Colour and the hairline stay,
   because they are how the row says it is interactive — removing those with
   the motion would leave a reduced-motion visitor with no hover feedback at
   all. */
@media (prefers-reduced-motion: reduce) {
  .ri-row, .ri-num, .ri-go { transition: none; }
  .ri-row:hover .ri-go, .ri-row:focus-visible .ri-go { transform: none; }
}

/* ⛔ A .band-index CHAPTER NUMBER LIVED HERE ON 2026-10-26 AND WAS REMOVED.
   It sat above the artist name — the least protected part of the band,
   because the scrim is drawn bottom-up — and across the four roster press
   shots it measured 4.33:1 at label size, then 1.39 / 1.77 / 2.98 at h3.
   It failed on two or three of four whichever way it was drawn, and every
   new photograph re-rolls it. The roster index numbers each artist on
   solid ground instead, where legibility is construction and not luck.
   ⚠️ tools/check-band-contrast.js still names .band-index, deliberately:
   if anyone rebuilds it, it is measured from the first run. */


/* ═══════════════════════════════════════════════════════════════════════════
   NEVER THE WORDMARK TWICE ON ONE SCREEN

   Stephan, 2026-09-17: "you have 7 Minutes from Mars in the navigation, and
   then you have 7 Minutes from Mars under it again."

   The fix is not to delete one. The bar's brand earns its place once you are
   deep in the page and the big wordmark is long gone — it just must not sit
   above its own twin. So it hides while a wordmark is on screen and fades in
   after.

   ⛔ THE DEFAULT IS VISIBLE, AND THAT DIRECTION IS DELIBERATE. The script adds
   the hiding class; if the script never runs the brand simply stays where it
   is. The opposite arrangement — hidden in CSS, revealed by JS — loses the
   site's name entirely the moment anything goes wrong, and a masthead that
   vanishes on a slow connection is a fault, not a degradation.
   ═══════════════════════════════════════════════════════════════════════════ */

.topbar .brand { transition: opacity var(--glide), transform var(--glide); }

.topbar.at-wordmark .brand {
  opacity: 0;
  transform: translateY(-4px);
  /* Still laid out, so the nav on the right does not jump sideways when the
     brand leaves — a nav that slides is a bigger distraction than a logo. */
  pointer-events: none;
}

/* ── the scroll cue ──────────────────────────────────────────────────
   A 100svh title card with nothing visible under it reads as the whole page.
   One hairline, one ember head and a label — the same language as the descent
   traces behind it, because a falling line is already what this site means.

   ⛔ IT SAYS "BEGIN DESCENT", NOT "SCROLL TO LISTEN". There is nothing to
   listen to: no audio is hosted and every streaming slot is still empty. A cue
   promising a thing the next screen cannot deliver is the same broken promise
   as a grey button you can press. The site is named for a descent, so the cue
   names the descent — true today, and still true the day the links land.

   ⛔ Decoration only: it claims no state, counts nothing, and is aria-hidden.
   It does not report how far down the page you are, because it cannot know
   that at the moment it is drawn. */
.scroll-cue {
  position: absolute;
  right: var(--s5); bottom: var(--s7);
  display: flex; flex-direction: column; align-items: center; gap: var(--s3);
  pointer-events: none;
}

.scroll-cue-label {
  /* ⚠️ Vertical, reading DOWNWARD, because it labels a downward movement and
     an instrument annotation runs along the axis it describes.

     ⛔ `writing-mode: vertical-rl` ALONE does that. This shipped with a
     `transform: rotate(180deg)` as well and a comment claiming the pair gave
     top-to-bottom — both wrong. vertical-rl is already top-to-bottom; the
     extra half-turn flipped it to bottom-to-top, so the cue for scrolling DOWN
     read UP. Caught by photographing the element on its own and actually
     looking at it, which is the only way this class of fault is ever found. */
  writing-mode: vertical-rl;
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  /* ⚠️ --dim, NOT --muted. Shipped as --muted first and MEASURED at 2.84:1
     against this ground — which fails AA for normal text and fails the
     large-text threshold too, at 11px. --dim is 5.85:1. A cue nobody can read
     is not restraint, it is a decoration pretending to be an affordance. */
  color: var(--dim);
  white-space: nowrap;
}

.scroll-cue-line {
  position: relative;
  width: 1px; height: 46px;
  background: linear-gradient(to bottom, transparent, var(--etch-warm));
  overflow: visible;
}
.scroll-cue-line::after {
  content: '';
  position: absolute; left: -1.5px; bottom: -2px;
  width: 4px; height: 4px; border-radius: 50%;
  background: var(--accent);
  animation: cue-fall 2.6s ease-in-out infinite;
}
@keyframes cue-fall {
  0%, 100% { transform: translateY(-30px); opacity: 0; }
  40%      { opacity: 1; }
  70%      { transform: translateY(0);     opacity: 1; }
  85%      { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .scroll-cue-line::after { animation: none; transform: none; opacity: 1; }
}
/* ⚠️ On a phone the label is the first thing to go, not the cue. The falling
   head still says "there is more"; the words are the affordable loss. */
@media (max-width: 720px) {
  .scroll-cue { right: var(--s4); bottom: var(--s6); }
  .scroll-cue-label { display: none; }
}

/* ── the closing statement ─────────────────────────────────────────────────
   What used to sit under the wordmark on the first screen. It reads better
   here: the page opens with the name and closes with what the name means. */
.statement {
  border-top: 1px solid var(--etch);
  padding-block: var(--s8);
}

/* ══ THE TEXT PAGES — /label/ and /about/ ═════════════════════════════════

   ⛔ A HEADLINE IS NOT A LABEL, AND THIS SITE ALREADY HAD ONLY LABELS.
   `.sec-head h2` is 12px mono, uppercase, wide-tracked — exactly right over
   "Archive" or "The roster", and wrong over a sentence like "It started on
   machines that fought back", which would be shouted one letter at a time.
   So the prose pages get a real heading: body face, sentence case, sized to be
   read rather than scanned. Two jobs, two classes; setting them alike is how a
   page ends up with no hierarchy at all. */
.sec-title {
  font-size: clamp(1.35rem, 2.6vw, 1.9rem);
  font-weight: 600; letter-spacing: -0.02em; line-height: 1.2;
  color: var(--text);
  margin-bottom: var(--s5);
  max-width: 24ch;
}

/* ⚠️ 62ch, and it is the widest measure on the site on purpose. These pages
   are the only ones somebody reads straight through rather than scanning, and
   they sit on the flat ground rather than over a photograph — so the column
   can afford what the hero paragraph could not. */
.prose { max-width: 62ch; }
.prose p {
  font-size: var(--t-body);
  font-weight: 300; line-height: 1.75; color: var(--dim);
}
.prose p + p { margin-top: var(--s5); }
.prose strong { color: var(--text); font-weight: 500; }
.prose a { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.prose a:hover, .prose a:focus-visible { color: var(--accent); }

/* The opening line carries more weight than the paragraphs under it — it is
   the sentence somebody reads if they read nothing else. */
.prose-lead {
  font-size: clamp(1.05rem, 2.1vw, 1.3rem) !important;
  font-weight: 300 !important;
  line-height: 1.55 !important;
  color: var(--text) !important;
  margin-bottom: var(--s5);
}
.prose-more { margin-top: var(--s6); }

/* ── the roster list ─────────────────────────────────────────────────
   Rows, not cards. The same call the archive made: a register reads DOWN a
   column, and five names in five boxes is five times the chrome for the same
   five names. The whole row is the target, not just the name. */
.roster-list { list-style: none; border-top: 1px solid var(--etch); }
.roster-list li { border-bottom: 1px solid var(--etch); }
.roster-list a {
  display: flex; align-items: baseline; gap: var(--s4); flex-wrap: wrap;
  padding: var(--s5) 0;
  text-decoration: none; color: var(--text);
  transition: color .2s ease, padding-left .2s ease;
}
.roster-list a:hover, .roster-list a:focus-visible {
  color: var(--accent);
  padding-left: var(--s3);
}
.roster-name { font-size: clamp(1.2rem, 2.4vw, 1.7rem); font-weight: 600; letter-spacing: -0.02em; }
.roster-line { font-size: var(--t-sm); font-weight: 300; color: var(--dim); }
.roster-list a:hover .roster-line { color: var(--dim); }

/* ── a full-bleed figure between sections ───────────────────────────────
   ⚠️ WIDTH AND HEIGHT ARE ON THE <img> ITSELF, not only here. Without them the
   browser cannot reserve the box before the bytes arrive, and every paragraph
   below jumps down the page as each photograph lands — on the two pages
   somebody is actually trying to READ. */
.lab-figure { margin-block: var(--s8); }
.lab-figure img {
  display: block; width: 100%; height: auto;
  max-height: 62svh; object-fit: cover;
  filter: saturate(0.92);
}

.statement-more { display: flex; gap: var(--s4); flex-wrap: wrap; margin-top: var(--s6); }

@media (max-width: 720px) {
  .sec-title { max-width: 100%; }
  .roster-list a { padding: var(--s4) 0; gap: var(--s2); }
  .roster-line { width: 100%; }
}
.statement p {
  font-size: clamp(1.05rem, 2.6vw, 1.6rem);
  font-weight: 300; line-height: 1.55; color: var(--dim);
  max-width: 46ch; margin-bottom: var(--s5);
}
.statement strong { color: var(--text); font-weight: 400; }
.statement .hero-count { display: block; }

/* 6 → 3 → 2. Every step divides 12, so the last row is never a stub. */
@media (max-width: 900px) { .cat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 520px) { .cat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ── THE LANDING NAVIGATION ────────────────────────────────────────────────
   The landing page carries no top bar, so its navigation sits under the
   wordmark and is part of the title card rather than a strip above it.
   Same voice as the top bar's nav — mono, uppercase, wide — so moving between
   the landing page and the rest of the site does not feel like two sites.

   ⛔ Every inheritable property that can change this component's box is
   declared on it. Referencing a host variable is one door; inheritance is the
   other, and it is silent — line-height alone once gave one shared bar two
   different heights across eight pages with byte-identical CSS. */
.hero-nav {
  display: flex; flex-wrap: wrap;
  gap: var(--s3) var(--s6);
  margin-top: var(--s6);
}
.hero-nav a {
  font-family: var(--font-mono);
  font-size: var(--t-label); font-weight: 400; font-style: normal;
  line-height: 1.2; letter-spacing: var(--track-wide); text-transform: uppercase;
  white-space: nowrap; text-decoration: none;
  color: var(--dim);
  padding-bottom: var(--s1);
  border-bottom: 1px solid transparent;
  transition: color var(--swift), border-color var(--swift);
}
.hero-nav a:hover, .hero-nav a:focus-visible {
  color: var(--accent);
  border-bottom-color: var(--etch-warm);
}

@media (max-width: 520px) {
  .hero-nav { gap: var(--s3) var(--s5); margin-top: var(--s5); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   LISTEN — the streaming row under a featured release.

   ⛔ IT RENDERS NOTHING WHEN THERE ARE NO LINKS, and that is deliberate. Every
   release in this database currently has zero, so a button drawn here today
   would be a dead end — and a CTA that goes nowhere is worse than no CTA. The
   moment a URL is pasted in the admin the button appears; nothing else has to
   change.
   ═══════════════════════════════════════════════════════════════════════════ */

.listen { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s5); }

/* ⚠️ BOTH ELEMENTS, not just the anchor. A placeholder slot is a <span> so
   it cannot be clicked into nothing - which meant it inherited none of the
   button box and rendered as bare grey text where a button was asked for.
   The shape belongs to the SLOT, not to whether it happens to be a link. */
.listen a, .listen span,
.artist-links a, .artist-links span {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-family: var(--font-mono);
  font-size: var(--t-label); font-weight: 500; font-style: normal;
  line-height: 1.2; letter-spacing: var(--track-wide); text-transform: uppercase;
  white-space: nowrap; text-decoration: none;
  color: var(--text);
  border: 1px solid var(--etch-warm);
  padding: var(--s3) var(--s5);
  transition: color var(--swift), border-color var(--swift), background-color var(--swift);
}
.listen a:hover, .listen a:focus-visible,
.artist-links a:hover, .artist-links a:focus-visible {
  color: var(--accent-lift);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
/* The service mark: drawn, not an icon font and not a downloaded logo. */
.listen a::before, .listen span::before,
.artist-links a::before, .artist-links span::before {
  content: ''; width: 5px; height: 5px; flex: none;
  background: var(--accent); transform: rotate(45deg);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE ARCHIVE — a LIST, not a grid.

   ⛔ Stephan: "the other songs below should be more like an archive, I don't
   like when they break the line." A grid can always leave a ragged last row —
   five releases in four columns is four then one — and no column count fixes
   it because the catalogue length is whatever it is. A list cannot break: each
   release is one full-width row, so the shape is the same at five items or
   fifty, at 1440px or 390px.
   ═══════════════════════════════════════════════════════════════════════════ */

.archive-list { list-style: none; border-top: 1px solid var(--etch); }

.arc-row {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s4);
  padding-block: var(--s3);
  border-bottom: 1px solid var(--etch);
  transition: background-color var(--swift);
}
.arc-row:hover { background: color-mix(in srgb, var(--panel) 55%, transparent); }

.arc-art {
  width: 56px; height: 56px;
  background: var(--panel-2);
  overflow: hidden; position: relative;
}
.arc-art img { width: 100%; height: 100%; object-fit: cover; }
/* An artist with no artwork gets a drawn plate, never a borrowed cover. */
.arc-art .cover-empty span { display: none; }

.arc-title {
  font-size: var(--t-base); font-weight: 600; line-height: 1.25; color: var(--text);
  /* Clamped rather than wrapped forever: lengths here are unpredictable and a
     row that grows to three lines reintroduces exactly the raggedness the list
     was built to remove. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.arc-sub {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track); text-transform: uppercase; color: var(--muted);
  margin-top: 2px;
}
.arc-links { display: flex; flex-wrap: wrap; gap: var(--s3); justify-content: flex-end; }
.arc-links a {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--dim); text-decoration: none; white-space: nowrap;
  border-bottom: 1px solid var(--etch);
  transition: color var(--swift), border-color var(--swift);
}
.arc-links a:hover { color: var(--accent); border-color: var(--etch-warm); }

@media (max-width: 560px) {
  /* The links drop under the title rather than squeezing the row. */
  .arc-row { grid-template-columns: 48px minmax(0, 1fr); gap: var(--s3); }
  .arc-art { width: 48px; height: 48px; }
  .arc-links { grid-column: 2; justify-content: flex-start; margin-top: var(--s2); }
}

/* ⛔ NO TRAILING MARGIN ON THE LAST THING IN THE COLUMN. With the block
   bottom-aligned to the cover, a margin-bottom on whatever happens to be last
   pushes the whole stack up by that much and the shared baseline is quietly
   off. Today that is .stamp-row's 24px, because no release has links yet; when
   links exist the .listen row becomes last and the same rule covers it.
   Same family as the empty stamp-row that spent its margin on nothing - a gap
   holding nothing reads as a rendering fault. */
.feature-meta > *:last-child { margin-bottom: 0; }

/* ── LISTEN SLOTS: lit when live, quiet when waiting ───────────────────────
   ⛔ A slot with no URL renders as a SPAN, never an <a href="#">. The box is
   identical so the layout reads the same, but nothing can be clicked into a
   void — and the colour difference means one glance down a discography says
   which releases still need a link. Lit = real. Quiet = owed. */
.listen .pending,
.artist-links .pending,
.arc-links .pending {
  color: var(--muted);
  border-color: var(--etch);
  cursor: default;
}
.listen .pending::before,
.artist-links .pending::before { background: var(--muted); }

/* The archive's right column: same two services, smaller. */
.arc-links .btn-sm {
  display: inline-flex; align-items: center; gap: var(--s1);
  font-family: var(--font-mono);
  font-size: var(--t-micro); font-weight: 400; font-style: normal;
  line-height: 1.2; letter-spacing: var(--track-wide); text-transform: uppercase;
  white-space: nowrap; text-decoration: none;
  border: 1px solid var(--etch-warm);
  border-bottom: 1px solid var(--etch-warm);
  padding: 5px var(--s3);
  color: var(--text);
  transition: color var(--swift), border-color var(--swift), background-color var(--swift);
}
.arc-links a.btn-sm:hover, .arc-links a.btn-sm:focus-visible {
  color: var(--accent-lift); border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.arc-links .btn-sm.pending { color: var(--muted); border-color: var(--etch); }

/* The featured title's two lines. Split at its balance point in PHP rather
   than left to the column width — a wrap that depends on the viewport gives a
   different break at every size, and this one is a composition. */
.feature-meta h2 .tl { display: block; }

/* The link row inside the hero band. Slightly tighter than the same row on a
   plain ground: five buttons sit under the name, and over a photograph they
   need to read as one line rather than a scattered pile. */
.band-body .artist-links { margin-top: var(--s5); gap: var(--s2); }
.band-body .artist-links a,
.band-body .artist-links span { padding: var(--s2) var(--s4); }
/* ⚠️ A shade more ground behind them than on a flat section — the band's scrim
   is a gradient, so a button can land on a bright part of the frame. */
.band-body .artist-links a { background: color-mix(in srgb, var(--bg) 45%, transparent); }
.band-body .artist-links .pending { background: color-mix(in srgb, var(--bg) 35%, transparent); }

/* ⛔ THE FIRST BAND ON A PAGE THAT HAS A BAR MUST SUBTRACT IT.
   An artist page opens straight onto a band, and the sticky bar sits above it
   in flow - so 100svh made the page 50px taller than the screen and pushed the
   link row to within 14px of the fold. Scoped to `main > .band:first-child`,
   which is true on an artist page and false on the homepage (the hero is
   first) and on /artists/ (the masthead is first), so neither is touched. */
/* ⛔ THE HERO GETS THE WHOLE SCREEN BACK. It used to be 100svh MINUS the bar,
   because the bar sat above it in flow and the two together had to make one
   screen. The bar floats now, so the subtraction would leave the hero short by
   exactly its height. */
main > .band:first-child { min-height: 100svh; }

/* ⚠️ AND EVERYTHING ELSE HAS TO CLEAR IT. The padding is the DEFAULT and the
   exemption is the special case, deliberately: a page that opens with ordinary
   content and forgets to think about the bar ends up with too much space at the
   top, which is untidy. The other way round it ends up with its first heading
   hidden underneath, which is a bug nobody notices until somebody screenshots
   it. `has-bar` keeps all of this off the landing page, which has no bar. */
html.has-bar main { padding-top: calc(var(--topbar-h) + env(safe-area-inset-top, 0px)); }

/* A page that OPENS on a full-bleed hero wants that hero under the bar. */
html.has-bar main:has(> .band:first-child),
html.has-bar main:has(> .masthead:first-child) { padding-top: 0; }

/* ⚠️ :has() is the one modern selector here, and its failure mode is the safe
   one: an engine without it keeps the padding, the hero starts below the bar,
   and the page looks like it did yesterday. Nothing breaks, nothing hides. */

/* ⛔ A FIXED BAR EATS ANCHOR TARGETS. LATEST RELEASE jumps to #feat-title,
   and the two new cues jump to #what and #started — all of which would land
   underneath the bar without this. Harmless on anything never scrolled to. */
[id] { scroll-margin-top: calc(var(--topbar-h) + var(--s4) + env(safe-area-inset-top, 0px)); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE MARK — the 7 as a descent trajectory, falling to a planet's limb.

   The numeral 7 is already the shape of an EDL profile: a horizontal entry
   stroke, then a steep fall. So the mark is the name and the story in one
   glyph, and it is the same path whether it is a 16px favicon or a 620px
   background.

   ⛔ IT CLAIMS NOTHING. It draws, it lands, it fades, it repeats — no counter,
   no readout, no numerals. Decoration may not imply a state it cannot know,
   and a mark that looked like telemetry would be pretending to measure
   something.
   ═══════════════════════════════════════════════════════════════════════════ */

.mark { display: block; overflow: visible; }

/* ⛔ NO vector-effect: non-scaling-stroke, AND THAT IS A BUG FIX.
   With it, the dash pattern was computed against the SCALED path rather than
   the normalised pathLength, so the trajectory rendered as two disconnected
   segments with a gap through the middle - a 7 with its corner missing. Found
   by looking at it; nothing errors and the markup is correct.
   It is also the right call for a logo: a mark's strokes should scale WITH the
   mark, not stay pinned to device pixels. Widths are in user units against the
   200 viewBox, so at 480px they render 2.4x thicker than the number. */
.mark-limb {
  stroke: var(--etch);
  stroke-width: 0.9;
}
.mark-seven {
  stroke: var(--accent);
  stroke-width: 1.4;
  stroke-linecap: square;
  stroke-linejoin: miter;
  /* pathLength is 100, so the dash maths is percentages and stays correct if
     the geometry is ever redrawn. */
  stroke-dasharray: 100;
}
.mark-touch { fill: var(--accent); }

/* ── the background instance on the landing page ───────────────────────────
   Right of the wordmark, which is where the composition leaves space. Sits
   behind the type inside .shell's stacking context, so it never covers a word
   and never catches a tap. */
.hero .mark {
  position: absolute;
  right: 2%;
  top: 46%;
  transform: translateY(-50%);
  width: min(46vw, 480px);
  height: auto;
  z-index: -1;
  pointer-events: none;
  opacity: 1;
}

/* ⚠️ The first pass was drawn in --etch-soft and --etch-warm and simply did
   not read at this size - a mark nobody can see is not a mark. Strong enough to
   be a shape, quiet enough to stay behind the wordmark. */
.hero .mark-limb  { stroke: var(--etch-warm); }
.hero .mark-seven { stroke: var(--accent); opacity: 0.6; }
.hero .mark-touch { fill: var(--accent); }

/* ── the fall ──────────────────────────────────────────────────────────────
   One slow loop: the trajectory draws in, holds landed, then clears. Entry is
   quick and the descent is long, which is the shape of the real thing. */
.hero .mark-seven { animation: mark-fall 14s cubic-bezier(0.4, 0, 0.5, 1) infinite; }
.hero .mark-touch { animation: mark-land 14s ease-out infinite; }

@keyframes mark-fall {
  0%             { stroke-dashoffset: 100; opacity: 0; }
  4%             { opacity: 1; }
  46%            { stroke-dashoffset: 0;   opacity: 1; }
  86%            { stroke-dashoffset: 0;   opacity: 1; }
  96%, 100%      { stroke-dashoffset: 0;   opacity: 0; }
}
@keyframes mark-land {
  0%, 44%        { opacity: 0; }
  50%            { opacity: 0.9; }
  86%            { opacity: 0.8; }
  96%, 100%      { opacity: 0; }
}

/* ⛔ REDUCED MOTION KEEPS THE MARK, LOSES THE MOVEMENT. The drawing is part of
   the composition; only the drawing OF it is motion. Landed and still. */
@media (prefers-reduced-motion: reduce) {
  .hero .mark-seven { animation: none; stroke-dashoffset: 0; opacity: 1; }
  .hero .mark-touch { animation: none; opacity: 0.8; }
}

@media (max-width: 860px) {
  /* Behind the type on a phone, quieter, so it never competes with the name. */
  .hero .mark { right: -18%; width: 88vw; opacity: 0.5; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE SITE HEROES ON A PHONE — SHORTER, SO MORE OF THE PICTURE SURVIVES

   His call, 2026-10-26, looking at /about/ /label/ /contact/ on his phone:
   "adjust the images a little bit more… maybe give the hero less height so we
   can see more of the image."

   ⛔ THAT READS BACKWARDS AND IT IS RIGHT. `.band-art img` is
   `object-fit: cover`, so the band is not a 16:9 slot — it is whatever shape
   the viewport is, and the image is scaled to cover the SHORT side. At 390 x
   844 that means a 1600x900 photograph is blown up until it is 844 tall, by
   which point it is 1500 wide and the phone is showing 26% OF ITS WIDTH. Take
   height away and the scale drops, so MORE of the frame fits across.

   Measured at 390: 844 tall shows 26% · 523 (62svh) shows 42%.

   ⛔ SCOPED TO `.page-hero`, WHICH ONLY THESE THREE PAGES WRITE. The artist
   pages also open on a band and are NOT touched — their hero carries the name,
   the story paragraph and the link row, which is a different amount of
   content, and he did not ask about them.
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 720px) {
  /* Beats `main > .band:first-child { min-height: 100svh }` on specificity,
     not on source order — that rule is unconditional and sits further up. */
  main > .band.page-hero:first-child { min-height: 62svh; }

  /* ⛔ AND THE SCRIM HAS TO FOLLOW, OR THE HEIGHT CHANGE SHIPS AN UNREADABLE
     KICKER. The band scrim is ONE gradient positioned by PERCENTAGE of the
     band, while the text block is sized by CONTENT — so taking 321px of
     height away does not move the text down the picture, it moves the
     PICTURE up behind the text. Measured the moment the height changed:
     the kicker went to 2.09:1 on /label/ and 2.52:1 on /contact/ against a
     4.5 requirement, from 5.59 and 5.47. The same structural fault already
     on this project's record, arriving from the other direction.

     So the dark half starts where the text does. The body is ~229px of a
     523px band, which puts its top edge at 56% — hence the ramp between 40%
     and 60%. ⚠️ The faces are in the TOP half of all three of these
     photographs and the bottom half is desk and foreground, so this darkens
     the part nobody is looking at. That is why it is affordable here and is
     not a general answer for the artist bands, where the subject fills the
     frame. */
  .band.page-hero .band-art::after {
    background: linear-gradient(to bottom,
      rgba(7,7,7,0.78) 0%,
      rgba(7,7,7,0.30) 18%,
      rgba(7,7,7,0.42) 38%,
      rgba(7,7,7,0.89) 54%,
      rgba(7,7,7,0.96) 100%);
  }

  /* ── AND THE NARROW CROP GETS STEERED, PER PAGE ──────────────────────────
     More width is visible now (26% -> 42%), which moves what lands in frame,
     so both anchors were re-chosen by SWEEPING them and looking at the
     result rather than by reasoning about the photograph.

     ⚠️ BOTH OF THESE ARE PHONE-ONLY, UNLIKE `.subject-left` ITSELF, WHICH
     APPLIES AT EVERY WIDTH. The desktop framing of all three heroes is his
     and is already approved; only the narrow crop needed steering, so only
     the narrow crop is touched.

     /about/: 20% put his head hard against the left edge with his face half
     out of frame — correct at 26% visible, wrong at 42%. 30% keeps him whole
     and brings in the DAW screens, the window and the plants. 42% was tried
     and is the worst of the three: the centre becomes a monitor, with his
     head cut on one side and her hair on the other, so nobody's face is in
     it.

     /contact/: this source is NOT the two-shot the record claimed. Looking
     at the file: he is a back-of-head foreground shape on the left, out of
     focus, and SHE is the subject, centre-right. So the frame follows her.
     55% slices her at the right edge; 65% centres her with the headphones
     and the city behind. */
  .band.page-hero.subject-left .band-art img { object-position: 30% 28%; }
  .band.page-hero.subject-right .band-art img { object-position: 65% 28%; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE STORY — a release's photo series, as a rail you scroll sideways.

   Stephan, 2026-10-28: he shoots a series for every song and posts it to
   Instagram and Facebook, "but it only lives there". This is where it lives
   on the site, under the record it was shot for.

   ⛔ THE FRAMES ARE SIZED BY HEIGHT, NOT BY WIDTH, AND THAT IS THE WHOLE
   DESIGN DECISION. Fixing the width and letting height follow leaves a ragged
   bottom edge the moment two frames have different aspects — these fifteen
   are 1600x899 and 1600x898, which is already two. Worse, a fixed-width box
   with object-fit:cover would HARD CROP the first portrait frame he ever
   adds, and an Instagram story is portrait far more often than not. Fixing
   the HEIGHT means a landscape frame is a wide card, a portrait frame is a
   narrow one, every shape arrives uncut, and the rail stays dead level.

   ⚠️ --ar IS SET INLINE FROM THE DATABASE'S REAL NUMBERS, so each frame
   reserves its true box before its image has loaded and the rail does not
   reflow under the reader. A row with no stored dimensions simply does not
   get the property, aspect-ratio falls back to `auto`, and that one frame
   sizes itself on load — a guessed 16/9 fallback would reserve the WRONG box,
   which is worse than reserving none.
   ═══════════════════════════════════════════════════════════════════════════ */

.story-rail {
  /* ⛔ THE PHONE STEP IS NOT A SHRUNK DESKTOP, IT IS THE AFFORDANCE.
     At 200px tall a 16:9 frame is 356px wide, and the wrap leaves 358px on a
     390px phone — so the first frame filled the screen exactly and the next
     one did not peek at all. A rail whose second item is invisible reads as
     one photograph, and nobody swipes a photograph. 172px puts ~50px of the
     next frame on screen, which is the entire reason anyone discovers there
     are fifteen. Measured in a screenshot, not reasoned about. */
  --story-h: 172px;
  list-style: none; margin: 0; padding: 0 0 var(--s3);
  display: flex; gap: var(--s3);
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  /* ⚠️ A horizontal swipe that runs past the end of a rail becomes the
     browser's BACK gesture on a trackpad and on iOS. Containing it means the
     worst case is a rail that stops, not a reader thrown off the page. */
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--muted) transparent;
}
.story-rail::-webkit-scrollbar { height: 8px; }
.story-rail::-webkit-scrollbar-track { background: transparent; }
.story-rail::-webkit-scrollbar-thumb { background: var(--panel-2); }
.story-rail::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* The rail is a focusable region so it can be scrolled from the keyboard —
   without this, fifteen photographs are reachable with a mouse and with a
   finger and by nobody else. */
.story-rail:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

.story-frame {
  flex: 0 0 auto;
  height: var(--story-h);
  aspect-ratio: var(--ar);
  scroll-snap-align: start;
  background: var(--bg-lift);
  border: 1px solid var(--etch);
  overflow: hidden;
  position: relative;
}
.story-frame img { display: block; height: 100%; width: auto; }

@media (min-width: 520px)  { .story-rail { --story-h: 230px; } }
@media (min-width: 720px)  { .story-rail { --story-h: 300px; gap: var(--s4); } }
@media (min-width: 1100px) { .story-rail { --story-h: 360px; } }

/* ── CLICKABLE ONLY ONCE THE SCRIPT HAS SAID SO ──────────────────────────
   ⛔ This class is added by JavaScript, never printed by PHP. A frame that
   looks clickable and is not is the exact failure this site already fixed on
   its link slots: an empty slot renders as a span so it can never be clicked
   into nothing. With no script, no dialog support, or a thrown error, these
   are photographs in a rail — which is the whole of what he asked for. The
   zoom is the extra, so the zoom is what degrades. */
.story-frame.is-zoomable { cursor: zoom-in; }
.story-frame.is-zoomable:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.story-frame.is-zoomable img { transition: transform var(--glide); }
.story-frame.is-zoomable:hover img { transform: scale(1.035); }

/* ── the arrows, also script-made ───────────────────────────────────────── */
.story-nav { margin-left: auto; display: flex; gap: var(--s2); }
.story-arrow {
  width: 32px; height: 32px; padding: 0;
  display: grid; place-items: center;
  background: transparent; color: var(--dim);
  border: 1px solid var(--etch);
  font-family: var(--font-mono); font-size: 1.05rem; line-height: 1;
  cursor: pointer;
  transition: color var(--swift), border-color var(--swift), opacity var(--swift);
}
.story-arrow:hover:not(:disabled) { color: var(--accent); border-color: var(--etch-warm); }
.story-arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* ⚠️ DISABLED AT THE ENDS RATHER THAN LEFT LIVE. A button that is pressed and
   does nothing reads as broken, and at both ends of a rail that is most of
   the presses. Quiet, not hidden — a control that vanishes moves the two
   beside it. */
.story-arrow:disabled { opacity: 0.3; cursor: default; }

/* ── THE FULL VIEW ──────────────────────────────────────────────────────
   A native <dialog>, which brings Esc, focus trapping and inertness with it.
   Rebuilding those by hand is how a lightbox ends up holding a keyboard
   reader inside a page they cannot leave. */
.lightbox {
  position: fixed; inset: 0; margin: 0; padding: 0; border: 0;
  width: 100%; height: 100%; max-width: 100%; max-height: 100%;
  background: transparent; color: var(--text);
  overflow: hidden;
}
.lightbox::backdrop { background: rgba(3, 3, 3, 0.95); }
.lightbox-stage { position: absolute; inset: 0; display: grid; place-items: center; padding: var(--s8) var(--s4); }
.lightbox-stage img {
  display: block; width: auto; height: auto;
  max-width: min(1600px, 94vw); max-height: 78vh;
  border: 1px solid var(--etch);
}
.lightbox-bar {
  position: absolute; left: 0; right: 0; bottom: var(--s5);
  display: flex; align-items: center; justify-content: center; gap: var(--s5);
}
.lightbox-count {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); color: var(--dim);
  min-width: 5ch; text-align: center;
}
.lightbox-close { position: absolute; top: var(--s4); right: var(--s4); }
/* ⛔ ON A PHONE THE FULL VIEW GOES EDGE TO EDGE, AND THAT IS NOT A STYLE
   PREFERENCE. A 16:9 photograph inside a portrait screen is width-bound, so
   94vw with a side gutter gave 366px of picture in an 844px-tall window — a
   small image floating in a lot of black, which reads as something that
   failed to load rather than as a photo viewer. Measured in a live
   screenshot, not reasoned about. Full width makes the void deliberate: a
   dark room around a cinematic frame. The border comes off for the same
   reason — a hairline at the screen edge is just a seam. */
@media (max-width: 520px) {
  .lightbox-stage { padding: var(--s7) 0; }
  .lightbox-stage img { max-width: 100vw; max-height: 74vh; border: 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* The hover zoom is the whole of the motion here; the rail's own smooth
     scrolling is asked for separately, in script, from the same query. */
  .story-frame.is-zoomable img { transition: none; }
  .story-frame.is-zoomable:hover img { transform: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE STORY, MADE AWKWARD TO TAKE — 2026-10-28, his ask.

   ⛔ READ THIS BEFORE BELIEVING ANY OF IT PROTECTS ANYTHING. The browser has
   to receive the picture in order to draw it, so the file is on the reader's
   machine before they see it. Nothing below stops a network panel, and
   nothing anywhere stops a screenshot. What it stops is a person REACHING for
   the picture: drag, right-click, long-press, Save image as. That is the real
   boundary, it is where almost all casual copying happens, and it is the only
   claim these rules are allowed to make.

   ⚠️ THE THING THAT ACTUALLY TRAVELS IS THE WATERMARK, and it is burned into
   the pixels by tools/import-release-media.php — not applied here, where any
   reader could switch it off in two clicks. These rules are the friction; the
   mark is the answer.

   ⛔ SCOPED TO THE STORY, AND THAT IS HIS INSTRUCTION, NOT A DETAIL. "Remove
   right click on the images, also just on the images." Covers, heroes, the
   catalogue wall and every link on the site keep their normal menu. A
   site-wide context-menu block is the move that annoys a hundred readers to
   inconvenience one.
   ═══════════════════════════════════════════════════════════════════════════ */

.story-frame img,
.lightbox-stage img {
  /* ⛔ THIS ONE LINE DOES MOST OF THE WORK. With the image out of the hit
     test, a right-click lands on its container instead — and a container is
     not an image, so the browser's menu has no "Save image as", no "Copy
     image" and no "Open image in new tab" to offer. Dragging it starts
     nothing either, because there is nothing under the cursor to drag. */
  pointer-events: none;

  /* The long-press sheet on iOS and Android, which is where a phone reader
     saves a picture. Without this the whole desktop half is theatre, since
     this site is read on a phone more than anywhere else. */
  -webkit-touch-callout: none;

  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

/* The frame and the full view take the hit instead, so nothing inside them
   can be selected or dragged out either. */
.story-frame,
.lightbox-stage,
.lightbox-holder {
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}

/* ⛔ THE HOLDER EXISTS BECAUSE pointer-events BROKE THE CLOSE RULE.
   The full view closes on a click that lands on the backdrop or the stage,
   and the picture was the thing stopping a click on itself from counting.
   With the image out of the hit test, clicking the photograph would have
   fallen through to the stage and CLOSED the view the reader just opened —
   a protection silently breaking a behaviour, which is the worst kind of
   change because nothing reports it. The holder is the picture's own box and
   it absorbs that click. */
.lightbox-holder {
  display: block;
  line-height: 0;
  max-width: min(1600px, 94vw);
  max-height: 78vh;
}
@media (max-width: 520px) {
  .lightbox-holder { max-width: 100vw; max-height: 74vh; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE RELEASE PAGE — 2026-10-30.

   ⛔ IT ADDS ALMOST NO CSS ON PURPOSE. The page is the `.feature` block the
   artist page already uses, plus `.archive-list`, plus the story rail. Reusing
   them inherits every fix those components have earned — including the one
   from 2026-10-26, where a redeclaration four hundred lines down had killed
   `.feature`'s responsive stack and the whole right column rendered at zero
   width on a phone for the site's entire life. A second component would have
   had to earn that fix again, by somebody noticing.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The release page's title is an <h1> — it IS the page — where the artist
   page's is an <h2> under the artist's name. Same type, different rank, so the
   selector takes both rather than the styling being copied. */
.feature-meta h1 {
  font-size: clamp(2rem, 5.4vw, 4rem);
  font-weight: 700;
  line-height: 0.96;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  margin-bottom: var(--s4);
}
.feature-meta h1 .tl { display: block; }

/* ⚠️ The kicker is a LINK on a release page and plain text on an artist page.
   It must still read as a kicker: no underline, and the accent arrives on
   hover rather than sitting there competing with the title. */
.feature-meta .kicker a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--swift), border-color var(--swift);
}
.feature-meta .kicker a:hover { color: var(--accent); border-bottom-color: var(--etch-warm); }
.feature-meta .kicker a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ── ARCHIVE ROWS ARE LINKS NOW ──────────────────────────────────────────
   ⛔ THE WHOLE ROW IS NOT THE LINK, AND THAT IS DELIBERATE. Each row already
   carries its own Spotify and Apple buttons; wrapping the row in an anchor
   would nest interactive elements, which is invalid and makes the store
   buttons unreachable from a keyboard. The TITLE is the link, the thumbnail
   is a second path to the same place, and the buttons keep working. */
.arc-title a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--swift), border-color var(--swift);
}
.arc-row:hover .arc-title a,
.arc-title a:hover { color: var(--accent); border-bottom-color: var(--etch-warm); }
.arc-title a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* The thumbnail links to the same page. ⚠️ It is aria-hidden with tabindex -1
   in the markup: a screen reader and a keyboard already have the title link,
   and announcing the same destination twice in one row is noise, not access. */
.arc-art a { display: block; width: 100%; height: 100%; }
.arc-art a img { display: block; width: 100%; height: 100%; object-fit: cover; }


/* ═══════════════════════════════════════════════════════════════════════════
   THE FEED — /feed/, 2026-10-30. One address for every bio.

   ⛔ Almost everything here is borrowed: `.feature` for the most recent
   record, `.archive-list` for the rest. Only the masthead and the follow rows
   are new, because only they are shapes the site did not already have.
   ═══════════════════════════════════════════════════════════════════════════ */

.feed-head { padding-top: var(--s9); }
.feed-title {
  font-family: var(--font-body);
  font-size: clamp(2.4rem, 8vw, 5rem);
  font-weight: 700;
  line-height: 0.94;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  margin-block: var(--s3) var(--s4);
}
.feed-line {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--dim);
}

/* The lead release's title is an <h3> — the page's <h1> is the label's name,
   and a feed is not a page about one record. ⚠️ Rank follows meaning; the SIZE
   is a separate decision and is set here rather than inherited from a rank. */
.feed-lead-title {
  font-size: clamp(1.75rem, 4.4vw, 3rem);
  font-weight: 700; line-height: 0.98;
  letter-spacing: -0.02em; text-transform: uppercase;
  margin-bottom: var(--s4);
}
.feed-lead-title .tl { display: block; }
.feed-lead-title a {
  color: inherit; text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--swift), border-color var(--swift);
}
.feed-lead-title a:hover { color: var(--accent); border-bottom-color: var(--etch-warm); }
.feed-lead-title a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.feature-cover a { display: block; width: 100%; height: 100%; }
.feature-cover a img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* ── where to follow ─────────────────────────────────────────────────────
   ⚠️ Rows, not cards — the same answer the archive reached: a grid of four
   artists is whatever ragged shape the roster happens to be, and the roster
   changes. */
.follow-list { list-style: none; margin: 0; padding: 0; }
.follow-row {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--s3) var(--s5);
  padding-block: var(--s4);
  border-top: 1px solid var(--etch);
}
.follow-row:last-child { border-bottom: 1px solid var(--etch); }
.follow-name { font-size: var(--t-lead); font-weight: 600; }
.follow-name a {
  color: inherit; text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--swift), border-color var(--swift);
}
.follow-name a:hover { color: var(--accent); border-bottom-color: var(--etch-warm); }
.follow-name a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

@media (max-width: 520px) {
  .follow-row { flex-direction: column; align-items: flex-start; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE PLAYER — 2026-11-08. Full songs, free, his call.

   ⛔ NOTHING HERE DRAWS A CONTROL UNTIL THE SCRIPT HAS SAID SO. `.is-playable`
   is added by app/inc/player-script.php and is the only thing that makes a row
   look pressable. Without it this is a tracklist with running times, which is
   an honest thing for a page to be. Same rule as the story rail, the link
   slots and the share arrows.
   ═══════════════════════════════════════════════════════════════════════════ */

.tracklist { list-style: none; margin: 0; padding: 0; }

.track {
  display: grid;
  grid-template-columns: 2.6rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s4);
  padding-block: var(--s3);
  border-top: 1px solid var(--etch);
}
.track:last-child { border-bottom: 1px solid var(--etch); }

.track-n {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); color: var(--muted);
}
.track-title { min-width: 0; font-weight: 500; }
.track-time {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.track.is-playable { cursor: pointer; }
.track.is-playable:hover { background: color-mix(in srgb, var(--panel) 55%, transparent); }
.track.is-playable:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ⚠️ THE CURRENT TRACK IS MARKED BY MORE THAN COLOUR. The number becomes the
   accent AND the title gains weight — colour alone is not a state to anybody
   who cannot distinguish it, and the script announces aria-current as well. */
.track.is-current .track-n,
.track.is-current .track-time { color: var(--accent); }
.track.is-current .track-title { font-weight: 700; }

/* ── THE TRANSPORT ───────────────────────────────────────────────────────
   ⛔ FIXED, AND THE PAGE MAKES ROOM FOR IT. A bar that floats over the last
   row of a tracklist hides the thing you were about to press. */
.transport {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: flex; align-items: center; gap: var(--s4);
  padding: var(--s3) var(--s4);
  background: var(--bg-lift);
  border-top: 1px solid var(--etch);
  /* ⚠️ The legibility is in the background, never in a filter — an engine
     without backdrop-filter must degrade to a flat wash, not to nothing. */
}
.transport[hidden] { display: none; }

.tp-play, .tp-close {
  flex: 0 0 auto; width: 38px; height: 38px; padding: 0;
  display: grid; place-items: center;
  background: transparent; color: var(--text);
  border: 1px solid var(--etch); cursor: pointer;
  transition: color var(--swift), border-color var(--swift);
}
.tp-play:hover, .tp-close:hover { color: var(--accent); border-color: var(--etch-warm); }
.tp-play:focus-visible, .tp-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The play and pause marks are DRAWN, not typed — ETCH's first law, and a
   glyph font would render a different shape on every platform. */
.tp-play::before {
  content: ''; width: 0; height: 0;
  border-left: 11px solid currentColor;
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
  margin-left: 2px;
}
.tp-play.is-playing::before {
  width: 11px; height: 13px; margin-left: 0;
  border: 0;
  border-left: 3.5px solid currentColor;
  border-right: 3.5px solid currentColor;
}
.tp-close { font-family: var(--font-mono); font-size: 1.1rem; line-height: 1; }

.tp-now { flex: 0 1 auto; min-width: 0; display: grid; }
.tp-title { font-weight: 600; font-size: var(--t-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tp-sub {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.tp-seek {
  flex: 1 1 auto; min-width: 60px; height: 20px;
  -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer;
}
.tp-seek::-webkit-slider-runnable-track { height: 2px; background: var(--panel-2); }
.tp-seek::-moz-range-track            { height: 2px; background: var(--panel-2); }
.tp-seek::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; margin-top: -4.5px;
  background: var(--accent); border: 0; border-radius: 50%;
}
.tp-seek::-moz-range-thumb { width: 11px; height: 11px; background: var(--accent); border: 0; border-radius: 50%; }
.tp-seek:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.tp-time {
  flex: 0 0 auto;
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); color: var(--dim);
  font-variant-numeric: tabular-nums; min-width: 9ch; text-align: right;
}

@media (max-width: 560px) {
  /* ⚠️ The seek bar gets its own row on a phone. At 390px the five controls in
     one line leave it about 40px wide, which is not a thing anyone can drag. */
  .transport { flex-wrap: wrap; row-gap: var(--s2); }
  .tp-now { order: 1; flex: 1 1 auto; }
  .tp-seek { order: 4; flex: 1 1 100%; min-width: 0; }
  .tp-time { order: 3; min-width: 0; }
}
