/* =========================================================
   JUNKSISTA — one page
   Palette sampled from the "0A" artwork and press photos.
   ========================================================= */

:root {
  /* colour */
  --bg:        #08080a;
  --bg-2:      #0e0e11;
  --surface:   #131317;
  --line:      rgba(255,255,255,.10);
  --line-soft: rgba(255,255,255,.06);
  --text:      #f3f1ef;
  --muted:     #9c9ca4;
  --red:       #c53238;   /* from the 0A cover */
  --red-hot:   #ff3b45;
  --teal:      #7fd4cd;   /* from the JUNKSISTA wordmark */

  /* type */
  --display: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --body:    "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* motion */
  --ease:     cubic-bezier(.16,1,.3,1);
  --ease-out: cubic-bezier(.22,.61,.36,1);
  --dur:      .9s;

  /* layout */
  --wrap: 1180px;
  --gut:  clamp(1.25rem, 4vw, 3rem);
  --header-h: 72px;
}

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

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 1rem);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--body);
  font-size: clamp(1rem, .95rem + .2vw, 1.075rem);
  line-height: 1.65;
  font-weight: 300;
  letter-spacing: .002em;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }
a   { color: inherit; }
p   { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }
em  { font-style: italic; }

::selection { background: var(--red); color: #fff; }

:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 3px;
}

.skip-link {
  position: fixed; top: .5rem; left: .5rem; z-index: 999;
  padding: .7rem 1.1rem; background: var(--red); color: #fff;
  font-weight: 600; text-decoration: none;
  transform: translateY(-160%);
  transition: transform .25s var(--ease);
}
.skip-link:focus { transform: none; }

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gut);
}

/* ---------- typography helpers ---------- */

/* Zwei Register, nicht eins, und jetzt ueber die ganze Achse. Archivo traegt
   wdth 62..125; benutzt wurden erst nur 106–125 (eine einzige Breite, entspre-
   chend gleichfoermig), dann 74 als schmales Gegenstueck. 74 war aber nicht das
   Ende der Achse, sondern ein Wert in ihrer Mitte — die Wortmarke auf dem
   0A-Cover steht deutlich schmaler als alles, was hier stand. Der schmale Pol
   sitzt deshalb jetzt auf 62 %, dem echten Boden der Achse: darunter klemmt die
   Datei, 56 % und 50 % rendern messbar identisch zu 62 %. Zusammen mit den
   125 % von .hero__title spannt die Seite damit die Breitenachse von Anschlag
   zu Anschlag, ohne eine zweite Schriftdatei.

   Der Deckel steigt von 6,4rem auf 7,5rem, und das ist gerechnet: bei 62 %
   statt 74 % braucht derselbe Satz 85,5 % der Breite, die Schrift kann also um
   1/0,855 hoch. Das Ergebnis ist breitenneutral — "Death By Heartache" misst
   919 px gegen 915 px vorher, "Recent Releases" 782 gegen 780 —, aber die Zeile
   ist 120 px hoch statt 102. Die zwei unteren clamp-Terme steigen im selben
   Verhaeltnis mit.

   Nach oben ist bei 7,5rem Schluss, und die Grenze setzt nicht die Spalte: bei
   8rem waechst "Recent Releases" auf 834 px, und dann wrappt der
   Bandcamp-Link in .section__head--row unter die Headline. Wer hoeher will,
   muss dort vorher hinsehen.

   Auf 375 px steht .display damit auf 54 px und .hero__title auf 51 px — die
   Section-Headline ist dort nominell die groessere. Das ist kein Fehler und
   will nicht "korrigiert" werden: die Hero-Zeile ist auf schmalen Fenstern
   breitengebunden (13,53vw, damit sie beide Fensterkanten trifft), sie laesst
   sich also nicht anheben, ohne die Randbuendigkeit zu verlieren. Sie dominiert
   dort ueber die Breite, nicht ueber die Hoehe. Wer stattdessen .display
   herunterzieht, nimmt die 1,17 wieder weg, um die es hier ging. */
.display {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 62%;
  font-size: clamp(3.35rem, 1.4rem + 8.2vw, 7.5rem);
  line-height: .88;
  letter-spacing: -.01em;
  text-transform: uppercase;
  margin: 0 0 .4em;
}
/* Die Gegenseite des Kontrasts: "0A" bleibt breit und fett. Es setzt
   font-stretch bewusst neu — ohne die Zeile erbte es die 62 % von .display. */
.display--xl {
  font-stretch: 118%;
  font-size: clamp(4rem, 2rem + 12vw, 10rem);
  letter-spacing: -.03em;
  line-height: .82;
  margin-bottom: .25em;
}

/* Die Section-Headlines laufen mit demselben chromatischen Versatz ein wie die
   Hero-Zeile — nur einmal, ohne den wiederkehrenden Aussetzer danach. Vorher
   kamen sie als reines Fade-Up herein, also in der Sprache jedes Baukastens;
   damit war die eine belegte Geste der Seite (Slice-Versatz vom 0A-Cover, rot
   links, tuerkis rechts) auf Hero und Release-Kacheln beschraenkt, waehrend
   fuenf Ueberschriften dazwischen neutral ankamen. Drei Punkte:

   - Einlauf plus genau ein Nachschlag, keine Schleife. @keyframes chGlitch
     bleibt dem Hero vorbehalten: dort steht eine einzige Zeile im Bild, hier
     waeren es fuenf dauerlaufende text-shadow-Animationen ueber die ganze
     Seite — genau die Sorte Effekt, die den Pane hier schon einmal angehalten
     hat (s. Grain). Der Nachschlag steht unten bei displayAftershock.
   - Beide Schatten stehen auch im Zielzustand als Schatten da, nur mit Offset 0
     und Alpha 0. text-shadow interpoliert ausschliesslich zwischen zwei Listen
     gleicher Laenge; gegen `none` springt der Wert hart.
   - transition-delay muss hier wieder gesetzt werden. Die Kurzform `transition`
     setzt sie auf 0 zurueck, und an --rv-d haengen die .08s, mit denen jede
     Headline hinter ihrem Eyebrow herlaeuft.

   Der Versatz ist mit .03em kleiner als die .055em im Hero: dort traegt ihn
   eine 195-px-Zeile, hier 120 px, und der Wert soll als Schnitt lesen und
   nicht als zweite Kontur. */
.display[data-reveal] {
  text-shadow: -.03em 0 0 var(--red), .03em 0 0 var(--teal);
  transition:
    opacity var(--dur) var(--ease),
    transform var(--dur) var(--ease),
    filter var(--dur) var(--ease),
    text-shadow .75s var(--ease);
  transition-delay: var(--rv-d, 0s);
}
.display[data-reveal].is-in {
  text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0);
  animation: displayAftershock .34s step-end 2s 1;
}

/* Der Nachschlag. Der Einlauf allein ist ein Uebergang, kein Glitch — dieselbe
   Beobachtung, aus der im Hero @keyframes chGlitch entstanden ist. Der
   Unterschied zum Hero ist Absicht und traegt die Abstufung der Seite: der
   Hero zuckt dauernd, die Headlines zucken einmal nach, die Kacheln zucken auf
   Hover. Liefe hier dieselbe Schleife, waeren alle drei gleich laut, und der
   Hero verloere genau das, was ihn heraushebt — aus der Geste wuerde der
   Normalzustand. Fuenf Punkte:

   - Genau eine Iteration. Kein `infinite`, also auch keine Pause ausserhalb des
     Viewports noetig (anders als Hero und Ticker, Punkt 9 in main.js): was
     einmal .34s laeuft und dann fertig ist, kostet im Hintergrund nichts.
   - Sie laeuft auf dem ganzen Element, nicht pro Buchstabe. Der Hero-Glitch
     braucht .ch, weil sein Reiz die auseinanderdriftenden Dauern sind; ein
     einzelner Burst muss nicht durch das Wort wandern. Das spart den Split
     fuer fuenf Headlines — main.js Punkt 1 nimmt weiter genau eine Zeile.
   - Erster und letzter Keyframe stehen auf dem Zielwert des Einlaufs (Offset 0,
     Alpha 0). Eine Animation ohne fill-mode faellt am Ende auf den darunter
     liegenden Wert zurueck; stimmt der nicht mit dem letzten Keyframe ueberein,
     springt die Zeile sichtbar.
   - Die 2 s Verzoegerung halten den Einlauf frei. Der ist nach .83 s durch
     (.75s Transition + .08s aus --rv-d); waehrend der Verzoegerung ist keine
     Animation aktiv, die Transition laeuft also unangetastet.
   - step-end, wie ueberall hier: es wird nichts interpoliert, ein Repaint faellt
     nur an den drei Keyframe-Grenzen an statt 60-mal pro Sekunde.

   Die drei Stufen sind ein Ereignis, nicht drei — sie sitzen in 265 ms
   zusammen. Die letzte trifft nur einen Kanal; ein Glitch erwischt nicht jedes
   Mal beide. */
@keyframes displayAftershock {
  0%        { text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0); }
  8%        { text-shadow: -.026em 0 0 var(--red), .018em 0 0 var(--teal); }
  32%       { text-shadow: .014em -.006em 0 var(--red), -.022em .006em 0 var(--teal); }
  55%       { text-shadow: -.03em 0 0 var(--red), 0 0 0 rgba(127,212,205,0); }
  78%, 100% { text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0); }
}

.lede {
  font-size: clamp(1.06rem, 1rem + .45vw, 1.3rem);
  line-height: 1.55;
  color: #dfdedd;
}

.eyebrow {
  display: flex; align-items: center; gap: .7rem;
  font-family: var(--display);
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--teal);
  margin: 0 0 1.1rem;
}
.eyebrow i {
  width: 26px; height: 1px; background: var(--teal); opacity: .6;
  flex: none;
}

/* ---------- chrome ---------- */

/* Static film grain: a single 140px tile, no animation.
   An animated grain layer costs a full-screen repaint every frame — not worth
   it on a page that is mostly large photography. */
.grain {
  position: fixed; inset: 0;
  z-index: 200;
  pointer-events: none;
  opacity: .04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.scroll-progress {
  position: fixed; inset: 0 0 auto 0; height: 2px; z-index: 150;
  pointer-events: none;
}
.scroll-progress i {
  display: block; height: 100%; width: 100%;
  background: linear-gradient(90deg, var(--red), var(--teal));
  transform: scaleX(0); transform-origin: 0 50%;
}

/* ---------- header ---------- */

.site-header {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  height: var(--header-h);
  display: flex; align-items: center;
  transition: background .4s var(--ease), border-color .4s var(--ease);
  border-bottom: 1px solid transparent;
}
.site-header.is-stuck {
  background: rgba(8,8,10,.72);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  border-bottom-color: var(--line-soft);
}

.header__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem;
}

.brand {
  display: inline-flex; align-items: center; gap: .7rem;
  text-decoration: none;
}
/* Die Wortmarke ist ein weißes PNG auf transparent, kein Text und keine
   CSS-Maske: als Maske war sie in Safari zusammen mit dem drop-shadow
   unsichtbar, und ein fehlendes Bild fällt hier wenigstens als alt-Text auf. */
.brand__logo {
  display: block;
  width: 116px; height: auto;
  /* Haarlinien über einem Foto: ohne den Schatten verschwindet der
     Schriftzug, sobald im Hero ein helleres Gesicht darunter liegt. */
  filter: drop-shadow(0 1px 5px rgba(0,0,0,.6));
  transition: opacity .3s var(--ease);
}
.brand:hover .brand__logo { opacity: .75; }
.brand__mark {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--red);
  box-shadow: 0 0 0 0 rgba(197,50,56,.6);
  animation: pulse 2.6s var(--ease) infinite;
}
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(197,50,56,.55); }
  70%  { box-shadow: 0 0 0 12px rgba(197,50,56,0); }
  100% { box-shadow: 0 0 0 0 rgba(197,50,56,0); }
}

.nav { display: flex; align-items: center; gap: 2rem; }
.nav ul {
  display: flex; align-items: center; gap: 1.9rem;
  list-style: none; margin: 0; padding: 0;
}
.nav__link {
  position: relative;
  text-decoration: none;
  font-size: .82rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color .3s var(--ease);
  padding-block: .35rem;
}
.nav__link::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 100%; height: 1px; background: var(--red);
  transform: scaleX(0); transform-origin: 100% 50%;
  transition: transform .45s var(--ease);
}
.nav__link:hover, .nav__link.is-active { color: var(--text); }
.nav__link:hover::after, .nav__link.is-active::after {
  transform: scaleX(1); transform-origin: 0 50%;
}

.burger {
  display: none;
  width: 44px; height: 44px;
  background: none; border: 0; cursor: pointer;
  padding: 0; position: relative;
}
.burger span {
  position: absolute; left: 11px; width: 22px; height: 1.5px;
  background: var(--text);
  transition: transform .4s var(--ease), opacity .25s;
}
.burger span:nth-child(1) { top: 19px; }
.burger span:nth-child(2) { top: 25px; }
.burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(3px) rotate(45deg); }
.burger[aria-expanded="true"] span:nth-child(2) { transform: translateY(-3px) rotate(-45deg); }

/* ---------- buttons ---------- */

/* Keine Radien mehr, hier und an allen sechs anderen Stellen (.slice — damals
   .tilt —, .quote, .pq--lead, .player, .release__art, .skip-link). Die Pille war
   der eine Wert,
   an dem die Seite wie ein Produkt-Template aussah: im Hero stand ein
   abgerundetes rotes Oval auf einem Foto mit zwei harten Gesichtern, und die
   vier Cover — Slice-Glitch, gepinselter Titel — lagen in Kacheln mit 4 px
   Eckenradius. Plakate haben rechte Winkel.
   Rund bleiben nur die Punkte (.brand__mark, .album__stat i,
   .news__date::before, .ticker__track i) und .player__play: die Punkte sind
   Interpunktion, kein
   Behaelter, und der Kreis um das Dreieck ist die Play-Konvention — quadratisch
   liest er als Stop. */
.btn {
  --btn-bg: var(--red);
  position: relative;
  display: inline-flex; align-items: center; gap: .6rem;
  padding: .95rem 1.7rem;
  border: 1px solid transparent;
  font-family: var(--display);
  font-size: .82rem; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  overflow: hidden;
  isolation: isolate;
  transition: transform .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease);
}
.btn em { font-style: normal; }
.btn svg {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round;
  transition: transform .4s var(--ease);
}
.btn:hover svg { transform: translate(2px,-2px); }
.btn::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--btn-bg);
  transform: translateY(0);
  transition: transform .45s var(--ease);
}
.btn::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--text);
  transform: translateY(101%);
  transition: transform .45s var(--ease);
}
.btn--solid { color: #fff; }
.btn--solid:hover { color: #08080a; }
.btn--solid:hover::after { transform: translateY(0); }

.btn--ghost {
  color: var(--text);
  border-color: var(--line);
}
.btn--ghost::before { background: transparent; }
.btn--ghost::after { background: rgba(255,255,255,.08); }
.btn--ghost:hover { border-color: rgba(255,255,255,.28); }
.btn--ghost:hover::after { transform: translateY(0); }

.btn--sm  { padding: .62rem 1.15rem; font-size: .72rem; }
.btn--lg  { padding: 1.15rem 2.2rem; font-size: .9rem; }

.link-arrow {
  display: inline-flex; align-items: center; gap: .5rem;
  text-decoration: none;
  font-size: .8rem; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
  padding-bottom: .35rem;
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.link-arrow svg {
  width: 14px; height: 14px; fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linecap: round;
  transition: transform .4s var(--ease);
}
.link-arrow:hover { color: var(--text); border-color: var(--red); }
.link-arrow:hover svg { transform: translate(3px,-3px); }

/* ---------- hero ---------- */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding-block: calc(var(--header-h) + 4rem) clamp(4rem, 10vh, 7rem);
  overflow: hidden;
}
/* Der Überstand muss den Parallax-Weg tragen: das JS verschiebt um höchstens
   speed/2 der Medienhöhe, bei 0.18 also 9 % — sonst wandert die Bildkante ins
   Bild und darunter steht nur noch der Seitenhintergrund. */
.hero__media { position: absolute; inset: -12% 0; z-index: 0; }
.hero__picture { display: block; width: 100%; height: 100%; }
.hero__img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 22%;
  filter: grayscale(.25) contrast(1.08) brightness(.82);
  transform: translate3d(0, var(--parallax-y, 0px), 0);
  will-change: transform;
}
/* the clip is graded in the file itself, so only the brightness is matched
   to .hero__img here — enough that the cross-fade does not jump */
.hero__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
  filter: contrast(1.02) brightness(.84);
  opacity: 0;
  transform: translate3d(0, var(--parallax-y, 0px), 0);
  transition: opacity 1.4s var(--ease);
  will-change: transform, opacity;
}
.hero__video.is-ready { opacity: 1; }

.hero__scrim {
  position: absolute; inset: 0;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(197,50,56,.20), transparent 55%),
    linear-gradient(to top, var(--bg) 4%, rgba(8,8,10,.78) 38%, rgba(8,8,10,.35) 72%, rgba(8,8,10,.65) 100%);
}

/* Keine Deckelung mehr: die Headline soll die Fensterbreite fuellen, und ein
   zentrierter 1560er-Block haette sie ab 1656 px Fensterbreite wieder
   eingefangen. Eyebrow, Zitat und Buttons haben eigene Maximalbreiten
   (40ch bzw. Inhalt), die ruecken dadurch nur an den Gutter statt an die Kante
   eines unsichtbaren Kastens — auf breiten Fenstern die plakativere Setzung. */
/* max-width: none, nicht bloss die 1560 weg — sonst greift wieder die 1180 aus
   .wrap und die Headline endet bei 1230 statt an der Fensterkante. */
.hero__inner { position: relative; z-index: 1; max-width: none; }

.hero__eyebrow {
  font-family: var(--display);
  font-size: .78rem; font-weight: 600;
  letter-spacing: .34em; text-transform: uppercase;
  color: var(--teal);
  margin: 0 0 1.2rem;
}

/* Randbuendig statt brav in der Spalte. Der negative Gutter zieht die Zeile an
   die Fensterkanten; die Groesse ist darauf gerechnet und nicht geraten:
   "JUNKSISTA" misst in Archivo 800 / wdth 125 genau 7,3926em, fuer 100vw also
   100/7,3926 = 13,53vw. letter-spacing muss dafuer auf 0 — die .01em davor
   verteilten sich auf neun Zeichen und schoben die Zeile ueber die Kante.
   Gedeckelt wird nicht ueber eine feste rem-Zahl, sondern ueber die Fenster-
   hoehe: ein fester Deckel haette die Zeile ab ~1780 px Breite bei fast, aber
   eben nicht ganz voller Breite stehen lassen — und knapp danebenliegend sieht
   schlechter aus als deutlich kleiner. 32svh greift nur auf flachen Fenstern,
   wo sonst Zitat und Buttons unter die Kante rutschen. */
.hero__title {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: max(2.2rem, min(13.53vw, 32svh));
  line-height: .84;
  letter-spacing: 0;
  text-transform: uppercase;
  margin: 0 0 1.4rem;
  margin-inline: calc(-1 * var(--gut));
  display: flex; flex-wrap: nowrap; white-space: nowrap;
}
/* Chromatischer Versatz, rot links und tuerkis rechts. Der Effekt ist belegt
   statt erfunden: das 0A-Cover zeigt genau diesen Slice-Versatz auf den beiden
   Gesichtern, und die zwei Farben der Palette sind dieselben. Er kommt zweimal
   vor — einmal beim Einlaufen (dieser Block) und danach als wiederkehrender
   Aussetzer (@keyframes chGlitch, weiter unten). Drei Punkte zum Einlaufen:
   - Er faehrt auf 0 zusammen und bleibt nicht stehen. Ein dauerhafter Farbsaum
     an 195-px-Versalien liest sich als kaputte Schriftdatei, nicht als Geste.
   - Beide Schatten muessen auch im Zielzustand als Schatten dastehen, nur mit
     Offset 0 und Alpha 0: text-shadow interpoliert ausschliesslich zwischen
     zwei Listen gleicher Laenge — gegen `none` springt der Wert hart.
   - Der Versatz steht in em, nicht in px. Die Headline laeuft von 2,2rem bis
     13,53vw; ein fester Pixelwert waere auf dem Handy ein Klotz und auf 1920
     nicht mehr zu sehen.
   Reduced Motion braucht hier keine eigene Regel: das JS zerlegt die Headline
   dann gar nicht erst (Punkt 1 in main.js), es gibt also kein .ch. */
.hero__title .ch {
  display: inline-block;
  transform: translateY(110%) rotate(6deg);
  opacity: 0;
  text-shadow: -.055em 0 0 var(--red), .055em 0 0 var(--teal);
  transition: transform 1s var(--ease), opacity .7s var(--ease),
              text-shadow .8s var(--ease);
  transition-delay: calc(var(--i) * .045s + .15s);
}
.hero__title.is-in .ch {
  transform: none; opacity: 1;
  text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0);
}

/* Der wiederkehrende Aussetzer. Einmal beim Reveal ist ein Uebergang, kein
   Glitch — aber eine dauerlaufende text-shadow-Animation ist das Gegenteil
   davon, was diese Seite verkraftet: der Schatten ist nicht compositor-faehig,
   jeder Frame malt die 195-px-Zeile neu, und ein dauerhafter Vollflaechen-
   Effekt hat den Pane hier schon einmal zum Stillstand gebracht (s. Grain).
   Vier Vorkehrungen dagegen, alle vier noetig:

   - `step-end` statt einer Kurve. Es wird nichts interpoliert, also gibt es nur
     an den Keyframe-Grenzen einen Repaint statt 60 pro Sekunde. Ein Glitch
     springt ohnehin, er blendet nicht.
   - Kurze Bursts in einem langen Zyklus. Vier Aussetzer von 45–120 ms auf
     ~9 Sekunden; dazwischen steht der Schatten auf 0 und kostet nichts.
   - Die Bursts sitzen auf unregelmaessigen Abstaenden (12,3 → 43,6 → 58,0 →
     89,2 %), sonst zaehlt das Auge den Takt mit.
   - Jeder Buchstabe hat eine eigene Dauer, nicht nur einen eigenen Versatz.
     Nur mit dem Versatz liefe ein Wanderwelle-Muster im Gleichtakt durch das
     Wort; unterschiedliche Dauern driften auseinander und richten sich nie
     wieder aus — das ist die eigentliche Quelle der Unregelmaessigkeit.

   Die Amplitude bleibt bei <= .055em, und das ist eine Grenze, keine Vorliebe:
   `.ch-wrap` hat `overflow: hidden` fuer den Vorhang, und die Wraps sind als
   Flex-Items ohne `flex: none` um ~3 px gestaucht — ein weiterer Schatten wird
   an der rechten Wrap-Kante abgeschnitten (bei .2em deutlich sichtbar). Wer
   mehr Weg will, muss an die Wrap-Geometrie, und daran haengt die gerechnete
   Randbuendigkeit der Zeile. Dass dabei pro Buchstabe unterschiedlich viel
   wegfaellt, ist kein Schaden: genau das laesst den Versatz uneinheitlich
   aussehen.

   Der Delay von 1,6 s haelt die erste Runde vom Reveal frei — der laeuft
   spaetestens nach 1,31 s aus (8 x .045s + .15s + .8s). */
@keyframes chGlitch {
  0%,    11.9% { text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0); }
  12.3%        { text-shadow: -.05em 0 0 var(--red), .038em 0 0 var(--teal); }
  13.0%        { text-shadow: .022em -.008em 0 var(--red), -.03em .008em 0 var(--teal); }
  13.6%, 43.2% { text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0); }
  /* nur ein Kanal — ein Glitch trifft nicht jedes Mal beide */
  43.6%        { text-shadow: .034em 0 0 var(--red), 0 0 0 rgba(127,212,205,0); }
  44.1%, 57.6% { text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0); }
  58.0%        { text-shadow: -.026em .01em 0 var(--red), .042em -.006em 0 var(--teal); }
  58.5%        { text-shadow: -.055em 0 0 var(--red), .012em 0 0 var(--teal); }
  59.3%, 88.8% { text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0); }
  89.2%        { text-shadow: .015em .008em 0 var(--red), -.04em 0 0 var(--teal); }
  89.9%, 100%  { text-shadow: 0 0 0 rgba(197,50,56,0), 0 0 0 rgba(127,212,205,0); }
}
.hero__title.is-in .ch {
  animation: chGlitch calc(8.6s + var(--i) * .37s) step-end
             calc(1.6s + var(--i) * 1.09s) infinite;
}
/* Ausserhalb des Viewports angehalten, wie das Hero-Video (Punkt 9 in main.js).
   Die Klasse setzt das JS und sie bedeutet "aus" statt "an": faellt das JS aus,
   laeuft der Glitch weiter statt fuer immer zu stehen. */
.hero.is-offscreen .hero__title .ch { animation-play-state: paused; }

/* wrapper keeps the slide-up clipped */
.hero__title .ch-wrap { display: inline-block; overflow: hidden; padding-block: .04em; }

/* the tagline is a review quote, not band copy — so it gets quote styling */
.hero__quote {
  margin: 0 0 2.2rem;
  max-width: 40ch;
  border-left: 2px solid var(--red);
  padding-left: clamp(1rem, 2vw, 1.5rem);
}
.hero__quote blockquote {
  margin: 0;
  font-size: clamp(1.15rem, 1rem + .8vw, 1.7rem);
  line-height: 1.35;
  color: #e8e7e6;
  font-weight: 300;
}
.hero__quote blockquote::before { content: "“"; }
.hero__quote blockquote::after  { content: "”"; }
.hero__quote figcaption {
  margin-top: .7rem;
  font-family: var(--display);
  font-size: .7rem; font-weight: 600;
  letter-spacing: .22em; text-transform: uppercase;
  color: var(--teal);
}

.hero__actions { display: flex; flex-wrap: wrap; gap: .8rem; }

.hero__scroll {
  position: absolute; right: var(--gut); bottom: clamp(4rem,10vh,7rem);
  z-index: 1;
  display: inline-flex; flex-direction: column; align-items: center; gap: .8rem;
  text-decoration: none;
  font-size: .62rem; letter-spacing: .28em; text-transform: uppercase;
  color: var(--muted);
  transition: color .3s var(--ease);
}
.hero__scroll:hover { color: var(--text); }
.hero__scroll i {
  width: 1px; height: 54px;
  background: linear-gradient(to bottom, var(--teal), transparent);
  position: relative; overflow: hidden;
}
.hero__scroll i::after {
  content: ""; position: absolute; inset: 0;
  background: var(--text);
  animation: trickle 2.2s var(--ease) infinite;
}
@keyframes trickle {
  0%   { transform: translateY(-100%); }
  60%,100% { transform: translateY(100%); }
}
@media (max-width: 720px) { .hero__scroll { display: none; } }

/* ---------- sections ---------- */

.section { padding-block: clamp(5rem, 12vh, 9rem); position: relative; }
.section__head { margin-bottom: clamp(2.5rem, 6vw, 4rem); }
.section__head--row {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 2rem; flex-wrap: wrap;
}
.section__head .display { margin-bottom: 0; }

/* ---------- album ---------- */

.album { background: linear-gradient(180deg, var(--bg), var(--bg-2) 60%, var(--bg)); }
.album__grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}
@media (max-width: 900px) { .album__grid { grid-template-columns: 1fr; } }

.album__art { position: sticky; top: calc(var(--header-h) + 3rem); }
@media (max-width: 900px) { .album__art { position: static; } }

/* Hier stand eine 3D-Neigung mit weissem Glanzfleck darauf (.tilt/.tilt__shine,
   dazu Punkt 6 in main.js). Beides ist raus, und zwar aus demselben Grund, aus
   dem border-radius raus ist: eine gekippte Kachel mit wanderndem Glanz ist die
   Sprache einer Produktseite, und sie lag ausgerechnet auf dem einen Bild, das
   die Seite gestalterisch traegt. Der Ersatz ist keine neue Erfindung, sondern
   das, was das Cover selbst tut — der waagerechte Slice-Versatz ueber den
   beiden Gesichtern, dieselbe Geste wie im Hero und auf den Release-Kacheln
   (s. "Der chromatische Versatz" in CLAUDE.md). Vier Punkte:

   - Er feuert und faellt zusammen, er bleibt nicht stehen. Ein dauerhaft
     versetztes Cover liest sich als kaputtes Bild. Die Animation laeuft einmal
     pro Hover und endet auf Deckkraft 0 — danach steht wieder das unberuehrte
     Artwork da.
   - step-end, wie beim Hero-Glitch: es wird nichts interpoliert, also faellt
     ein Repaint nur an den Keyframe-Grenzen an. Ein Slice springt ohnehin.
   - Der Versatz steht in Prozent der Kachelbreite, nicht in px. Die Spalte
     laeuft von 317 px bis ueber 500 px; ein fester Wert waere unten ein Klotz.
   - Kein Filter und keine Einfaerbung. Das Cover bringt Rot und Tuerkis selbst
     mit, und "Die Artworks nicht filtern" gilt hier wie ueberall — verschoben
     wird, nicht abgedunkelt.

   Nur fuer echte Zeiger: auf Touch bleibt :hover nach dem Tippen haengen, und
   ein Cover, das nach einer Beruehrung zerschnitten stehen bleibt, ist genau
   der Zustand, den Punkt 1 ausschliesst. */
.slice {
  position: relative;
  overflow: hidden;
  box-shadow: 0 30px 80px -30px rgba(0,0,0,.9), 0 0 0 1px var(--line-soft);
}
.slice img { width: 100%; }

/* Ohne echten Zeiger gibt es den Hover nie — dann duerfen die drei Baender aber
   auch nicht im Fluss stehen: als leere Inline-Spans haengen sie dem Cover sonst
   eine Zeilenhoehe unter das Bild, innerhalb des Rahmens. Der Grundzustand ist
   deshalb display:none und nicht bloss "keine Animation"; erst die Media Query
   unten stellt sie auf. */
.slice__b { display: none; }

@media (hover: hover) and (pointer: fine) {
  /* Der Pfad steht hier und nicht als --art im style-Attribut des Markups, und
     das ist keine Vorliebe: eine relative URL in einer Custom Property loest
     der Browser gegen die Basis des Stylesheets auf, in dem das var() steht —
     nicht gegen die des Dokuments, in dem die Eigenschaft deklariert wurde.
     Aus `url(assets/images/cover-0A.jpg)` im HTML wird damit still
     `assets/css/assets/images/cover-0A.jpg`, und die Baender bleiben leer.
     Ein `/assets/...` wuerde das aufloesen, bricht aber unter file:// — und die
     Seite wird lokal auch so geoeffnet (s. der ?v=-Hinweis in CLAUDE.md).
     Stylesheet-relativ stimmt in beiden Faellen. */
  .slice__b {
    display: block;
    position: absolute; inset: 0;
    background-image: url(../images/cover-0A.jpg);
    background-size: cover;
    background-position: center;
    opacity: 0;
    pointer-events: none;
  }
  /* Drei Baender auf verschiedenen Hoehen, verschieden weit und verschieden
     herum versetzt — gleich weit waere es ein Doppelbild statt eines Schnitts.
     Das <img> ist Kind 1, die Baender sind 2 bis 4. */
  .slice__b:nth-child(2) { clip-path: inset(21% 0 66% 0); --sx: -3.2%; --sd: 0s;    }
  .slice__b:nth-child(3) { clip-path: inset(47% 0 38% 0); --sx:  2.4%; --sd: .05s;  }
  .slice__b:nth-child(4) { clip-path: inset(71% 0 14% 0); --sx: -1.6%; --sd: .11s;  }

  .slice:hover .slice__b {
    animation-name: sliceHit;
    animation-duration: .62s;
    animation-timing-function: step-end;
    animation-delay: var(--sd, 0s);
  }
}
@keyframes sliceHit {
  0%, 100% { opacity: 0; transform: translate3d(0, 0, 0); }
  6%       { opacity: 1; transform: translate3d(var(--sx), 0, 0); }
  22%      { opacity: 0; transform: translate3d(0, 0, 0); }
  38%      { opacity: 1; transform: translate3d(calc(var(--sx) * -.6), 0, 0); }
  52%      { opacity: 0; transform: translate3d(0, 0, 0); }
  66%      { opacity: 1; transform: translate3d(calc(var(--sx) * .35), 0, 0); }
  80%      { opacity: 0; transform: translate3d(0, 0, 0); }
}

.album__credit {
  margin-top: 1rem;
  font-size: .72rem; letter-spacing: .16em; text-transform: uppercase;
  color: #55555c;
}

/* Zwei Knoepfe seit den Release-Seiten: der Bandcamp-Knopf bleibt der direkte
   Weg zum Kauf, daneben steht der Verteiler auf l/0a/. Dieselbe Geometrie wie
   .hero__actions, damit das Paar an beiden Stellen gleich sitzt. */
.album__actions { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: 2rem; }

.album__stat {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 1rem;
  margin: 0;
  font-family: var(--display);
  font-size: .78rem; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--muted);
}
.album__stat i { width: 4px; height: 4px; border-radius: 50%; background: var(--red); }

.feats {
  list-style: none; margin: 2rem 0 0; padding: 0;
  border-top: 1px solid var(--line-soft);
}
.feats li {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: .85rem 0;
  border-bottom: 1px solid var(--line-soft);
  font-family: var(--display);
  font-weight: 600; font-stretch: 108%;
  font-size: clamp(1rem, .9rem + .5vw, 1.3rem);
  text-transform: uppercase; letter-spacing: .01em;
  transition: color .3s var(--ease), padding-left .4s var(--ease);
}
.feats li:hover { color: var(--teal); padding-left: .6rem; }
.feats span {
  font-family: var(--body);
  font-size: .78rem; font-weight: 400;
  letter-spacing: .12em;
  color: var(--muted);
  text-transform: uppercase;
}

/* tracklist */
.tracklist { margin-top: 2.5rem; border-top: 1px solid var(--line); }
.tracklist summary {
  list-style: none;
  cursor: pointer;
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.1rem 0;
  font-family: var(--display);
  font-size: .8rem; font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--muted);
  transition: color .3s var(--ease);
}
.tracklist summary::-webkit-details-marker { display: none; }
.tracklist summary:hover { color: var(--text); }
.tracklist summary i {
  width: 12px; height: 12px; position: relative; flex: none;
}
.tracklist summary i::before,
.tracklist summary i::after {
  content: ""; position: absolute; inset: 50% 0 auto 0; height: 1.5px;
  background: currentColor;
  transition: transform .4s var(--ease);
}
.tracklist summary i::after { transform: rotate(90deg); }
.tracklist[open] summary i::after { transform: rotate(0deg); }
.tracklist[open] summary { color: var(--text); }

.tracklist ol {
  list-style: none; margin: 0; padding: 0 0 1.5rem;
  animation: fadeDown .55s var(--ease) both;
}
@keyframes fadeDown {
  from { opacity: 0; transform: translateY(-8px); }
}
.tracklist li {
  display: flex; gap: 1rem; align-items: baseline;
  padding: .6rem 0;
  border-bottom: 1px solid var(--line-soft);
  transition: color .3s var(--ease);
}
.tracklist li:hover { color: var(--teal); }
.t__no {
  font-family: var(--display); font-size: .72rem; font-weight: 700;
  letter-spacing: .1em; color: var(--red); flex: none; width: 2ch;
}
.t__name { font-weight: 400; }
.t__name em {
  display: block;
  font-style: normal;
  font-size: .78rem; letter-spacing: .06em;
  color: var(--muted);
}

/* ---------- news ---------- */

/* Der Abschnitt steht zwischen dem auf --bg auslaufenden .album und der
   --bg-2-Flaeche von .press — nach oben liegt also gleiche Farbe auf gleicher
   Farbe, und zwischen der letzten Albumzeile und der Eyebrow stehen 240 px
   Leere, in der nichts sagt, dass hier ein neuer Abschnitt anfaengt. Die Regel
   verlangt fuer einen abgesetzten Abschnitt eins von beidem: weiche Kante oder
   Haarlinie. Hier ist es die Haarlinie, ueber die volle Fensterbreite wie bei
   .press, plus ein rotes Stueck an ihrem Anfang — dieselbe Farbe, die unten den
   neuesten Eintrag markiert, und die einzige Stelle, an der die Linie
   ueberhaupt gesehen wird: sie sitzt buendig unter der Eyebrow-Spalte. */
.news { border-top: 1px solid var(--line); }
.news::before {
  content: ""; position: absolute; top: -1px; height: 1px;
  /* buendig mit der Textspalte, nicht am Fensterrand: die Spalte wird auf
     dieser Seite genau einmal verlassen, beim Duo-Foto. Das ist die linke
     Innenkante des .wrap, ohne dass hier ein Element im .wrap noetig waere. */
  left: max(var(--gut), (100% - var(--wrap)) / 2 + var(--gut));
  width: clamp(72px, 12vw, 132px);
  background: var(--red);
}

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

.news__item {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: clamp(.6rem, 4vw, 3rem);
  padding-block: clamp(1.8rem, 4vw, 2.6rem);
  border-top: 1px solid var(--line);
}
.news__item:first-child { padding-top: 0; border-top: 0; }

/* Der einzige Schmuck des Abschnitts, und er sagt etwas: rot markiert den
   neuesten Eintrag. Bei nur einem Eintrag traegt der Punkt die Zeile, damit
   die Liste nicht wie ein leerer Rest aussieht. */
.news__date {
  display: flex; align-items: baseline; gap: .7rem;
  margin: 0;
  font-family: var(--display);
  font-size: .78rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted);
}
.news__date::before {
  content: ""; flex: none;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--line);
  transform: translateY(-1px);
}
.news__item:first-child .news__date { color: var(--text); }
.news__item:first-child .news__date::before { background: var(--red); }

.news__title {
  margin: 0 0 .6rem;
  font-family: var(--display);
  font-weight: 700; font-stretch: 106%;
  font-size: clamp(1.25rem, 1.05rem + .9vw, 1.9rem);
  line-height: 1.15;
  letter-spacing: -.005em;
}
.news__body p { max-width: 54ch; color: #c9c8cc; }

@media (max-width: 720px) {
  .news__item { grid-template-columns: 1fr; }
  .news__date { margin-bottom: .9rem; }
}

/* ---------- press ---------- */

.press { background: var(--bg-2); border-block: 1px solid var(--line-soft); }

/* Drei Reihen, drei Breiten: eine Karte, zwei, dann drei. Mit sechs gleich
   breiten Kacheln unter der roten Flaeche liest sich der Abschnitt als Tabelle —
   derselbe Fehler, den .releases mit vier gleichen Quadraten hatte. Statt eines
   zweiten Effekts traegt die Reihenfolge die Abstufung: die Zitate werden von
   oben nach unten schmaler und kleiner, das Auge laeuft die Treppe hinunter.
   Sechs Spalten sind die kleinste Zahl, in der sich 2 und 3 pro Reihe ohne
   Bruch aufteilen lassen (span 3 und span 2).
   Kein auto-fit mehr: die Spuren muessen zaehlbar sein, sonst kann eine Karte
   nicht die halbe Reihe belegen. */
.press__list {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: clamp(1.8rem, 4vw, 3.2rem);
}
.pq { grid-column: span 2; }
/* Die mittlere Stufe. Breiter als drei Karten waeren, schmaler als die rote
   Flaeche — und der Schriftgrad sitzt genauso dazwischen (49,6 / 32,6 / 25,6 px
   bei 1440). Weniger als 260px Textbreite traegt der Grad nicht, deshalb sind
   es zwei und nicht drei pro Reihe. */
.pq--wide { grid-column: span 3; }

.pq { position: relative; padding-top: 1.6rem; border-top: 1px solid var(--line); }
/* the opening quote mark doubles as the section's only ornament */
.pq::before {
  content: "“";
  position: absolute; top: -.35em; left: -.06em;
  font-family: var(--display); font-weight: 800;
  font-size: 4rem; line-height: 1;
  color: var(--red);
  opacity: .35;
  pointer-events: none;
}
/* Die erste echte Farbflaeche der Seite. Rot kam bis hierhin nur als Button,
   Punkt und Haarlinie vor — auf einer Seite, deren Palette aus einem roten
   Cover stammt. Weiss auf --red liegt bei 7,8:1, der Text bleibt also auch in
   .72rem lesbar; die Haarlinie und das ::before-Ornament weichen der Flaeche. */
.pq--lead {
  grid-column: 1 / -1;
  border-top: 0;
  background: linear-gradient(152deg, var(--red), #94232a);
  /* Die untere Grenze oben traegt das 4rem-Anfuehrungszeichen: bei 2.4rem sass
     es auf der ersten Zeile auf, sichtbar erst auf dem Handy, wo 5vw den
     Abstand nicht mehr aufzieht. */
  padding: clamp(3.2rem, 5vw, 3.8rem) clamp(1.6rem, 4vw, 3.2rem) clamp(1.8rem, 4vw, 2.8rem);
  box-shadow: 0 30px 70px -40px rgba(197,50,56,.8);
}
.pq--lead::before {
  top: .12em; left: clamp(1.4rem, 3.6vw, 2.9rem);
  color: #fff;
  opacity: .28;
}
/* Kein <mark>-Gelb: nur die Farbe, kein Kasten */
.pq--lead mark { background: none; color: var(--teal); }
.pq--lead .pq__src a { color: #fff; }
.pq--lead .pq__src a:hover { border-bottom-color: #fff; }
.pq--lead .pq__src span { color: rgba(255,255,255,.78); }

/* Die dritte Farbflaeche der Seite und die erste in Tuerkis. Rot hatte mit
   .pq--lead und .video zwei, Tuerkis war ausschliesslich Text, Eyebrow und
   Haarlinie — auf einer Palette, die zur Haelfte aus Tuerkis besteht.
   Sie steht bewusst in derselben Sektion wie die rote Karte: Tuerkis neben Rot
   ist die Kombination vom 0A-Cover, also belegt und nicht erfunden. Und sie
   steht in der Mitte der Dreierreihe, nicht am Anfang — links unter der roten
   Karte laesse sie sich als deren Fortsetzung lesen.
   Kontrast (Gegenstueck zur --red-Tabelle in CLAUDE.md):
     #08080a auf --teal   11,6 : 1   alles, auch die .72rem-Quellenzeile
     #fff    auf --teal    1,5 : 1   nichts
   Das Vorzeichen dreht sich hier also: auf Rot ist Weiss die einzige sichere
   Farbe, auf Tuerkis ist es Schwarz. */
.pq--teal {
  border-top: 0;
  background: var(--teal);
  color: #08080a;
  padding: clamp(2.8rem, 4vw, 3.2rem) clamp(1.4rem, 3vw, 2rem) clamp(1.6rem, 3vw, 2rem);
}
/* Das Anfuehrungszeichen haengt bei .pq ausserhalb des Kastens (top: -.35em) —
   auf einer Flaeche muss es hinein, wie bei .pq--lead. */
.pq--teal::before {
  top: .1em; left: clamp(1.2rem, 2.6vw, 1.7rem);
  color: #08080a;
  opacity: .32;
}
.pq--teal .pq__src a { color: #08080a; }
.pq--teal .pq__src a:hover { border-bottom-color: #08080a; }
/* 6,25 : 1 auf --teal — traegt die 11,5-px-Zeile noch. */
.pq--teal .pq__src span { color: rgba(8,8,10,.72); }

.pq blockquote {
  margin: 0 0 1.2rem;
  font-family: var(--display);
  font-weight: 500; font-stretch: 106%;
  font-size: clamp(1.1rem, 1rem + .55vw, 1.6rem);
  line-height: 1.28;
  letter-spacing: -.005em;
  color: var(--text);
}
.pq--lead blockquote {
  font-weight: 700;
  font-size: clamp(1.5rem, 1.1rem + 2.4vw, 3.1rem);
  line-height: 1.1;
  max-width: 22ch;
}
/* Muss hier unten stehen, nicht oben beim Rest von .pq--teal: `.pq blockquote`
   setzt color auf --text und hat mit (0,1,1) dieselbe Spezifitaet wie
   `.pq--teal blockquote` — bei Gleichstand entscheidet die Reihenfolge, und
   oben stehend verliert die Regel. Genau deshalb steht .pq--lead blockquote
   ebenfalls hier. */
.pq--teal blockquote { color: #08080a; }
/* Mittlere Stufe zwischen .pq--lead (700) und .pq (500) — der Grad allein
   traegt den Unterschied nicht, wenn die Karte nur anderthalb mal so breit ist. */
.pq--wide blockquote {
  font-weight: 600;
  font-size: clamp(1.3rem, 1.05rem + 1.1vw, 2.05rem);
  line-height: 1.18;
}
.pq--wide::before { font-size: 5rem; }
.pq blockquote em { font-style: italic; }

.pq__src { display: flex; flex-direction: column; gap: .2rem; margin: 0; }
.pq__src a {
  align-self: flex-start;
  font-family: var(--display);
  font-size: .78rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  text-decoration: none;
  color: var(--teal);
  border-bottom: 1px solid transparent;
  transition: border-color .35s var(--ease);
}
.pq__src a:hover { border-bottom-color: var(--teal); }
.pq__src span {
  font-size: .72rem; letter-spacing: .14em; text-transform: uppercase;
  color: #55555c;
}

/* Zwei Spalten statt sechs: die breite Karte nimmt die ganze Reihe, die
   schmalen stehen zu zweit. Die Treppe bleibt damit erhalten, nur eine Stufe
   flacher. */
@media (max-width: 900px) {
  .press__list { grid-template-columns: repeat(2, 1fr); }
  .pq { grid-column: span 1; }
  .pq--wide { grid-column: span 2; }
}
@media (max-width: 520px) {
  .press__list { grid-template-columns: 1fr; }
  .pq, .pq--wide { grid-column: span 1; }
}

.section__quote {
  margin: 1.2rem 0 0;
  display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap;
  font-size: clamp(1rem, .95rem + .35vw, 1.2rem);
  color: #c9c8cc;
}
.section__quote a {
  font-family: var(--display);
  font-size: .72rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  text-decoration: none;
  color: var(--teal);
  border-bottom: 1px solid transparent;
  transition: border-color .35s var(--ease);
}
.section__quote a:hover { border-bottom-color: var(--teal); }

/* ---------- video ---------- */

/* Die zweite Farbflaeche. Zwischen der Press-Karte und dem Footer lief die
   Seite vorher drei Bildschirmhoehen ohne Farbe durch. Derselbe Verlauf wie
   .pq--lead, damit die beiden roten Flaechen als dieselbe Geste lesen.
   Ein harter Farbwechsel an den Kanten ist hier richtig — die Regel gegen
   nackte Kanten gilt fuer den Sechs-Stufen-Sprung von --bg-2 auf --bg, der
   sich als Rechteck im Hintergrund liest. Rot auf Schwarz ist unmissverstaend-
   lich Absicht.
   Kontrast auf --red (Tabelle in CLAUDE.md): Weiss 7,8:1, Tuerkis nur 4,55:1 —
   also fuer Fliesstext ab 16 px und Headlines brauchbar, aber nicht fuer die
   .72rem-Eyebrow und die .72rem-Quelle darunter. Beide stehen deshalb hier
   weiss, nicht tuerkis. */
.video {
  background: linear-gradient(152deg, var(--red), #94232a);
}
.video .eyebrow { color: #fff; }
.video .eyebrow i { background: #fff; }
.video .section__quote { color: rgba(255,255,255,.92); }
.video .section__quote a { color: #fff; }
.video .section__quote a:hover { border-bottom-color: #fff; }
/* #55555c waere auf Rot nicht mehr zu sehen */
.video .player__note { color: rgba(255,255,255,.82); }
/* Der rote Knopf verschwand auf dem rosa Artwork fast — und ein rotes Element
   auf roter Flaeche ist ohnehin kein Signal mehr. Weiss mit rotem Dreieck. */
.video .player__play { background: #fff; }
.video .player__play svg { fill: var(--red); }
.video .player__facade:hover .player__play { background: var(--teal); }
/* Der schwarze Rahmen des Clips (s. .player__facade) liest sich auf Rot als
   gesetzter Rahmen — auf Schwarz sah er aus wie ein kaputtes Thumbnail. */
.video .player {
  box-shadow: 0 40px 90px -40px rgba(0,0,0,.65), 0 0 0 1px rgba(0,0,0,.35);
}

.player {
  position: relative;
  overflow: hidden;
  box-shadow: 0 40px 90px -40px rgba(0,0,0,.95), 0 0 0 1px var(--line-soft);
}
.player__facade {
  display: block; width: 100%; position: relative;
  padding: 0; border: 0; background: #000; cursor: pointer;
  aspect-ratio: 16 / 9;
}
.player__facade img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform .8s var(--ease), filter .6s var(--ease);
}
.player__facade:hover img { transform: scale(1.04); filter: brightness(.82); }
/* Verlauf statt brightness-Filter auf dem ganzen Bild: das Artwork bleibt
   ungedaempft, abgedunkelt wird nur die untere Kante, wo .player__meta steht.
   ::after ist die letzte Kindbox und laege sonst ueber Button und Text — beide
   bekommen deshalb z-index: 1. */
.player__facade::after {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(to top, rgba(8,8,10,.88), rgba(8,8,10,.2) 40%, transparent 68%);
  pointer-events: none;
}

.player__play {
  z-index: 1;
  position: absolute; inset: 0; margin: auto;
  width: clamp(64px, 8vw, 96px); height: clamp(64px, 8vw, 96px);
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--red);
  box-shadow: 0 0 0 0 rgba(197,50,56,.5);
  transition: transform .45s var(--ease), background .35s var(--ease);
}
.player__play svg { width: 42%; height: 42%; fill: #fff; margin-left: 8%; }
.player__facade:hover .player__play { transform: scale(1.12); background: var(--red-hot); }

/* Der Iframe wird ueber das Poster gelegt und eingeblendet, statt es zu
   ersetzen. Vorher stand in main.js Punkt 7 ein replaceWith(): das Bild war
   mit dem Klick weg, und bis YouTube antwortete, stand an seiner Stelle ein
   leerer schwarzer Kasten — auf einer langsamen Verbindung dasselbe Bild wie
   beim alten mask-Vorhang, naemlich "laedt noch". Drei Punkte:

   - Das Poster bleibt im DOM und traegt weiter die Hoehe. Wuerde es entfernt,
     fiele der 16:9-Kasten in sich zusammen, sobald der Iframe noch nichts
     rendert. Es geht ueber visibility raus und nicht ueber display, damit die
     Deckkraft ueberhaupt animierbar bleibt — und damit der Button danach aus
     der Tab-Reihenfolge faellt.
   - Der Iframe ist bis dahin nicht anklickbar. Er liegt mit z-index darueber;
     ohne pointer-events: none klickte man waehrend des Ladens ins Unsichtbare
     statt auf das Poster.
   - Das Poster bleibt :first-child, der mask-Vorhang haengt also weiter daran
     (s. [data-reveal="mask"] > :first-child). Der Iframe wird angehaengt, nicht
     eingeschoben. */
.player__frame {
  position: absolute; inset: 0; z-index: 2;
  width: 100%; height: 100%; border: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity .45s var(--ease);
}
.player.is-playing .player__frame { opacity: 1; pointer-events: auto; }
.player.is-playing .player__facade {
  opacity: 0; visibility: hidden;
  transition: opacity .45s var(--ease), visibility 0s .45s;
}

/* Waehrend des Ladens laeuft eine Haarlinie ueber die Unterkante. Sie ist die
   Antwort auf den Klick — ohne sie steht das Poster unveraendert da und der
   Klick sieht aus, als waere er ins Leere gegangen.
   Warum eine Linie und kein Spinner: die Seite kennt Haarlinien als Sprache
   (--line, .eyebrow i, die Kante unter News) und kein einziges rundes
   Ladesymbol. Und es ist eine reine transform-Animation auf einem kleinen
   Element — sie laeuft im Compositor und hoert nach spaetestens 4 s wieder auf
   (Netz in main.js Punkt 7), ist also keiner der dauerlaufenden
   Vollflaechen-Effekte, vor denen die Animations-Notiz warnt.

   Unten und nicht oben, und das ist keine Willkuer: die Oberkante des Posters
   sind gelb-schwarze Warnstreifen aus dem Artwork, auf denen eine rote Linie
   verschwindet. Unten daempft .player__facade::after dieselben Streifen — der
   Verlauf, der .player__meta lesbar haelt.

   Gedaempft ist aber nicht dunkel, und der Balken soll nicht davon abhaengen,
   was das Artwork an dieser Stelle zeigt. .player::after legt deshalb eine
   dunkle Schiene ueber die volle Breite darunter; das Rot laeuft darauf und
   nicht auf dem Motiv. Beide zusammen lesen ausserdem als das, was sie sind:
   eine Leiste, auf der etwas laeuft. */
.player::after {
  content: ""; position: absolute; z-index: 2;
  bottom: 0; left: 0; height: 3px; width: 100%;
  background: rgba(8,8,10,.78);
  opacity: 0;
  transition: opacity .2s var(--ease);
  pointer-events: none;
}
.player.is-loading::after { opacity: 1; }

.player::before {
  content: ""; position: absolute; z-index: 3;
  bottom: 0; left: 0; height: 3px; width: 28%;
  background: var(--red);
  opacity: 0;
  transform: translate3d(-100%, 0, 0);
  pointer-events: none;
}
.player.is-loading::before {
  opacity: 1;
  animation: playerLoad 1.15s cubic-bezier(.65,0,.35,1) infinite;
}
/* 100/28 = 357 % der eigenen Breite bis zur rechten Kante, plus 100 %, damit
   das Segment vollstaendig hinauslaeuft. .player hat overflow: hidden. */
@keyframes playerLoad {
  from { transform: translate3d(-100%, 0, 0); }
  to   { transform: translate3d(457%, 0, 0); }
}

.player__meta {
  position: absolute; z-index: 1;
  left: clamp(1rem,3vw,2rem); bottom: clamp(1rem,3vw,2rem);
  display: flex; flex-direction: column; align-items: flex-start; gap: .2rem;
  text-align: left;
}
.player__meta b {
  font-family: var(--display); font-weight: 700; font-stretch: 112%;
  font-size: clamp(1rem, .9rem + .6vw, 1.5rem);
  text-transform: uppercase; letter-spacing: .01em; color: var(--text);
}
.player__meta i {
  font-style: normal; font-size: .72rem; letter-spacing: .2em;
  text-transform: uppercase; color: var(--teal);
}
.player__note {
  margin-top: 1rem; font-size: .78rem; color: #55555c; letter-spacing: .04em;
}
.player iframe { width: 100%; aspect-ratio: 16/9; border: 0; display: block; }

/* ---------- quotes ---------- */

/* Kein harter Sprung an den Kanten. Von #0e0e11 auf #08080a sind es nur sechs
   Stufen Grau, aber die Kante läuft über die volle Breite: beim Scrollen liest
   sie sich als Rechteck, das plötzlich aufpoppt — sichtbar wurde das direkt
   unter dem Duo-Foto, wo diese Kante 116 px tiefer sitzt. .album blendet dafür
   schon immer auf --bg aus, .press rahmt seine Kanten mit Haarlinien; .quotes
   hatte weder das eine noch das andere. */
.quotes {
  background: linear-gradient(180deg,
    var(--bg), var(--bg-2) 12%, var(--bg-2) 88%, var(--bg));
}
/* Die Karten greifen in die Unterkante des Fotos, statt darunter zu stehen: die
   beiden verzahnen sich, und die Zuordnung ist ohne Avatar lesbar — Diana links
   im Bild (gespiegelt, s. .portrait img) wie in der Karte, Boog rechts. z-index,
   weil .portrait als spaeterer Nachbar sonst obenauf liegt. */
.quotes__grid {
  display: grid; gap: clamp(1.5rem, 3vw, 2.5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
  position: relative;
  z-index: 1;
  margin-top: calc(-1 * clamp(1.5rem, 4vw, 3.5rem));
}
.quote {
  margin: 0;
  padding: clamp(1.6rem, 3vw, 2.6rem);
  background: var(--surface);
  border: 1px solid var(--line-soft);
  position: relative;
  overflow: hidden;
  transition: border-color .45s var(--ease), transform .45s var(--ease);
}
.quote::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--red);
  transform: scaleY(0); transform-origin: 0 0;
  transition: transform .7s var(--ease);
}
.quote--alt::before { background: var(--teal); }
.quote.is-in::before { transform: scaleY(1); }
.quote:hover { border-color: rgba(255,255,255,.16); transform: translateY(-4px); }

/* the statements run long — smaller and airier than a pull quote, or the two
   cards read as a wall of text */
.quote blockquote {
  margin: 0 0 1.6rem;
  font-size: clamp(.96rem, .92rem + .28vw, 1.08rem);
  line-height: 1.72;
  color: #e2e1e0;
}
.quote blockquote::before { content: "“"; color: var(--red); font-size: 2em; line-height: 0; vertical-align: -.35em; margin-right: .1em; }
.quote--alt blockquote::before { color: var(--teal); }

.quote figcaption { display: flex; flex-direction: column; gap: .1rem; }
.quote figcaption b {
  font-family: var(--display); font-weight: 700; font-stretch: 112%;
  text-transform: uppercase; letter-spacing: .1em; font-size: .95rem;
}
.quote figcaption span {
  font-size: .72rem; letter-spacing: .18em; text-transform: uppercase; color: var(--muted);
}

/* Volle Fensterbreite, ausserhalb des .wrap. Das Foto ist die einzige Stelle,
   an der die Seite ihre 1180-px-Spalte verlaesst — genau deshalb sitzt es hier
   und nicht im Rahmen. Kein border-radius: eine gerundete Ecke am Fensterrand
   sieht nach vergessener Kachel aus. */
.portrait {
  margin-top: clamp(2.5rem, 6vw, 4.5rem);
  overflow: hidden;
  max-height: 62vh;
}
/* Das Foto füllt seinen Rahmen exakt aus — ohne den Zoom liefe es beim Scrollen
   an der Unterkante heraus und legte einen schwarzen Streifen frei. 8 % nach
   jeder Seite decken den Weg von speed/2 = 6 % ab.
   Negatives scaleX spiegelt das Bild: Diana steht damit links, wie in der ersten
   Karte darunter. Nur hier gespiegelt, nicht im Hero — dort traegt dasselbe Foto
   keine Zuordnung. */
.portrait img {
  width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(.15) contrast(1.05);
  transform: translate3d(0, var(--parallax-y, 0px), 0) scale(-1.16, 1.16);
  will-change: transform;
}

/* ---------- discography ---------- */

/* Vier gleich grosse Quadrate im Raster waren der generischste Block der Seite.
   Die erste Spalte ist breiter: 0A ist das aktuelle Album, um das die ganze
   Seite gebaut ist, die drei anderen sind Singles und eine EP. Weil
   .release__art aspect-ratio: 1 hat, wird die breitere Spalte automatisch zur
   groesseren Flaeche, und die Bildunterschrift der 0A-Kachel sitzt tiefer als
   die der anderen drei — die Reihe bekommt damit eine Kante, ohne dass ein
   zweiter Effekt noetig waere.
   Kein auto-fill mehr: das liess bei 1180 px eine fuenfte Spur entstehen, die
   leer blieb. Die Spaltenzahl ist jetzt an jedem Breakpoint gesetzt. */
.releases {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: clamp(1rem, 2.2vw, 1.8rem);
  grid-template-columns: 1.4fr 1fr 1fr 1fr;
  align-items: start;
}
@media (max-width: 900px) { .releases { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .releases { grid-template-columns: 1fr; } }
.release a {
  display: block; text-decoration: none;
  transition: transform .5s var(--ease);
}
.release a:hover { transform: translateY(-6px); }
.release__art {
  display: block; position: relative;
  overflow: hidden;
  aspect-ratio: 1;
  box-shadow: 0 18px 44px -22px rgba(0,0,0,.9), 0 0 0 1px var(--line-soft);
}
.release__art::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, transparent 40%, rgba(197,50,56,.35));
  opacity: 0; transition: opacity .45s var(--ease);
}
.release a:hover .release__art::after { opacity: 1; }
/* Kein saturate/brightness im Ruhezustand. Die vier Cover sind die einzige
   Stelle, an der die Band selbst laut wird — pinkes Oxytocin, blauer
   Mittelfinger —, und ein Filter drehte sie auf einer ohnehin grauen Seite
   leiser. Das Hover-Signal traegt jetzt allein scale + .release__art::after. */
.release__art img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform .7s var(--ease);
}
.release a:hover .release__art img { transform: scale(1.06); }

.release__meta { display: flex; flex-direction: column; gap: .15rem; margin-top: .9rem; }
.release__meta b {
  font-family: var(--display); font-weight: 700; font-stretch: 108%;
  font-size: .95rem; text-transform: uppercase; letter-spacing: .03em;
}
/* Derselbe chromatische Versatz wie beim Hero-Reveal (s. .hero__title .ch),
   hier aber in px und ohne Transition. Drei Abweichungen mit Grund:
   - px statt em, weil .055em bei .95rem ein Achtel Pixel waere.
   - Er schnappt, statt einzublenden. Ein Glitch, der ueber .35s weich
     aufzieht, ist kein Glitch mehr.
   - 1 px, nicht 2. Der Versatz muss im Verhaeltnis zur Schrifthoehe stehen,
     nicht absolut: 2 px auf 15,2 px sind 13 %, im Hero sind es 5,5 % — bei
     einem zweizeichigen Titel wie "0A" las sich das als Schmier statt als
     Schnitt. 1 px liegt bei 6,6 % und bleibt trotzdem eine harte Kante.
   Er ersetzt den Farbwechsel auf Tuerkis, der hier vorher stand: ein tuerkiser
   Schatten neben tuerkiser Schrift ist unsichtbar, und der Titel bleibt weiss,
   damit beide Kanten sichtbar sind. Das Hover-Signal traegt ohnehin schon
   scale + .release__art::after. */
.release a:hover .release__meta b {
  text-shadow: -1px 0 0 var(--red), 1px 0 0 var(--teal);
}
.release__meta i {
  font-style: normal; font-size: .72rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted);
}
/* Die Dienste stehen als Namen unter der Kachel — nicht als Knoepfe darauf.
   Ein Hover-Overlay waere die Smartlink-Seite ein zweites Mal: dieselbe Liste
   an einer Stelle, die niemand nachzieht, wenn in
   smartlink/releases/<slug>.json ein Dienst dazukommt, und ohne deren
   Reihenfolge-Aussage (Bandcamp zuerst, rot) — fuenf gleich grosse Logos sind
   eine Auswahl, kein Vorschlag. Dazu drei praktische Gruende: auf Touch gibt
   es Hover nicht, fuenf lesbare Knoepfe brauchen auf 227 px praktisch das
   ganze Cover, und sie brauchen einen Scrim darueber — genau die Abdunklung,
   die zehn Zeilen weiter oben bewusst rausgeflogen ist.
   Ein drittes Grau gibt es dafuer nicht: unterschieden wird ueber Groesse und
   Laufweite, wie bei Jahr und Label in .discog. Gemischte Schreibung statt
   Versalien haelt die Zeile als Aufzaehlung lesbar und nicht als zweites
   Meta-Label.
   Sie laeuft je nach Fensterbreite ein- oder zweizeilig, und das ist keine
   Nachlaessigkeit: die schmalen Kacheln sind bei 1440 px 227 px breit, bei
   1100 px nur 210 px, die laengste Zeile braucht 230 px. Einzeilig ueber den
   ganzen Bereich ginge nur mit ~9 px Schrift. Statt dessen bricht sie
   ausgeglichen um (text-wrap: balance) — sonst steht "YouTube" allein in der
   zweiten Zeile. Die Trennzeichen haengen im Markup per NBSP am Wort davor
   und "Apple Music" ist ebenso zusammengebunden: ohne das beginnt die zweite
   Zeile mit einem "·" oder der Name selbst reisst auseinander.
   Namen wie im Katalog, also "Apple Music" und nicht "Apple":
   die Kurzform spart eine Zeile bei etwa jeder zweiten Fensterbreite und
   kostet dafuer einen zweiten Namen fuer denselben Dienst — die
   Smartlink-Seite dahinter schreibt ihn aus. */
.release__svc {
  margin-top: .2rem;
  font-size: .68rem; line-height: 1.5;
  letter-spacing: .01em; color: var(--muted);
  text-wrap: balance;
}

/* volle Discography unter den Kacheln
   Jahr und Label sind beide Metadaten und stehen deshalb auf demselben Grau;
   getrennt werden sie durch Position, Groesse und Laufweite, nicht durch Farbe.
   Ein drittes, dunkleres Grau waere hier naheliegend und faellt durch: #6e6e77
   auf --bg sind 3,96:1, und die Label-Zeile laeuft auf 10,9 px. */
.discog {
  margin-top: clamp(3.5rem, 8vw, 6rem);
  display: grid;
  gap: clamp(2.5rem, 5vw, 3.5rem);
}
.discog__title {
  margin: 0;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line);
  font-family: var(--display);
  /* 62 % wie .display — bei 12,8 px sieht man den Unterschied zu 74 % kaum,
     aber die Seite soll nicht zwei verschiedene schmale Werte kennen. */
  font-size: .8rem; font-weight: 700; font-stretch: 62%;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--muted);
}
.discog__list { list-style: none; margin: 0; padding: 0; }
.discog__list li {
  display: grid;
  grid-template-columns: 4.25rem minmax(0, 1fr) auto;
  gap: 1rem; align-items: baseline;
  padding: .8rem 0;
  border-bottom: 1px solid var(--line-soft);
}
.d__year {
  font-family: var(--display); font-weight: 700;
  font-size: .78rem; letter-spacing: .1em;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.d__title { font-size: 1.12rem; }
.d__title em {
  font-style: normal; font-size: .8rem; color: var(--muted);
  white-space: nowrap;
}
.d__label {
  font-family: var(--display);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted);
  text-align: right;
}

/* Die 17 EPs und Singles laufen zweispaltig — einspaltig waeren sie eine
   Kolonne, die laenger ist als der ganze Rest des Abschnitts. Multicol statt
   Grid, weil die Spalten sich dann selbst fuellen und die Reihenfolge dabei
   senkrecht bleibt: erst die Spalte zu Ende, dann die naechste. */
.discog__list--dense { columns: 2; column-gap: clamp(2rem, 5vw, 4rem); }
.discog__list--dense li { break-inside: avoid; padding: .62rem 0; }
.discog__list--dense .d__title { font-size: .98rem; }
@media (max-width: 900px) { .discog__list--dense { columns: 1; } }
@media (max-width: 520px) {
  .discog__list li { grid-template-columns: 3.5rem minmax(0, 1fr); row-gap: .2rem; }
  .d__label { grid-column: 2; text-align: left; }
}

/* ---------- ticker ---------- */

/* Zwei Spuren mit Songtiteln, gegenlaeufig, zwischen Duo-Abschnitt und
   Discography. Der Anlass ist inhaltlich, nicht dekorativ: die Titel sind das
   raueste Material, das die Band hat — "Fuck Your Pretty Face", "(C)Lick Me",
   "Naked Wet Hot" —, und sie standen bisher ausschliesslich in der
   Katalogliste auf 10,9 px Grau. Sie stehen hier woertlich, ohne Kuerzung,
   also gilt dieselbe Regel wie fuer Titel ueberall auf der Seite.

   Vier Punkte, an denen das haengt:

   - Volle Fensterbreite, und das ist der zweite Bruch der Spaltenkante nach
     dem Duo-Foto. Er nimmt dem ersten nichts, weil das Band gar keine linke
     Kante hat, an der man es mit den fuenf Abschnitten vergleichen koennte —
     es ist eine Bande wie die Progress-Leiste, kein Abschnitt. Wer daraus
     einen Abschnitt macht (Eyebrow, Headline, Inhalt), hat den Bruch dann
     doch zweimal.
   - transform statt margin/left. Ein dauerlaufender Vollbreite-Effekt hat den
     Pane hier schon einmal angehalten (s. Grain unter Animationen) — der Grund
     war ein Repaint pro Frame. translate3d auf zwei Elementen laeuft im
     Compositor und malt nichts neu. Deshalb auch keine Farb- oder
     Deckkraft-Animation an dieser Stelle.
   - Angehalten ausserhalb des Viewports, wie Hero-Video und Hero-Glitch
     (Punkt 9 in main.js). Die Klasse heisst is-offscreen und bedeutet "aus"
     statt "an": faellt das JS aus, laeuft das Band weiter statt zu stehen.
   - Die zwei Kopien pro Spur muessen identisch sein. Die Schleife springt bei
     -50 %, und das ist nur dann nahtlos, wenn die zweite Haelfte exakt die
     erste ist. Wer einen Titel ergaenzt, ergaenzt ihn zweimal. */
.ticker {
  border-block: 1px solid var(--line-soft);
  padding-block: clamp(1.6rem, 3vw, 2.6rem);
  display: grid;
  gap: clamp(.1rem, .6vw, .5rem);
  overflow: hidden;
  /* Die Titel laufen an beiden Fensterkanten aus statt dort abgeschnitten zu
     werden — eine harte Kante liest sich als abgeschnittener Text, nicht als
     laufendes Band. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
}
.ticker__row { overflow: hidden; }
.ticker__track {
  display: flex; align-items: center;
  width: max-content;
  will-change: transform;
  animation: tickerRun 46s linear infinite;
}
/* Die zweite Spur laeuft zurueck und langsamer. Gleichlaufend waeren es zwei
   Zeilen desselben Bandes; gegenlaeufig entsteht die Schere, die den Block
   traegt. */
.ticker__row--back .ticker__track {
  animation-name: tickerRunBack;
  animation-duration: 58s;
}
@keyframes tickerRun     { to { transform: translate3d(-50%, 0, 0); } }
@keyframes tickerRunBack { from { transform: translate3d(-50%, 0, 0); } }

.ticker.is-offscreen .ticker__track { animation-play-state: paused; }

.ticker__track span {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 62%;          /* schmaler Anschlag wie .display */
  font-size: clamp(2.4rem, 6.4vw, 5.6rem);
  line-height: 1.06;
  letter-spacing: -.01em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* Punkt als Interpunktion zwischen den Titeln — rund bleiben auf dieser Seite
   genau die Punkte und der Play-Knopf, s. "Rechte Winkel, keine Radien". */
.ticker__track i {
  flex: none;
  width: .42em; height: .42em;
  margin-inline: clamp(.9rem, 2.2vw, 2rem);
  border-radius: 50%;
  background: var(--red);
  font-size: clamp(2.4rem, 6.4vw, 5.6rem);
}
/* Die untere Spur steht als Kontur. Sie haelt den Block davon ab, zwei
   gleichwertige Zeilen zu sein, und die Haarlinie ist ohnehin die Sprache der
   Seite. -webkit-text-stroke faellt ohne Unterstuetzung auf gefuellten Text
   zurueck, nicht auf unsichtbaren — deshalb steht das transparente Fuellen im
   @supports und nicht daneben. */
.ticker__row--back .ticker__track span { color: rgba(255,255,255,.34); }
@supports (-webkit-text-stroke: 1px red) {
  .ticker__row--back .ticker__track span {
    color: transparent;
    -webkit-text-stroke: 1px rgba(255,255,255,.34);
  }
}
.ticker__row--back .ticker__track i { background: var(--teal); }

@media (max-width: 520px) {
  /* Auf dem Handy traegt eine Spur den Block allein — zwei gegenlaeufige
     Zeilen brauchen Breite, um als Schere zu lesen, und haben sie dort nicht. */
  .ticker__row--back { display: none; }
}

/* ---------- footer ---------- */

/* Eigene Fläche statt derselben Schwärze wie die Discography darüber — sonst
   läuft der Kontaktblock als Anhängsel der Releases mit. bg-2 wie bei Press und
   Quotes, dazu der rote Schimmer von oben, der vorher die Newsletter-Bande vom
   Rest abgesetzt hat. */
.footer {
  background:
    radial-gradient(72% 130% at 50% 0%, rgba(197,50,56,.13), transparent 62%),
    linear-gradient(180deg, var(--bg), var(--bg-2) 16%);
  border-top: 1px solid var(--line);
  padding-block: clamp(4.5rem, 10vh, 7.5rem) 2rem;
}
.footer__top {
  display: flex; justify-content: space-between; gap: clamp(2rem,6vw,5rem);
  flex-wrap: wrap;
  padding-bottom: clamp(2.5rem, 6vw, 4rem);
  border-bottom: 1px solid var(--line-soft);
}
.footer__mail {
  display: inline-block;
  font-family: var(--display); font-weight: 800; font-stretch: 108%;
  font-size: clamp(1.4rem, 1rem + 2vw, 2.6rem);
  letter-spacing: -.01em;
  text-decoration: none;
  background-image: linear-gradient(var(--red), var(--red));
  background-size: 0% 1px; background-position: 0 100%; background-repeat: no-repeat;
  transition: background-size .5s var(--ease);
}
.footer__mail:hover { background-size: 100% 1px; }
.footer__note { margin-top: 1rem; max-width: 42ch; color: var(--muted); font-size: .92rem; }

.socials { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .1rem; }
.socials a {
  display: inline-flex; align-items: center; gap: .8rem;
  text-decoration: none;
  padding: .5rem 0;
  font-family: var(--display); font-weight: 600; font-stretch: 106%;
  font-size: .85rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted);
  transition: color .3s var(--ease), transform .4s var(--ease);
}
.socials a:hover { color: var(--text); transform: translateX(5px); }
.socials svg {
  width: 18px; height: 18px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.6;
}
.socials li:first-child svg { fill: currentColor; stroke: none; }
.socials li:nth-child(4) svg, .socials li:nth-child(5) svg { fill: currentColor; stroke: none; }
.socials li:nth-child(3) svg path { fill: currentColor; stroke: none; }

/* Die Schriftgroesse ist auf die viewBox gerechnet, nicht geraten: "JUNKSISTA"
   misst in Archivo 800 / wdth 125 bei 100 px genau 739,3 px, fuer 1000 Einheiten
   also 100 x 1000/739,3 = 135,3. textLength korrigiert den Rest ueber die
   Laufweite (lengthAdjust="spacing" — keine verzerrten Glyphen). */
.footer__mark {
  margin-top: clamp(2.5rem, 7vw, 5rem);
  margin-bottom: clamp(1.2rem, 3vw, 2.2rem);
}
.footer__mark svg { display: block; width: 100%; height: auto; overflow: visible; }
.footer__mark text {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: 135.3px;
  fill: var(--text);
}

.footer__bottom {
  display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
  padding-top: 1.8rem;
  font-size: .74rem; letter-spacing: .1em; text-transform: uppercase;
  color: #4e4e56;
}
.footer__bottom p { margin: 0; }
.footer__bottom a { text-decoration: none; transition: color .3s var(--ease); }
.footer__bottom a:hover { color: var(--text); }

/* Impressum und Datenschutz laufen heller als der Rest der Zeile. #4e4e56 auf
   --bg sind 2,43:1 — fuer den Copyright-Vermerk egal, aber der Impressum-Link
   muss leicht erkennbar sein, und das ist bei 11,8 px die Farbe, nicht die
   Position. --muted liegt bei 7,34:1. */
.footer__legal a { color: var(--muted); }

/* =========================================================
   REVEAL ANIMATIONS
   ========================================================= */

[data-reveal] {
  opacity: 0;
  transform: translate3d(var(--rv-x, 0), var(--rv-y, 28px), 0);
  transition:
    opacity var(--dur) var(--ease),
    transform var(--dur) var(--ease),
    filter var(--dur) var(--ease);
  transition-delay: var(--rv-d, 0s);
}
[data-reveal="up"]    { --rv-y: 30px; }
[data-reveal="left"]  { --rv-x: -36px; --rv-y: 0; }
[data-reveal="right"] { --rv-x: 36px;  --rv-y: 0; }
[data-reveal="zoom"]  { --rv-y: 0; transform: scale(.94); }
[data-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: none;
}

/* The mask curtain sits on the child, never on the observed element itself:
   an element clipped to zero height has an empty intersection rect, the
   observer never fires for it and the reveal never arrives. */
[data-reveal="mask"] { --rv-y: 40px; }

/* Der Vorhang faehrt aus der Mitte nach aussen auf, nicht von oben nach unten.
   Das ist keine Geschmacksfrage: `inset(0 0 100% 0)` ist Zeile fuer Zeile von
   oben — also exakt die Bewegung, die ein progressiv ladendes JPEG macht. Auf
   dem grossen Duo-Foto las sich der Reveal deshalb nicht als Geste, sondern
   als langsam ladendes Bild, und je groesser die Flaeche, desto eindeutiger.
   Waagerecht kann es kein Ladeartefakt sein, weil kein Bild seitlich laedt.

   Aus der Mitte und nicht von einer Seite, aus zwei Gruenden:
   - `.portrait img` ist per `scale(-1.16, 1.16)` gespiegelt, und clip-path
     wirkt im lokalen Koordinatensystem *vor* dem transform. Ein einseitiger
     Aufzug liefe dort sichtbar andersherum als auf Cover und Videobild. Ein
     symmetrischer ist von der Spiegelung unabhaengig.
   - Auf dem Duo-Foto liegt in der Mitte die dunkle Luecke zwischen den beiden.
     Der Vorhang oeffnet also aus dem Leeren heraus, und die zwei Gesichter
     kommen zuletzt und gleichzeitig — was die Zuordnung der Zitatkarten
     darunter stuetzt statt ihr zuvorzukommen.

   0,95 s statt 1,1 s: mit --ease (starkes Ease-out) liegt der Weg ohnehin
   vorne, die Zeile war nur der zweite Grund fuer den Eindruck "laedt noch". */
[data-reveal="mask"] > :first-child {
  clip-path: inset(0 50% 0 50%);
  transition: clip-path .95s var(--ease);
  transition-delay: var(--rv-d, 0s);
}
[data-reveal="mask"].is-in > :first-child { clip-path: inset(0 0 0 0); }

/* staggered children */
[data-reveal="stagger"] { opacity: 1; transform: none; }
[data-reveal="stagger"] > * {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  transition: opacity .75s var(--ease), transform .75s var(--ease);
  transition-delay: calc(var(--i, 0) * 90ms);
}
[data-reveal="stagger"].is-in > * { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  [data-reveal], [data-reveal="stagger"] > *, [data-reveal="mask"] > :first-child,
  .hero__title .ch {
    opacity: 1 !important; transform: none !important; clip-path: none !important;
  }
  .grain { display: none; }
  .hero__video { display: none; }
}

/* =========================================================
   RESPONSIVE
   ========================================================= */

@media (max-width: 860px) {
  .burger { display: block; }
  .nav {
    position: fixed; inset: 0;
    flex-direction: column; justify-content: center;
    gap: 2.5rem;
    background: rgba(8,8,10,.97);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    opacity: 0; visibility: hidden;
    transition: opacity .45s var(--ease), visibility .45s;
  }
  .nav.is-open { opacity: 1; visibility: visible; }
  .brand { position: relative; z-index: 2; }
  .brand__logo { width: 104px; }
  .nav ul { flex-direction: column; gap: 1.4rem; text-align: center; }
  .nav__link { font-size: 1.35rem; letter-spacing: .18em; }
  .nav__link::after { display: none; }
  .burger { z-index: 2; }
  body.nav-open { overflow: hidden; }
}

@media (max-width: 900px) {
  .hero__img { object-position: 50% 34%; filter: grayscale(0) contrast(1.05) brightness(.9); }
  .hero__video { display: none; }
  .hero__scrim {
    background:
      radial-gradient(120% 70% at 50% 0%, rgba(197,50,56,.18), transparent 55%),
      linear-gradient(to top, var(--bg) 2%, rgba(8,8,10,.86) 34%, rgba(8,8,10,.30) 70%, rgba(8,8,10,.55) 100%);
  }
}

@media (max-width: 560px) {
  /* Beide Paare umbrechen hier untereinander. Ohne die volle Breite stehen
     zwei verschieden lange Knoepfe linksbuendig uebereinander, und die Kante
     rechts franst aus. */
  .hero__actions .btn,
  .album__actions .btn { width: 100%; justify-content: center; }
  .footer__bottom { flex-direction: column; gap: .5rem; }
}
