/* ==========================================================================
   Some1Elsewhere productions (S1E)
   Placeholder site. Vanilla CSS, no build step, no runtime third-party request.

   Design system
   -------------
   Direction  : clean editorial, warm monochrome, light.
   Accent     : none. Ink itself is the accent. The brand device is a printed
                "slug" (filled ink rectangle) around the 1 in Some1Elsewhere.
   Radius lock: 0 everywhere. Sharp, print-like. No exceptions.
   Type       : TASA Orbiter (variable wght) throughout, JetBrains Mono
                for catalogue metadata. Both self-hosted in /fonts.

   Motion thesis
   -------------
   The world is a printing press; the page is a sheet going through it.
     Focal moment : the hero. The portrait is the whole stage; the wordmark
                    sets over it, the slug prints itself, and on scroll the ink
                    plate drifts out of register with the paper "1".
     Continuity   : ink wipes left to right, the direction the press travels.
     Depth        : scroll-driven parallax in register layers.
     Feedback     : hover is ink arriving, from the side you came from.
   Parallax and scrub are CSS scroll-driven animations, so they run off the
   main thread. Where unsupported they simply do not run and the page is
   still complete. Nothing here uses a scroll event listener.
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Fonts (self-hosted, same-origin only)
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "TASA Orbiter";
  font-style: normal;
  /* wght is the only axis in the v2 file - no width, no optical size. The
     "opsz 60"/"ss03"/"cv05" that float around in copies of this recipe are
     dead declarations against this binary. */
  font-weight: 400 800;
  font-display: swap;
  src: url("fonts/tasa-orbiter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "TASA Orbiter";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("fonts/tasa-orbiter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("fonts/jetbrains-mono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("fonts/jetbrains-mono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

/* Registered so it interpolates: a plain custom property would snap between
   colourways instead of shifting. */
@property --accent {
  syntax: "<color>";
  inherits: true;
  initial-value: #8a8578;
}

:root {
  /* Idle colourway: a warm grey, i.e. no colour at all. A release sets this
     to its own accent while it plays and it reverts on stop.
     The value itself is switched instantly and the *consumers* below are what
     ease: transitioning a registered custom property on the root turned out to
     stall indefinitely on one of the two pages, and a colourway that only
     sometimes animates is worse than one that reliably does. */
  /* Idle colourway is the label pink now, not an absence of colour: on black
     the accent is the one live thing on the page, so it is never switched off,
     only re-tuned toward whichever record is playing. */
  /* THE LABEL ACCENT. Pink is retired. The chrome now takes its colour from
     the page's own ground - the orange-to-red the whole site stands on - so
     the buttons, the press rule, the active nav item and the curtain all read
     as the same material as the stage rather than as a second brand colour
     imported from nowhere.

     #f2680f is not a new colour: it is the gradient's own midpoint stop, which
     is why it sits correctly against both ends of the ground. Measured 6.34:1
     as ink on the sheet and as a ground under --paper labels.

     This is the LABEL accent only. A release overrides --accent from its own
     colourway while it plays and on its own page, and none of that is touched
     here - the pink was never in that path. */
  --accent: #f2680f;
  /* Where a surface is big enough to carry the gradient rather than a flat
     sample of it: the curtain, and anything else that wants the full sweep. */
  --accent-sheet: linear-gradient(
    160deg,
    var(--ground-top) 0%,
    #f2680f 48%,
    var(--ground-bottom) 100%
  );

  /* KINO ground. The names keep their roles - paper is the stock you print on,
     ink is the mark you put on it - but the stock is black cinema stock now
     and the mark is light. Every rule downstream reads these, so the whole
     system inverts here rather than in two hundred places. */
  --paper: #0a0a0b;
  --paper-lift: #121214;
  --ink: #edebe6;
  --ink-2: #c9c6c0;
  --muted: #8a8781;
  --hair: rgba(237, 235, 230, 0.14);
  --hair-soft: rgba(237, 235, 230, 0.07);
  /* Raw channels, so scrims and glass can be mixed from the same two colours. */
  --ground-rgb: 10, 10, 11;
  --fore-rgb: 237, 235, 230;

  /* Type */
  /* One typeface for the whole site now. The fallback behind it is the system
     grotesk rather than Archivo: with Orbiter set everywhere, a second webfont
     that only ever paints during a failed load is 176KB of nothing. */
  --sans: "TASA Orbiter", ui-sans-serif, system-ui, -apple-system,
    "Segoe UI Variable Text", "Segoe UI", Roboto, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    monospace;
  /* Display and text are one family now. The alias is kept because two
     hundred declarations read --display and the distinction it names - which
     grade of the scale a thing takes - is still real. */
  --display: var(--sans);

  /* THE TYPE SCALE
     Orbiter is cut tight, and the motion.dev recipe (wght 690, -0.04em, lh 1)
     is a DISPLAY recipe - correct at 60px and illegible mush at 16px, where
     -0.04em closes the counters. So three grades, and text is tracked at zero.

     Weights sit a little above where they would on paper: light type on a
     black ground optically thins, so 430 here reads like 400 does on white. */
  --wght-display: 690;
  --wght-mid: 600;
  --wght-strong: 545;
  --wght-text: 430;
  --track-display: -0.04em;
  --track-mid: -0.022em;
  --track-strong: -0.008em;
  --track-text: 0em;

  /* Rhythm */
  --pad: clamp(1.25rem, calc(4.2 * var(--uw)), 3.75rem);
  --maxw: 1180px;
  --sec: clamp(4.5rem, calc(9 * var(--uw)), 8.5rem);
  --header-h: 61px;

  /* REDUX ground. The page is a gradient; the site sits on a dark sheet over
     it. The sheet IS the wrap made visible, so it uses the same --maxw the
     content column has always used - that is why the gutters land where they
     do rather than at some new number. */
  --ground-top: #ff9700;
  --ground-bottom: #aa1d10;
  /* THE GUTTER IS THE CONSTANT NOW, NOT THE SHEET.

     The sheet used to cap at --maxw, so past about 1350px the page stopped
     growing and the gradient gutters took everything else - 500px a side on a
     2000px screen, with the head-and-band composition marooned in the middle of
     it. Inverted: the gutter is clamped and the sheet is whatever is left.

     The ramp is written as an explicit interpolation rather than a plain vw so
     both ends are exact rather than approximately right:
       390px  ->  8px   the phone's current gutter, unchanged, to the pixel
       1440px -> 92px   the owner's ceiling, reached here
       wider  -> 92px   held, and the sheet takes every pixel after that
     0.08 is (92 - 8) / (1440 - 390); nothing is rounded into it.

     % rather than vw on purpose: vw includes the scrollbar, and this value
     decides the width of elements that must not overflow because of it. */
  /* THE 885 FRAME. The whole composition - gradient gutters included - now
     caps at 885px and floats on plain black past that: the poster column the
     owner asked for. --site-w is that cap; the black outside it is painted by
     the html::after curtains (see section 25b), so nothing about the band or
     the fixed-background registration had to move. The gutter ramp keeps the
     phone's exact 8px and now tops out at 14px (reached by ~465px wide)
     instead of 92. */
  --site-w: min(100%, 885px);
  /* The frozen viewport unit: 1vw until the frame caps, 8.85px (885/100)
     after. Every horizontal measure that used to be Nvw is now
     calc(N * var(--uw)), so past 885px the whole proportional system holds
     the exact state it had AT 885 instead of growing out of a fixed column.
     (Not derived from --site-w: that carries a %, and a % inside font-size
     resolves against the parent's font, not the width.) */
  --uw: min(1vw, 8.85px);
  --gutter-w: clamp(8px, calc(8px + (100% - 390px) * 0.08), 14px);
  --sheet-w: calc(var(--site-w) - 2 * var(--gutter-w));
  /* How far down the page the sheet starts, and how tall the hero band is.
     The band runs from under the header to --band-h; the sheet's top edge cuts
     across it at --sheet-top, so the band's lower part reads behind the sheet. */
  /* NEW PROPORTION. 100px was not enough - the complaint was never the sheet's
     offset, it was that you see too little of the photograph. So the band grows
     from 39vh to ~58vh of visible artwork and the sheet falls to ~46vh, which
     puts the type back down where it sat before REDUX and gives the picture
     room to be a picture.

     svh, not vh: on a phone vh is the TALL viewport, so a 58vh band is taller
     than the screen actually is while the URL bar is up. svh is the small
     viewport and the proportion holds through the bar's show/hide.

     The min() caps are the fold guard. They only bite on tall screens, where a
     percentage would run away from the wordmark; on short ones the svh term
     wins, so the mark stays at ~46% of the viewport at every height and is
     never below the fold. */
  --hero-band-h: min(58svh, 620px);
  /* Pulled up from 46svh. The hero used to open with the wordmark; that has
     gone to the masthead, so the sheet line no longer has to leave room for it
     and the tile row rises by most of a wordmark's height. The reserved
     playing-type block still sits between the two. */
  --sheet-top: min(33svh, 400px);

  /* Motion */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  /* The signature curve: leaves instantly, lands without a bounce. Everything
     that sweeps, fills or takes over the frame uses this one. */
  --ease-signature: cubic-bezier(0.25, 0, 0, 1);

  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   2. Reset + base
   -------------------------------------------------------------------------- */

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

/* The gradient lives on body, and html carries the ink so body's background
   does not propagate to the canvas and get painted viewport-sized instead of
   document-sized. */
html {
  /* Plain black past the 885 frame; inside it the sheet and gradients paint
     their own grounds, so this only ever shows beside and beyond them. */
  background: #000;
  /* The band bleeds to 100vw, which is wider than the scrollable area once a
     scrollbar is present. body already clips, but html is the scroller, so the
     clip has to be declared here too or the page gains 5px of sideways travel. */
  overflow-x: hidden;
  overflow-x: clip;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Browser surfaces belong to the design too. */
  caret-color: var(--ink);
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--fore-rgb), 0.24) transparent;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(var(--fore-rgb), 0.2);
  border: 3px solid var(--paper);
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(var(--fore-rgb), 0.34);
}

body {
  margin: 0;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  /* WebKit (iOS Safari) will still scroll the viewport sideways with
     `overflow-x: hidden` here when a nested scroll container - the reels
     track - lays out wider than the page. `clip` is the one that actually
     holds in Safari, and it cannot be scrolled programmatically either.
     `hidden` stays first as the fallback for engines without `clip`. */
  overflow-x: hidden;
  overflow-x: clip;
  /* Orange holds nearly flat across the top two fifths and then falls to red,
     which is what keeps the header and band sitting in a field of colour
     rather than in the middle of a ramp. */
  /* The gradient is NOT painted here any more - see section 25. body's own
     background would sit ABOVE any z-index:-1 pseudo-element of body (a
     negative-z child paints in the ROOT stacking context, since body is
     static and makes no stacking context of its own), which buried the
     release tint completely. Both grounds are pseudo-elements now, so they
     layer against each other instead of against an unreachable backdrop. */
  background: transparent;
  color: var(--ink);
  font-family: var(--sans);
  font-size: clamp(1rem, 0.96rem + calc(0.2 * var(--uw)), 1.0625rem);
  font-weight: var(--wght-text);
  letter-spacing: var(--track-text);
  line-height: 1.62;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

img,
svg {
  display: block;
  max-width: 100%;
}

h1,
h2,
h3,
p,
ul,
ol,
figure {
  margin: 0;
}

ul {
  padding: 0;
  list-style: none;
}

a {
  color: inherit;
  text-decoration: none;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}

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

::selection {
  background: var(--accent);
  color: var(--paper);
}

/* The gutter never falls inside the notch or the rounded corners in landscape:
   whichever is larger, the designed gutter or the device's own safe inset. */
.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--pad);
  padding-inline: max(var(--pad), env(safe-area-inset-left, 0px))
    max(var(--pad), env(safe-area-inset-right, 0px));
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip {
  position: absolute;
  left: var(--pad);
  top: 0;
  z-index: 40;
  padding: 0.7rem 1rem;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transform: translateY(-120%);
}

.skip:focus-visible {
  transform: translateY(0);
  outline-offset: -4px;
}

/* --------------------------------------------------------------------------
   3. Ambient ground (fixed, pointer-events: none, never on scrolling content)
   -------------------------------------------------------------------------- */

.ambient {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background:
    radial-gradient(calc(54 * var(--uw)) calc(48 * var(--uw)) at 20% 8%, rgba(var(--fore-rgb), 0.045), transparent 72%),
    radial-gradient(calc(58 * var(--uw)) calc(52 * var(--uw)) at 88% 82%, rgba(217, 106, 140, 0.035), transparent 74%);
  animation: drift 34s var(--ease-in-out) infinite;
  will-change: transform;
}

@keyframes drift {
  0%,
  100% {
    transform: translate3d(0, 0, 0) scale(1);
  }
  50% {
    transform: translate3d(-2.5%, 2%, 0) scale(1.08);
  }
}

/* The reels run. A slow ken-burns on whatever is in the frame, cover or empty
   sleeve, so the motion is identical once a real cover lands. --reel-phase
   holds the three permanently out of phase with each other; three tiles
   breathing in unison would read as a machine rather than as three reels. */
@media (prefers-reduced-motion: no-preference) {
  .reel__cover {
    animation: reel-drift 28s var(--ease-in-out) infinite;
    animation-delay: var(--reel-phase, 0s);
    will-change: transform;
  }

  .reel__bed {
    animation: reel-bed-drift 34s var(--ease-in-out) infinite;
    animation-delay: var(--reel-phase, 0s);
  }
}

/* Scale only, and barely. A translating ken-burns on a square sleeve pushes
   the artwork off centre and shaves the lettering; breathing keeps it
   centred, so at most 2% is ever outside the frame. */
@keyframes reel-drift {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.045);
  }
}

/* The bed drifts the other way, so the tile has depth without the sleeve
   itself having to move much. */
@keyframes reel-bed-drift {
  0%,
  100% {
    transform: scale(1.08) translate3d(-1.2%, -1%, 0);
  }
  50% {
    transform: scale(1.16) translate3d(1.4%, 1.3%, 0);
  }
}

/* The stage breathes. 26s keeps it well clear of the ~0.2Hz band that reads
   as a pulse rather than as stillness. */
@media (prefers-reduced-motion: no-preference) {
  .hero__bg-breathe {
    animation: stage-breathe 26s var(--ease-in-out) infinite;
    will-change: transform;
  }
}

@keyframes stage-breathe {
  0%,
  100% {
    transform: scale(1) translate3d(0, 0, 0);
  }
  50% {
    transform: scale(1.05) translate3d(0, -0.8%, 0);
  }
}

/* --------------------------------------------------------------------------
   4. The slug - the brand device
   Ink rectangle with the character reversed out of it.
   .slug__reg is the ink plate: it is the layer that can go out of register.
   -------------------------------------------------------------------------- */

.slug {
  position: relative;
  display: inline-block;
  line-height: 0.92;
  padding-inline: 0.155em;
  margin-inline: 0.055em;
  color: var(--paper);
  z-index: 0;
}

.slug__reg {
  position: absolute;
  inset: 0;
  z-index: -1;
}

/* The fill has to sit exactly on the digit's ink, and the numbers are the
   font's, not a guess: measured against TASA Orbiter at the wordmark's own
   size and weight, the "1" runs from 0.119em below the slug's box top to
   0.121em above its bottom. Archivo wanted 0.071em / -0.011em - switching the
   display face without re-pinning these is what leaves the box floating off
   the cap line. Both are em, so every instance scales with it. */
.slug__fill {
  position: absolute;
  inset: 0.119em 0 0.121em 0;
  background: var(--ink);
  transform-origin: left center;
}

.slug__char {
  position: relative;
}

/* --------------------------------------------------------------------------
   5. Header + press rule
   -------------------------------------------------------------------------- */

/* The header is the one piece of chrome that content passes under, so it is
   the other place the frosted material belongs. Solid by default, frosted
   only where backdrop-filter exists. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--paper);
  border-bottom: 1px solid var(--hair-soft);
  /* Positioning context for the band, which is absolutely placed against the
     bottom edge. */
  isolation: isolate;
}

:root {
  /* Sized against the nav's own box, not by eye: at a 68px header the links
     occupy y 18-50, so 18px puts the band's top edge exactly on their bottom
     and nothing the field draws can ever land behind a link. */
  --band-h: 18px;
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header {
    background: rgba(var(--ground-rgb), 0.66);
    -webkit-backdrop-filter: blur(18px) saturate(1.5);
    backdrop-filter: blur(18px) saturate(1.6);
    border-bottom-color: rgba(var(--fore-rgb), 0.06);
  }

  /* A soft edge where content slides under the chrome, instead of a hard rule
     doing all the separating on its own. */
  .site-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 16px;
    background: linear-gradient(rgba(var(--ground-rgb), 0.55), rgba(var(--ground-rgb), 0));
    pointer-events: none;
  }
}

/* THE HEADER BAND
   The field carries on in the chrome once the hero has gone. It is reserved to
   a strip along the header's bottom edge rather than run behind the whole bar:
   the nav and the wordmark keep their own clean ground, so no measurement
   depends on what the field happens to be drawing this frame. */
.header__field {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--band-h, 22px);
  width: 100%;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  transition: opacity 620ms var(--ease-signature);
  /* Soft at both ends so the strip reads as a band of the field rather than a
     cropped rectangle of it. */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 6%, #000 94%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, #000 6%, #000 94%, transparent 100%);
}

.header__field.is-awake.is-live {
  opacity: 1;
}

.site-header__in {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: var(--header-h);
}

/* Travel of the sheet through the press: reading position, drawn as ink
   filling the header's own hairline rather than as a bar of its own. */
.press-rule {
  position: fixed;
  top: var(--header-h);
  /* Was left:0/right:0 - the full viewport, so the strip ran out past both
     ends of the chrome and across the gradient gutters. The header is
     sheet-width; its rule has to be too. left/right/width/auto margins is the
     centring that works on a fixed element. */
  left: 0;
  right: 0;
  width: var(--sheet-w);
  margin-inline: auto;
  height: 1px;
  z-index: 21;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  pointer-events: none;
}

.mark {
  font-family: var(--display);
  font-weight: 690;
  font-size: 1.15rem;
  letter-spacing: -0.04em;
  font-variation-settings: "wght" 690;
  line-height: 1;
  white-space: nowrap;
}

.mark .slug {
  padding-inline: 0.18em;
  margin-inline: 0.05em;
}

/* The header carries the full name now, so it has to survive next to four nav
   items at 390px. It is set narrower and tighter than the footer's short mark
   rather than smaller: condensing keeps it the same optical weight while
   giving back the ~14% of width the nav needs. */
/* Orbiter has no width axis, so the 14% the old "wdth" 86 used to claw back
   at 390px has to come from somewhere else. It comes from the size step and
   the tracking: a slightly lower floor plus Orbiter's own -0.04em, which is
   tighter than the -0.028em Archivo was set at here. Measured at 390 below. */
/* The owner asked for the nav in the new face by name. Mono keeps the genuinely
   telemetric voice - dates, timecodes, meta labels, section markers - but the
   navigation is chrome, not telemetry, and it reads better set in the same
   typeface as everything it leads to. Uppercase wants positive tracking, and a
   proportional face needs a touch more of it than a fixed-advance one did. */
/* The links are blocks now, not underlined labels: each one runs the full
   height of the header row and carries the ghost-button mechanics from §12
   (grain ground, shearing sweep) minus the icon and minus the corner
   brackets, which read as clutter at this size and were removed by request.
   Same skeleton, and the whole box is the hit area. */
.nav {
  align-self: stretch;
  display: flex;
  align-items: stretch;
  gap: var(--nav-gap, clamp(0.35rem, calc(1 * var(--uw)), 0.6rem));
  font-size: 0.74rem;
  font-weight: 520;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.nav a {
  --nav-label: var(--muted);
  --nav-sweep: var(--accent);
  --nav-label-on: var(--paper);
  --nav-grain: rgba(var(--fore-rgb), 0.05);

  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  padding-inline: var(--nav-pad, 1.05rem);
  color: var(--nav-label);
  /* The ghost button's raked hairline grain, one notch quieter: four of these
     sit side by side on the frosted bar, so the texture has to whisper. */
  background-image: repeating-linear-gradient(
    119deg,
    var(--nav-grain) 0 1px,
    transparent 1px 6px
  );
  white-space: nowrap;
  transition: color 0.2s ease 0.15s;
}

/* The sweep. Same zero-width sliver hinged off the left edge as .btn::after. */
.nav a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--nav-sweep);
  clip-path: polygon(-1% 0%, -1% 0%, -100% 100%, -1% 100%);
  transition: clip-path 0.4s var(--ease-signature);
}

/* "page" on the page itself, "true" on a release page marking the section it
   belongs to - a release detail page is not the catalogue, but it is under it.
   The filled block is the active tab: what the underline used to say. */
.nav a[aria-current="page"],
.nav a[aria-current="true"] {
  color: var(--nav-label-on);
}

.nav a[aria-current="page"]::after,
.nav a[aria-current="true"]::after {
  clip-path: polygon(0% 0%, 200% 0%, 100% 100%, 0% 100%);
}

/* Keyboard gets the same sweep as the pointer - state, not decoration. */
.nav a:focus-visible {
  color: var(--nav-label-on);
}

.nav a:focus-visible::after {
  clip-path: polygon(0% 0%, 200% 0%, 100% 100%, 0% 100%);
}

@media (hover: hover) and (pointer: fine) {
  .nav a:hover {
    color: var(--nav-label-on);
  }
  .nav a:hover::after {
    clip-path: polygon(0% 0%, 200% 0%, 100% 100%, 0% 100%);
  }
}

/* --------------------------------------------------------------------------
   6. Layout shells
   -------------------------------------------------------------------------- */

.main {
  position: relative;
  z-index: 2;
  flex: 1 0 auto;
}

.section {
  padding-block: var(--sec);
  border-top: 1px solid var(--hair);
}

.section--after-hero {
  padding-top: clamp(3.25rem, calc(6 * var(--uw)), 5rem);
}

.page-head {
  padding-block: clamp(3.5rem, calc(8 * var(--uw)), 7rem) clamp(2.5rem, calc(5 * var(--uw)), 4rem);
}

.page-head__lead {
  margin-top: clamp(1.15rem, calc(2.4 * var(--uw)), 1.75rem);
}

.prose {
  max-width: 68ch;
}

.prose__lead {
  max-width: 30ch;
  margin-bottom: clamp(1.75rem, calc(3.5 * var(--uw)), 2.5rem);
  padding-bottom: clamp(1.75rem, calc(3.5 * var(--uw)), 2.5rem);
  border-bottom: 1px solid var(--hair);
}

@media (min-width: 1000px) {
  .prose {
    padding-left: 12%;
  }
}

/* --------------------------------------------------------------------------
   7. Typography scale
   -------------------------------------------------------------------------- */

.wordmark {
  font-family: var(--display);
  /* The reference recipe: wght 690, -0.04em, line-height 1. */
  font-weight: 690;
  font-variation-settings: "wght" 690;
  /* Low floor on purpose: the mark must never clip inside .hero__mask.
     The ceiling came down when the catalogue took over the deck: the hero is
     one bottom-anchored stack, so every pixel the tiles gained pushed the
     wordmark up the frame, and at 8.8rem it ended up at 11.8% of the stage -
     in the brightest part of the photograph, where it measured 2.34:1. The
     tiles are the main event on this page now and the mark can say so. */
  font-size: clamp(2rem, calc(7.4 * var(--uw)), 5.9rem);
  line-height: 1;
  letter-spacing: -0.04em;
  white-space: nowrap;
}

.h-display {
  font-family: var(--display);
  font-weight: 690;
  font-variation-settings: "wght" 690;
  font-size: clamp(2.1rem, calc(6.4 * var(--uw)), 4.5rem);
  line-height: 1;
  letter-spacing: -0.04em;
  text-wrap: balance;
}

.h-mid {
  font-family: var(--display);
  font-weight: var(--wght-mid);
  font-variation-settings: "wght" 600;
  font-size: clamp(1.45rem, calc(3.2 * var(--uw)), 2.1rem);
  line-height: 1.12;
  letter-spacing: var(--track-mid);
  text-wrap: balance;
}

.lead {
  font-size: clamp(1.2rem, calc(2.3 * var(--uw)), 1.6rem);
  line-height: 1.42;
  letter-spacing: -0.012em;
  max-width: 30ch;
}

.body {
  max-width: 60ch;
  color: var(--ink-2);
}

.body + .body {
  margin-top: 1.15rem;
}

.meta {
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   8. Buttons and links
   -------------------------------------------------------------------------- */

/* THE SWEEP
   One mechanic, two grounds. A fill sheared off the left edge unshears
   across the face on the signature curve, and the label's colour flips on a
   *delay* so the ink changes behind the advancing edge rather than with it.
   The delay is the whole trick: take it out and the button reads as a plain
   colour change. Corner brackets push outward on approach and snap inward on
   press, so the button has a resting frame, a reaching frame and a held one.

     .btn         primary   - pink chip, black label; sweeps to black/pink
     .btn--ghost  secondary - textured ground, ink label; sweeps to the colourway

   Both sit on the same skeleton, so there is one set of mechanics to reason
   about and the variants differ only in which four colours they name. */

.btn {
  --btn-ground: var(--accent);
  --btn-label: var(--paper);
  --btn-sweep: var(--paper);
  --btn-label-on: var(--accent);
  --btn-grain: rgba(var(--ground-rgb), 0.085);
  --btn-bracket: rgba(var(--ground-rgb), 0.45);

  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.95rem 1.05rem 0.95rem 1.4rem;
  background-color: var(--btn-ground);
  /* The ground is never a flat chip: a raked hairline grain, the same angle
     everywhere, so the fill reads as a printed surface. */
  /* A rake, not a stripe. At the alpha the fill wants to be legible it stops
     being texture and starts looking like a rendering fault, which is exactly
     how it read on a phone at 1.5x before these values came down. */
  background-image: repeating-linear-gradient(
    119deg,
    var(--btn-grain) 0 1px,
    transparent 1px 6px
  );
  color: var(--btn-label);
  font-size: 0.95rem;
  font-weight: var(--wght-strong);
  letter-spacing: var(--track-strong);
  line-height: 1;
  white-space: nowrap;
  /* --mx/--my are written by the magnetic pointer handler in script.js */
  transform: translate3d(var(--mx, 0px), var(--my, 0px), 0);
  transition: color 0.2s ease 0.15s, background-color 0.2s ease,
    transform 0.45s var(--ease-out);
}

/* The fill. Clipped to a zero-width sliver hinged off the left edge; the
   polygon's right-hand points sit past 100% so the top edge is covered for
   the whole travel and only the bottom-left corner shears. */
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--btn-sweep);
  clip-path: polygon(-1% 0%, -1% 0%, -100% 100%, -1% 100%);
  transition: clip-path 0.4s var(--ease-signature);
}

/* Four corner brackets, eight hairlines. */
.btn::before {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(var(--btn-bracket), var(--btn-bracket)) 0 0 / 8px 1px no-repeat,
    linear-gradient(var(--btn-bracket), var(--btn-bracket)) 0 0 / 1px 8px no-repeat,
    linear-gradient(var(--btn-bracket), var(--btn-bracket)) 100% 0 / 8px 1px no-repeat,
    linear-gradient(var(--btn-bracket), var(--btn-bracket)) 100% 0 / 1px 8px no-repeat,
    linear-gradient(var(--btn-bracket), var(--btn-bracket)) 0 100% / 8px 1px no-repeat,
    linear-gradient(var(--btn-bracket), var(--btn-bracket)) 0 100% / 1px 8px no-repeat,
    linear-gradient(var(--btn-bracket), var(--btn-bracket)) 100% 100% / 8px 1px no-repeat,
    linear-gradient(var(--btn-bracket), var(--btn-bracket)) 100% 100% / 1px 8px no-repeat;
  /* `inset` is layout, which this file otherwise avoids - but the pseudo has
     no children and nothing downstream depends on its box, so the pass is
     free, and it is the only way to push all four sides by the same 2px on a
     box whose aspect ratio is unknown. Safari agrees with Chrome here. */
  transition: inset 0.28s var(--ease-signature);
}

.btn__label {
  position: relative;
  z-index: 1;
}

.btn__icon {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  flex: 0 0 26px;
  background: currentColor;
  color: var(--btn-label);
  transition: transform 0.24s var(--ease-out), color 0.2s ease 0.15s;
}

.btn__icon svg {
  color: var(--btn-ground);
  transition: color 0.2s ease 0.15s;
}

.btn:active::before {
  inset: 3px;
  transition-duration: 0.12s;
}

.btn:active {
  transform: translate3d(var(--mx, 0px), var(--my, 0px), 0) scale(0.985);
  transition-duration: 0.12s, 0.12s, 0.12s;
}

/* Keyboard gets the same sweep as the pointer - it is state, not decoration. */
.btn:focus-visible::after {
  clip-path: polygon(0% 0%, 200% 0%, 100% 100%, 0% 100%);
}

.btn:focus-visible {
  color: var(--btn-label-on);
}

.btn:focus-visible .btn__icon {
  color: var(--btn-label-on);
}

.btn:focus-visible .btn__icon svg {
  color: var(--btn-sweep);
}

@media (hover: hover) and (pointer: fine) {
  /* While the pointer is inside, the button tracks it: shorter transition so
     it feels attached rather than trailing. */
  .btn.is-magnetic {
    transition-duration: 0.2s, 0.2s, 0.12s;
  }
  .btn:hover::after {
    clip-path: polygon(0% 0%, 200% 0%, 100% 100%, 0% 100%);
  }
  .btn:hover::before {
    inset: -3px;
  }
  .btn:hover {
    color: var(--btn-label-on);
  }
  .btn:hover .btn__icon {
    color: var(--btn-label-on);
    transform: translate3d(2px, -2px, 0);
  }
  .btn:hover .btn__icon svg {
    color: var(--btn-sweep);
  }
}

/* Secondary. Nothing but grain and brackets at rest; the colourway is what
   arrives on approach. */
.btn--ghost {
  --btn-ground: transparent;
  --btn-label: var(--ink);
  --btn-sweep: var(--accent);
  --btn-label-on: var(--paper);
  --btn-grain: rgba(var(--fore-rgb), 0.06);
  --btn-bracket: rgba(var(--fore-rgb), 0.38);
  padding: 0.95rem 1.2rem;
  gap: 0.6rem;
}

.btn--ghost .btn__icon {
  width: auto;
  height: auto;
  flex: 0 0 auto;
  background: none;
  color: inherit;
  transition: transform 0.3s var(--ease-out), color 0.2s ease 0.15s;
}

.btn--ghost .btn__icon svg {
  color: inherit;
}

.btn--ghost:focus-visible .btn__icon,
.btn--ghost:focus-visible .btn__icon svg {
  color: var(--btn-label-on);
}

@media (hover: hover) and (pointer: fine) {
  .btn--ghost:hover .btn__icon,
  .btn--ghost:hover .btn__icon svg {
    color: var(--btn-label-on);
  }
  .btn--ghost:hover .btn__icon {
    transform: translate3d(3px, -3px, 0);
  }
}

.link {
  position: relative;
  display: inline-block;
  font-weight: var(--wght-strong);
  letter-spacing: var(--track-strong);
  padding-bottom: 2px;
}

.link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform-origin: left center;
  transition: transform 0.28s var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .link::after {
    transform: scaleX(0.999);
  }
  .link:hover::after {
    transform: scaleX(0);
    transform-origin: right center;
  }
}

/* --------------------------------------------------------------------------
   9. Home - hero
   -------------------------------------------------------------------------- */

/* Full bleed: the stage breaks out of the page's centred column to the
   viewport edges, while the content inside stays on the grid. */
.hero {
  position: relative;
  min-height: calc(100dvh - var(--header-h));
  display: grid;
  align-content: end;
  /* The deck fills the lower stage now, so the hero no longer needs a deep
     top padding to push the wordmark down. */
  padding-block: clamp(2.5rem, calc(6 * var(--uw)), 4rem) clamp(2.5rem, calc(4.5 * var(--uw)), 3.5rem);
  overflow: hidden;
  /* The glass panels must sample the background image. Both live inside
     .hero, so promoting .hero puts them in one backdrop root; Chrome will not
     let a backdrop-filter sample a separately composited sibling otherwise,
     and the parallaxed image is always composited. */
  transform: translateZ(0);
}

.hero__wrap {
  position: relative;
  z-index: 1;

}

.hero__mask {
  overflow: hidden;
  padding-block: 0.14em 0.1em;
  margin-block: -0.14em -0.1em;
}


/* DESIGN CANVAS. script.js sets .tiles-off on <html> for ?tiles=off: the
   deck keeps its box (the sheet edge, the tear and every measure stay put)
   but paints nothing, so the hero reads as a clean canvas to redesign the
   tiles over. visibility, not display - the geometry is the point. */
html.tiles-off .hero__deck {
  visibility: hidden;
}

/* Panel and catalogue side by side in the lower stage. The reels are sized
   from the column rather than from the page, which is what lets three 9:16
   tiles live above the fold without shrinking the wordmark. */
.hero__deck {
  display: grid;
  gap: clamp(1.1rem, calc(2.4 * var(--uw)), 2.25rem);
  margin-top: clamp(1rem, calc(2.2 * var(--uw)), 1.75rem);
}

/* The catalogue is what the visitor came for, so it takes roughly two thirds
   of the deck and the call to action stands down to a column beside it. */
/* One column since REDUX: the catalogue runs the full width of the sheet and
   the call to action is a band underneath it, not a panel beside it. */
.hero__deck {
  grid-template-columns: minmax(0, 1fr);
}

/* --------------------------------------------------------------------------
   9b. The ASCII field
   Painted by script.js into a transparent canvas laid over the photograph and
   under every scrim. The mask is the contrast contract: the field is allowed
   to be at full strength across the top of the stage, and is held off the band
   where the wordmark and the release title set. Without it the glyphs are
   white ink landing exactly where light type needs a dark ground.
   -------------------------------------------------------------------------- */

.hero__field {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 1;
  width: 100%;
  /* Only as tall as the band the mask actually shows. Everything below this
     is type, and the field is not allowed there - so there is no reason to
     resolve, strike and composite cells for it. Roughly two thirds of the
     per-frame work disappears with this one line. */
  height: 45%;
  pointer-events: none;
  /* hard-light, not screen, and the difference is the whole of the owner's
     "make it react with the image".

     screen can only ever lighten, so the cloud washed over the photograph
     without ever engaging with it. hard-light lets the BLEND layer decide per
     channel: where the cloud's channel is above half it screens, where it is
     below half it multiplies. A saturated cloud therefore drives the
     photograph's channels apart instead of flooding them - the picture's own
     structure stays legible straight through the colour.

     The contrast argument survives intact, because the one case that matters
     is unchanged: hard-light with a pure white blend pixel is screen(base, 1),
     which is white. The glyphs composite to exactly white over any ground
     exactly as they did under screen. */
  mix-blend-mode: hard-light;
  /* Fades in once the first frame is on the canvas, so the hero never shows a
     half-resolved field on load. */
  opacity: 0;
  transition: opacity 900ms var(--ease-signature);
  /* Stops are percentages of the canvas, which is 34% of the hero - so 41%
     here is 14% of the stage and 85% is 29%. The ramp is set against the
     highest type on the stage - the release title's cap line at 15.5% - and
     44% of the canvas is 15% of the stage, so the field is out before any
     type begins. Holding it entirely clear of the type is what lets the
     glyphs stay SOLID white: inside the band the veil is barely 0.04, so
     nothing dims them, and below it they simply are not drawn.
     This got tighter when the glyphs went solid white and gained an additive
     colour cloud - the old 85% stop overlapped the wordmark's cap line by
     39px, which was affordable for a dim wash and is not for a graphic
     element that only ever adds light. Raise any of these and the wordmark
     loses its ground. */
  /* Binary like the glyphs themselves (owner decree): full strength through
     the band, then one short clean edge. Ends at 40% - earlier than the old
     44% - so type clearance only grew. */
  /* Idle: the field runs to 42% of a canvas that is 45% of the stage - 18.9%
     of the stage - and the wordmark's cap line is at 20.6%. */
  --field-ramp: rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 34%,
    rgba(0, 0, 0, 0) 42%;
  -webkit-mask-image: linear-gradient(to bottom, var(--field-ramp));
  mask-image: linear-gradient(to bottom, var(--field-ramp));
}

.hero__field.is-live {
  opacity: 1;
}

/* Playing, the release title takes the stage and its cap line sits at 15.5% -
   five points higher than the wordmark's. The band cannot keep the height it
   has when idle without landing behind the title, so it gives those points
   back for as long as a record is on. This is the cap the owner's "a third
   taller" runs into: idle it grew from 13.6% to 18.9% of the stage, but
   during playback the type is simply there and the field yields. */
.is-playing-audio .hero__field {
  --field-ramp: rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 24%,
    rgba(0, 0, 0, 0) 31%;
}

/* --------------------------------------------------------------------------
   9c. The hero's two type states
   Idle the stage carries the label's name. While a preview plays it carries
   the record's, with a one-line description beside it. Both states share one
   grid cell, so the swap is a crossfade with no reflow underneath it, and the
   cell is always as tall as the taller of the two.
   -------------------------------------------------------------------------- */

.hero__type {
  display: grid;
}

.hero__type > * {
  grid-area: 1 / 1;
  align-self: end;
}

/* Out is quicker than in: the old name should be gone before the new one
   arrives, or the two read as one smeared word. */
.hero__mask {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  transition: opacity 260ms var(--ease-signature),
    transform 380ms var(--ease-signature);
}

.is-playing-audio .hero__mask {
  opacity: 0;
  transform: translate3d(0, -0.14em, 0);
  pointer-events: none;
}

.hero__now {
  display: grid;
  gap: 0.6rem;
  opacity: 0;
  pointer-events: none;
  transform: translate3d(0, 0.16em, 0);
  transition: opacity 420ms var(--ease-signature) 180ms,
    transform 520ms var(--ease-signature) 180ms;
}

.is-playing-audio .hero__now {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* The name is set to the wordmark's optical weight but wraps, because a
   release title is not a fixed string the way the label's name is. */
.hero__now-title {
  font-family: var(--display);
  font-weight: 690;
  font-variation-settings: "wght" 690;
  /* Capped lower than the wordmark on purpose: a two-line release title set
     at the wordmark's size climbs to 20% of the stage, above where the veil
     has any density to give it. */
  font-size: clamp(1.7rem, calc(5.6 * var(--uw)), 4.3rem);
  line-height: 1;
  letter-spacing: -0.04em;
  text-wrap: balance;
}

.hero__now-genre {
  font-family: var(--mono);
  font-size: 0.78rem;
  line-height: 1.5;
  letter-spacing: 0.055em;
  text-transform: uppercase;
  color: var(--ink-2);
  max-width: 26ch;
}

.hero__now-side {
  display: grid;
  gap: 0.75rem;
  align-content: end;
}

/* Wide enough for the pair to sit side by side: the name takes the room it
   needs and the description hangs off its right shoulder, on the baseline. */
@media (min-width: 900px) {
  .hero__now {
    grid-template-columns: minmax(0, auto) minmax(16ch, 26ch);
    align-items: end;
    gap: clamp(1.25rem, calc(3 * var(--uw)), 2.75rem);
  }

  .hero__now-side {
    padding-bottom: 0.4em;
    border-top: 1px solid var(--hair);
    padding-top: 0.9rem;
  }
}

/* --------------------------------------------------------------------------
   9d. The living lyric
   The line that is sounding, with the words lighting as they are sung. It is
   set in the page's own voice rather than a music app's: paper-white on the
   word that has landed, muted on the ones still coming, and the next line
   ghosted underneath so the eye has somewhere to go.
   -------------------------------------------------------------------------- */

.hero__lyric {
  opacity: 0;
  transition: opacity 420ms var(--ease-signature) 120ms;
}

.hero__lyric.is-on {
  opacity: 1;
}

.lyric__now {
  font-size: clamp(0.95rem, calc(1.35 * var(--uw)), 1.2rem);
  line-height: 1.38;
  /* A lyric is read at singing speed, so it gets weight and zero tracking -
     the one place on the site where legibility beats tightness outright. */
  letter-spacing: 0em;
  font-weight: 560;
  /* Unsung words are present but not yet lit, so the line reads as one
     sentence arriving rather than as words appearing out of nothing. 0.38 was
     tuned when this was decoration on a dark stage; the owner's REDUX target
     asks for the current line to be READ, and 0.38 measures 3.13:1. 0.66 puts
     the un-lit half at 6.6:1 and the lit words still jump to full ink. */
  color: rgba(var(--fore-rgb), 0.66);
  text-wrap: balance;
}

.lyric__w {
  transition: color 160ms linear, opacity 160ms linear;
}

.lyric__w.is-sung {
  color: var(--ink);
}

.lyric__next {
  margin-top: 0.35rem;
  font-size: clamp(0.82rem, calc(1.05 * var(--uw)), 0.95rem);
  line-height: 1.34;
  /* The line that has not arrived yet. Deliberately below the current one -
     it is a preview, not content - but lifted off 0.26, which was invisible. */
  color: rgba(var(--fore-rgb), 0.46);
  text-wrap: balance;
}

.hero__lyric.is-instrumental {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --------------------------------------------------------------------------
   9e. The lyric sheet
   The whole text on demand. Built by script.js, because without JavaScript
   there is nothing behind it and a button that does nothing is worse than no
   button at all.
   -------------------------------------------------------------------------- */

.reel__lyrics {
  margin-top: 0.55rem;
  align-self: start;
  padding: 0.3rem 0;
  border: 0;
  background: none;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
  position: relative;
  transition: color 0.18s ease;
}

.reel__lyrics::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.15rem;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.28s var(--ease-signature);
}

@media (hover: hover) and (pointer: fine) {
  .reel__lyrics:hover {
    color: var(--accent);
  }
  .reel__lyrics:hover::after {
    transform: scaleX(1);
  }
}

.reel__lyrics:focus-visible {
  color: var(--accent);
}

.sheet {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: end center;
}

.sheet__scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.sheet__panel {
  position: relative;
  width: min(46rem, 100%);
  max-height: min(78dvh, 46rem);
  display: flex;
  flex-direction: column;
  background: var(--paper-lift);
  border: 1px solid var(--hair);
  border-bottom: 0;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  animation: sheet-in 340ms var(--ease-signature) both;
}

@keyframes sheet-in {
  from {
    opacity: 0;
    transform: translate3d(0, 16px, 0);
  }
}

.sheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem clamp(1.15rem, calc(3 * var(--uw)), 2rem);
  border-bottom: 1px solid var(--hair);
  background: var(--paper-lift);
}

.sheet__head .meta {
  color: var(--ink);
}

.sheet__close {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 1px solid var(--hair);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: color 0.18s ease, border-color 0.18s ease;
}

@media (hover: hover) and (pointer: fine) {
  .sheet__close:hover {
    color: var(--accent);
    border-color: var(--accent);
  }
}

.sheet__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: clamp(1.25rem, calc(3 * var(--uw)), 2rem);
  padding-bottom: clamp(2rem, calc(5 * var(--uw)), 3.5rem);
}

.sheet__line {
  font-size: clamp(1rem, calc(1.5 * var(--uw)), 1.15rem);
  line-height: 1.58;
  color: var(--ink-2);
}

.sheet__section {
  margin-top: 1.75rem;
  margin-bottom: 0.5rem;
  color: var(--accent);
}

.sheet__section:first-child {
  margin-top: 0;
}

.sheet__instrumental {
  font-family: var(--mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The page behind a sheet does not scroll. Both elements, because the
   stylesheet configures html as the scroller and overflow on body alone left
   iOS free to keep moving the page; the fixed body at a saved negative offset
   (see script.js) is what actually pins it, and overscroll-behavior stops the
   scroll chaining that produced the rubber-band behind the dialog. */
.has-sheet,
.has-sheet body {
  overflow: hidden;
  overscroll-behavior: none;
}

.has-sheet body {
  position: fixed;
  inset-inline: 0;
  width: 100%;
}

.reels--hero {
  margin-top: 0;
}

.reels--hero .reel__info {
  padding: clamp(0.7rem, calc(1.1 * var(--uw)), 1rem);
  gap: 0.2rem;
}

.reels--hero .reel__date {
  font-size: 0.62rem;
  letter-spacing: 0.09em;
}

.reels--hero .reel__title {
  font-size: clamp(0.8rem, calc(1.05 * var(--uw)), 0.98rem);
  line-height: 1.22;
}

.reels--hero .pf {
  margin-top: 0.4rem;
  gap: 0.6rem;
}

.reels--hero .pf__icon {
  width: 16px;
  height: 16px;
}

.hero__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem 2rem;
  margin-top: 0;
  align-content: center;
  z-index: 3;
}

.hero__cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.75rem;
}

/* --------------------------------------------------------------------------
   9a. The stage
   The portrait is the whole hero background, full bleed, and everything else
   floats over it. Three nested transform layers so nothing fights:
     .hero__bg          scroll parallax, the deepest rate in the stack
     .hero__bg-breathe  ambient breathing loop
     .hero__bg-img      pointer drift
   The veil is deliberately NOT inside any of them: a readability scrim that
   moves is not a readability scrim.

   Treatment: warm duotone, not full colour. The rest of the site is strict
   warm monochrome, and a full-colour photograph here would be the only colour
   on the whole site and would read as a foreign object rather than as this
   label's cover. Grayscale plus a measured sepia turns it into a duotone
   press photograph, which is what the print concept wants. For full colour,
   set --hero-filter to none.
   -------------------------------------------------------------------------- */

:root {
  --hero-focus: 50% 26%;
  /* Softens the upscale of a 1000px sleeve stretched to a full-bleed stage.
     Set to 0 once dedicated hero graphics land - they will not need it. */
  --hero-release-blur: 2px;
  /* KINO: strict black and white. The warm duotone belonged to the paper
     world; on black stock the portrait is a print, not a tint.

     The brightness is a contrast fixture, not a taste setting. At 0.82 the
     idle wordmark measured 4.16:1 against the bokeh on the right of the frame
     - the one colourway never checked when the staged duotone was tuned,
     because the duotone runs far darker and passed on its own. Removing the
     ticker made it worse by lifting the type 54px into a brighter band.

     Dropping brightness far enough to fix it on its own (0.70) worked and
     made the sitter's face murky, because it pays for one bright corner by
     dimming the whole frame. The failing pixel was the bokeh on the RIGHT, so
     most of the correction is a right-edge vignette in the veil instead.

     brightness(0.8) IS the owner's "image at 80%": over a black ground a 0.8
     multiplier and an 0.8 layer opacity are the same pixel. It is expressed
     as brightness rather than opacity so it cannot collide with the opacity
     transitions the release crossfade already drives on these same layers. */
  --hero-filter: grayscale(1) contrast(1.16) brightness(0.8);
}

.hero__bg {
  position: absolute;
  /* Overhang on both sides so the parallax never exposes an edge. */
  inset: -20% 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.hero__bg-breathe {
  position: absolute;
  inset: 0;
}

.hero__bg-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hero-focus);
  filter: var(--hero-filter);
  transform: translate3d(var(--ptr-x, 0px), var(--ptr-y, 0px), 0);
  transition: transform 0.55s var(--ease-out);
}

/* One static element carrying every readability guarantee:
     1. a warm ink wash over the whole photograph, so it tones to the palette
     2. a bottom ramp to solid paper, which both gives the wordmark a ground
        and dissolves the image into the paper sections below
     3. a left wash, because the type is left-aligned  */
.hero__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  --veil-ramp: rgba(var(--ground-rgb), 0) 0%, rgba(var(--ground-rgb), 0.04) 10%,
    rgba(var(--ground-rgb), 0.42) 15%, rgba(var(--ground-rgb), 0.68) 21%,
    rgba(var(--ground-rgb), 0.84) 28%, rgba(var(--ground-rgb), 0.92) 40%,
    rgba(var(--ground-rgb), 0.96) 60%, rgba(var(--ground-rgb), 0.98) 78%,
    var(--paper) 92%;
  background:
    /* The ramp is tuned against measured pixels, not by eye: it reaches
       meaningful density just above the wordmark's cap line so the worst
       pixel behind the type clears 7:1, while the face above stays clear. */
    /* Same job, opposite direction: the lower band is driven down to solid
       black so light type has a ground, and the ramp is re-measured against
       real pixels rather than guessed. */
    /* The single most load-bearing gradient on the site, and the shape is the
       whole point: it must stay out of the way across the top of the frame,
       where the photograph and the ASCII field are the design, and it must be
       dense BEFORE the type starts, not gradually on the way through it.

       The two numbers it is cut against: the release title's cap line at 20%
       of the stage and the wordmark's at 24%. Everything above 14% is the
       image at the owner's 80%; between 16% and 30% the ramp does all of its
       work at once. Flattening this out is what dropped the wordmark to
       2.16:1 when the flat pass above was thinned.

       It is a custom property because the stops are cut against where the
       type actually lands, and a phone's hero stacks the same content into a
       portrait frame - its wordmark sits at 10.8% of the stage where the
       desktop's sits at 20.6%. One ramp cannot serve both; see section 18. */
    linear-gradient(to bottom, var(--veil-ramp)),
    /* Reads left-to-right as: ground under the type, a clear middle for the
       sitter's face, then a vignette closing the right edge - which is where
       the out-of-focus highlights sit and where the wordmark ran out of
       ground. A vignette is also simply what this photograph wants. */
    linear-gradient(
      to right,
      rgba(var(--ground-rgb), 0.62) 0%,
      rgba(var(--ground-rgb), 0.2) 46%,
      rgba(var(--ground-rgb), 0.14) 63%,
      rgba(var(--ground-rgb), 0.52) 100%
    ),
    /* The flat pass used to sit at 0.34-0.5 across the whole stage, which is
       what made an 80% image read like a 45% one. It now barely touches the
       top of the frame and only closes up where the type lives. */
    linear-gradient(to bottom, rgba(var(--ground-rgb), 0.05), rgba(var(--ground-rgb), 0.46));
}

/* --------------------------------------------------------------------------
   9c. Panels and the frosted material
   One component, two states. Over paper it is a solid lifted surface; over the
   photographic plate it becomes frosted glass, because that is the only place
   on the site where controls sit on top of an image and need the material to
   stay legible. Radius stays 0 like everything else, so the glass reads as a
   darkroom mask rather than as an iOS pill dropped into a print layout.
   -------------------------------------------------------------------------- */

.panel {
  position: relative;
  padding: clamp(1rem, calc(2.1 * var(--uw)), 1.4rem) clamp(1.05rem, calc(2.3 * var(--uw)), 1.6rem);
  background: var(--paper-lift);
  border: 1px solid var(--hair);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .panel--glass {
    background: rgba(var(--ground-rgb), 0.52);
    -webkit-backdrop-filter: blur(22px) saturate(1.5);
    backdrop-filter: blur(22px) saturate(1.5);
    border-color: rgba(var(--fore-rgb), 0.1);
    /* Still light catching the material, just far less of it on black. */
    border-top-color: rgba(var(--fore-rgb), 0.2);
    box-shadow:
      inset 0 1px 0 rgba(var(--fore-rgb), 0.12),
      0 12px 32px -14px rgba(0, 0, 0, 0.6);
  }
}

/* Vibrancy: text over a translucent surface needs more contrast and a little
   more weight than the same text over paper, not less. */
.panel--glass .meta {
  color: var(--ink-2);
  font-weight: 500;
  letter-spacing: 0.12em;
}


/* --------------------------------------------------------------------------
   10. Home - index rows
   -------------------------------------------------------------------------- */

.index-row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.5rem 2rem;
  padding-block: clamp(1.6rem, calc(3.6 * var(--uw)), 2.5rem);
  border-top: 1px solid var(--hair-soft);
}

.index-row:first-child {
  border-top: 0;
}

/* Ink arrives from the side the pointer entered. --o is set in script.js. */
.index-row__rule {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: var(--o, left) center;
  transition-property: transform, background-color;
  transition: transform 0.5s var(--ease-out);
}

.index-row__label {
  font-family: var(--display);
  font-weight: var(--wght-mid);
  font-variation-settings: "wght" 600;
  font-size: clamp(1.5rem, calc(3.4 * var(--uw)), 2.25rem);
  line-height: 1.08;
  letter-spacing: var(--track-mid);
  transition: transform 0.45s var(--ease-out);
}

.index-row--lead {
  padding-block: clamp(2.1rem, calc(4.6 * var(--uw)), 3.25rem);
}

.index-row--lead .index-row__label {
  font-size: clamp(2.1rem, calc(5.6 * var(--uw)), 3.6rem);
  letter-spacing: -0.04em;
}

.index-row__note {
  grid-column: 1 / -1;
  max-width: 46ch;
  color: var(--muted);
  font-size: 0.975rem;
  margin-top: 0.4rem;
  transition: transform 0.45s var(--ease-out) 40ms, color 0.3s ease;
}

.index-row__arrow {
  align-self: center;
  display: inline-flex;
  color: var(--muted);
  transition: transform 0.4s var(--ease-out), color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .index-row:hover .index-row__rule {
    transform: scaleX(1);
  }
  .index-row:hover .index-row__label,
  .index-row:hover .index-row__note {
    transform: translate3d(10px, 0, 0);
  }
  .index-row:hover .index-row__note {
    color: var(--ink-2);
  }
  .index-row:hover .index-row__arrow {
    transform: translate3d(6px, -6px, 0);
    color: var(--ink);
  }
}

/* The release takes the stage. Its source is set per tile via data-hero, so
   swapping in dedicated artwork later is a file drop plus one attribute. */
.hero__bg-release {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: var(--hero-release, none);
  background-size: cover;
  background-position: 50% 50%;
  /* Full colour, full strength. This layer used to be a grayscale duotone at
     53% brightness - a structural luminance cap that made any future artwork
     safe without tuning. The owner looked at it and said the obvious thing:
     you cannot see the cover.

     So the cap moves off the image and onto the scrim. The artwork is now
     untouched except for the upscale blur, and .hero__guard below - which only
     exists while a release is staged - is what buys the type its ground. The
     cost is that the guard is now cut against MEASURED artwork rather than
     against a guaranteed ceiling: a future hero graphic far brighter than
     these six sleeves would need it re-measured. That trade is deliberate. */
  filter: blur(var(--hero-release-blur, 2px));
  transform: scale(1.06);
  opacity: 0;
  transition: opacity 950ms var(--ease-out);
}

/* The accent no longer recolours the sleeve - it would be repainting the very
   thing the owner asked to be able to see. What is left is a whisper of the
   colourway at the very top, where no type ever lands, so the stage still
   belongs to the record's colour without touching the art. */
.hero__bg-release::after {
  content: "";
  position: absolute;
  inset: 0 0 62% 0;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--accent) 34%, transparent),
    transparent
  );
  mix-blend-mode: soft-light;
}

.is-hero-release .hero__bg-release {
  opacity: 1;
}

/* A staged sleeve is far brighter than the portrait ever is - the E.G.A.L.
   lettering measured 2.99:1 behind the wordmark on its own. This deepens the
   type band only while a release is up, so the artwork keeps the upper stage
   and the wordmark keeps its ground. Tuned against measured pixels. */
.hero__guard {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  /* Recut for full-strength artwork. The old ramp was flat until 34% of the
     stage, which was fine behind a 53%-brightness duotone and does nothing at
     all behind a lit sleeve - the release title's cap line is at 15.5%. This
     one closes in the 10-20% band, the same window the veil uses, and stacks
     with it. */
  background: linear-gradient(
    to bottom,
    rgba(var(--ground-rgb), 0) 0%,
    rgba(var(--ground-rgb), 0.04) 9%,
    rgba(var(--ground-rgb), 0.44) 14%,
    rgba(var(--ground-rgb), 0.68) 20%,
    rgba(var(--ground-rgb), 0.82) 30%,
    rgba(var(--ground-rgb), 0.9) 45%,
    rgba(var(--ground-rgb), 0.95) 68%,
    var(--paper) 92%
  );
  transition: opacity 950ms var(--ease-out);
}

.is-hero-release .hero__guard {
  opacity: 1;
}

.is-hero-release .hero__bg-img {
  opacity: 0;
}

.hero__bg-img {
  transition: opacity 950ms var(--ease-out);
}

/* --------------------------------------------------------------------------
   9a3. The colourway takeover
   While a record plays the whole page moves into its palette, not just the
   hero. Weighted into the corners so the tint is unmistakable at the edges of
   the screen and still leaves the middle - where the type lives - alone.
   -------------------------------------------------------------------------- */

.colorwash {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(calc(76 * var(--uw)) 58vh at 8% -6%, var(--accent), transparent 68%),
    radial-gradient(calc(76 * var(--uw)) 58vh at 96% 106%, var(--accent), transparent 70%);
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 950ms var(--ease-out);
}

.is-playing-audio .colorwash {
  opacity: 0.2;
}

/* The rules of the page take the colour too. */
.is-playing-audio {
  --hair: color-mix(in srgb, var(--accent) 42%, rgba(var(--fore-rgb), 0.14));
  --hair-soft: color-mix(in srgb, var(--accent) 34%, rgba(var(--fore-rgb), 0.07));
}


/* --------------------------------------------------------------------------
   10b. Reels
   Three portrait tiles, the cover drifting inside each. "Reel" is read here
   the way a press shop reads it, not the way a social app does: radius stays
   0, the frame is a hairline, and the empty state is the same unprinted
   sleeve as the catalogue. Sparse by instruction: frame, catalogue number,
   title, listening links. Nothing else.
   -------------------------------------------------------------------------- */

.reels {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1rem, calc(2.4 * var(--uw)), 2rem);
  margin-top: clamp(1.75rem, calc(4 * var(--uw)), 3rem);
}

/* The reels aspect the owner asked for. The artwork inside is square and is
   never cropped to fit it: a 9:16 crop of a 1:1 sleeve would cut the lettering
   off half this catalogue. The square sits at the top, the release sits under
   it, and the frame holds both. */
.reel {
  display: flex;
  flex-direction: column;
  aspect-ratio: 9 / 16;
  border: 1px solid var(--hair);
  background: var(--paper-lift);
}

.reel__art {
  position: relative;
  flex: 1;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-bottom: 1px solid var(--hair);
  /* Empty state, kept as the pattern for releases that do not exist yet. */
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(var(--fore-rgb), 0.07) 0 1px,
    transparent 1px 9px
  );
}

/* A square sleeve cannot fill a 9:16 frame, and cropping it to fit would cut
   the lettering off half this catalogue. So the frame fills itself with the
   same cover thrown far out of focus, and the sharp sleeve sits on top of it
   at its true proportions. The bands above and below become part of the
   record rather than empty tile. */
.reel__bed {
  position: absolute;
  inset: -14%;
  background-image: var(--cover, none);
  background-size: cover;
  background-position: center;
  /* The bed follows its sleeve: monochrome and held well down until the
     record is the one playing. */
  filter: grayscale(1) blur(26px) brightness(0.55);
  transition: filter 950ms var(--ease-out);
  transform: scale(1.08);
}

.reel__cover {
  position: relative;
  /* It is also the link to the release now. */
  display: grid;
  color: inherit;
  text-decoration: none;
  width: 100%;
  aspect-ratio: 1 / 1;
  /* The artwork is a real <img> now, not a background: it belongs in the
     document for a crawler, a reader mode and an image search, and it gets an
     alt. The layer keeps its own filter and ken-burns, which apply to the
     image through it exactly as they did to the background. */
  background-image: none;
  /* KINO reverses the light system's rule. There, the artwork was the only
     colour and shipped untouched. Here the whole page is black and white and
     colour means one thing: this is alive. A sleeve is monochrome until it is
     hovered or playing, and then it comes up in full. */
  filter: var(--cover-filter, grayscale(1) contrast(1.08) brightness(0.92));
  transition: filter 950ms var(--ease-out);
  display: grid;
  place-items: center;
}

/* Image and fallback mark share one grid cell, so the mark is simply what is
   there when the file is missing or still loading. */
.reel__img,
.reel__mark {
  grid-area: 1 / 1;
}

.reel__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--cover-focus, 50% 50%);
  display: block;
  z-index: 1;
}

/* The title is the second way into the release page - a 17px link is a poor
   target on its own, and the cover is the one people will actually hit. */
.reel__link {
  color: inherit;
  text-decoration: none;
}

@media (hover: hover) and (pointer: fine) {
  .reel__link:hover {
    color: var(--accent);
  }
}

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

.reel__mark {
  font-family: var(--display);
  font-weight: 690;
  font-variation-settings: "wght" 690;
  font-size: clamp(0.95rem, calc(1.7 * var(--uw)), 1.3rem);
  line-height: 0.92;
  letter-spacing: -0.04em;
  color: var(--ink);
  opacity: 0.5;
  white-space: nowrap;
}

/* A cover that is actually there does not need the empty sleeve's mark. */
.reel[style*="--cover"] .reel__mark {
  display: none;
}

.reel__pass {
  position: absolute;
  inset: -10% -40%;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    102deg,
    transparent 40%,
    rgba(var(--fore-rgb), 0.18) 50%,
    transparent 60%
  );
  transform: translate3d(-32%, 0, 0);
  transition: transform 0.95s var(--ease-out), opacity 0.35s ease;
}

.reel__info {
  /* Sizes to its own content: the art takes whatever is left, which is what
     makes the sleeve sit in a band of its own blurred bed. */
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 0.15rem;
  padding: clamp(0.85rem, calc(1.6 * var(--uw)), 1.15rem);
}

.reel__date {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.reel__title {
  font-size: clamp(0.95rem, calc(1.5 * var(--uw)), 1.0625rem);
  font-weight: 560;
  letter-spacing: var(--track-strong);
  line-height: 1.26;
  text-wrap: balance;
}

@media (hover: hover) and (pointer: fine) {
  .reel:hover .reel__cover {
    filter: none;
  }

  .reel:hover {
    border-color: var(--accent);
  }

  .reel:hover .reel__pass {
    opacity: 1;
    transform: translate3d(32%, 0, 0);
  }
}

/* The heading row carries no note; the reels below are the content. */
.index-row--bare {
  padding-block: clamp(1.4rem, calc(3 * var(--uw)), 2rem);
}

/* --------------------------------------------------------------------------
   10c. Listening platforms
   Brand marks drawn as inline SVG in the site's ink, in the owner's order of
   prominence. Each is a link slot: while its href is still "#" the attribute
   selector below holds it in a pending state, and the moment a real URL is
   pasted in the pending state lifts itself. No CSS or JS to touch.
   -------------------------------------------------------------------------- */

.pf {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  margin-top: 0.55rem;
}

.pf__link {
  position: relative;
  display: inline-flex;
  color: var(--ink-2);
  transition: color 0.2s ease, opacity 0.2s ease, transform 0.2s var(--ease-out);
}

.pf__icon {
  width: 17px;
  height: 17px;
  fill: currentColor;
}

/* Counterforms are struck in paper, the way the marks are built. */
.pf__counter {
  fill: none;
  stroke: var(--paper);
  stroke-width: 1.15;
  stroke-linecap: round;
}

.pf__fill-counter {
  fill: var(--paper);
}

/* Pending: no URL yet. Self-lifting. Held back enough to read as inactive,
   not so far back that it reads as dust. */
.pf__link[href="#"] {
  opacity: 0.42;
  pointer-events: none;
}

@media (hover: hover) and (pointer: fine) {
  .pf__link:hover {
    color: var(--ink);
    transform: translate3d(0, -1px, 0);
  }
}


/* --------------------------------------------------------------------------
   10d. Preview playback
   The tile is the control surface, the bar is the transport, and the accent
   of whatever is playing is loaned to the page for as long as it plays.
   -------------------------------------------------------------------------- */

/* The play affordance. Real button, so it works by tap, click and keyboard;
   hover preview on a fine pointer is the enhancement layered on top. */
/* Round, big, and centred in the lower third of the artwork - the owner's
   spec. The translate is part of the button's resting geometry, so every
   state that touches transform must carry it along. */
.reel__play {
  position: absolute;
  left: 50%;
  top: 70%;
  transform: translate(-50%, -50%);
  z-index: 3;
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  padding: 0;
  border: 1px solid rgba(var(--fore-rgb), 0.4);
  border-radius: 50%;
  background: rgba(var(--ground-rgb), 0.58);
  color: var(--paper-lift);
  cursor: pointer;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  transition: background-color 600ms var(--ease-out), border-color 0.2s ease,
    transform 0.2s var(--ease-out);
}

.reel__glyph {
  width: 22px;
  height: 22px;
  fill: currentColor;
}

/* The triangle's visual mass sits left of its box centre; one nudge rights it. */
.reel__glyph--play {
  margin-left: 3px;
}

/* THE GLYPH SAYS WHAT A CLICK WILL DO, NOT WHAT THE AUDIO IS DOING. A hover
   preview plays sound, but the button still reads > - because the click it
   invites PINS the record. Pause only appears once the visitor has pinned
   it, i.e. once a click would actually stop something they chose.
   .is-pinned carries that; .is-playing keeps dressing the tile (frame,
   colour, bed) for previews and pins alike. */
.reel__glyph--pause,
.reel.is-pinned .reel__glyph--play {
  display: none;
}

.reel.is-pinned .reel__glyph--pause {
  display: block;
}

.reel.is-pinned .reel__play {
  background: var(--accent);
  border-color: rgba(var(--fore-rgb), 0.85);
}

/* The frame takes the record's colour while it plays, and the sleeve comes
   up out of black and white with it. */
.reel {
  transition: border-color 950ms var(--ease-out);
}

.reel.is-playing .reel__cover {
  filter: none;
}

.reel.is-playing .reel__bed {
  filter: blur(26px) saturate(1.3) brightness(0.72);
}

.reel.is-playing {
  border-color: var(--accent);
}

@media (hover: hover) and (pointer: fine) {
  .reel__play:hover {
    background: rgba(var(--ground-rgb), 0.8);
    transform: translate(-50%, -50%) scale(1.06);
  }
}

/* Browsers block sound before the page has had a real gesture. When that
   happens the tile says so instead of silently doing nothing. */
.reel__hint {
  position: absolute;
  left: 0.6rem;
  right: 3.6rem;
  bottom: 0.6rem;
  z-index: 3;
  margin: 0;
  padding: 0.4rem 0.5rem;
  background: rgba(var(--ground-rgb), 0.78);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.2;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}

.reel.is-locked .reel__hint {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   The transport bar
   -------------------------------------------------------------------------- */

/* The tour's position chip, beside the transport's status line. */
.player__tour {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--accent);
  white-space: nowrap;
}

.player {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* The transport lives inside the 885 frame like everything else. */
  width: var(--site-w);
  margin-inline: auto;
  z-index: 30;
  border-top: 1px solid var(--hair);
  background: var(--paper-lift);
  transform: translate3d(0, 0, 0);
  transition: transform 0.45s var(--ease-out), opacity 0.3s ease;
  /* The home indicator sits over the bottom edge on a modern iPhone. Stand the
     transport's controls off it rather than under it. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.player[hidden] {
  display: none;
}

/* Once the bar is up it covers the foot of the page, so give the page back
   the height it is standing on. */
.has-player body {
  padding-bottom: 68px;
  padding-bottom: calc(68px + env(safe-area-inset-bottom, 0px));
}

@media (max-width: 620px) {
  /* 66px is the bar at its tallest here: a 44px touch target plus its own
     padding, which is taller than the 58px floor .player__in sets. */
  .has-player body {
    padding-bottom: 66px;
    padding-bottom: calc(66px + env(safe-area-inset-bottom, 0px));
  }
}

.player.is-entering {
  transform: translate3d(0, 100%, 0);
  opacity: 0;
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .player {
    background: rgba(var(--ground-rgb), 0.68);
    -webkit-backdrop-filter: blur(22px) saturate(1.5);
    backdrop-filter: blur(22px) saturate(1.5);
    border-top-color: rgba(var(--fore-rgb), 0.2);
    box-shadow: 0 -12px 32px -16px rgba(0, 0, 0, 0.7);
  }
}

/* The analyser draws here. It sits behind the controls, bled to both edges,
   so on the home page it reads straight across the portrait above it. */
.player__wave {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0.5;
  pointer-events: none;
}

/* The seek control. It is 2px of ink to look at and a 16px band to hit -
   the padding is the target, the bar is the drawing. */
.player__seek {
  position: absolute;
  left: 0;
  right: 0;
  top: -8px;
  height: 16px;
  cursor: pointer;
  touch-action: none;
  z-index: 4;
}

.player__seek:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

/* A hairline of the track so the control is visibly seekable before anyone
   drags it. */
.player__seek::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 7px;
  height: 2px;
  background: var(--hair);
}

.player__progress {
  transition: background-color 950ms var(--ease-out);
  position: absolute;
  left: 0;
  top: 7px;
  height: 2px;
  width: 100%;
  background: var(--accent);
  transform: scaleX(var(--progress, 0));
  transform-origin: left center;
  pointer-events: none;
}

.player__in {
  position: relative;
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, calc(2 * var(--uw)), 1.5rem);
  min-height: 66px;
  padding-block: 0.6rem;
}

.player__toggle {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--paper);
  cursor: pointer;
  transition: background-color 600ms var(--ease-out), transform 0.12s var(--ease-out);
}

.player__toggle:active {
  transform: scale(0.96);
}

.player__glyph {
  width: 13px;
  height: 13px;
  fill: currentColor;
}

.player__glyph--play {
  display: none;
}

.player.is-paused .player__glyph--play {
  display: block;
}

.player.is-paused .player__glyph--pause {
  display: none;
}

.player__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.95rem;
  font-weight: 560;
  letter-spacing: var(--track-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.player__time {
  flex: 0 0 auto;
  display: flex;
  gap: 0.35rem;
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.player__links {
  flex: 0 0 auto;
  margin-top: 0;
}

@media (max-width: 620px) {
  .player__time {
    display: none;
  }

  .player__in {
    gap: 0.75rem;
    min-height: 58px;
  }
}

/* The colourway reaches the stage: a wash over the upper photograph only.
   It deliberately stops well above the wordmark so the measured contrast of
   the veil's lower ramp is untouched in every colourway. */
.hero__tone {
  position: absolute;
  /* Stops at 48% of the hero, which measures ~49px clear of the wordmark's
     cap line. Any overlap at all costs contrast even where the gradient looks
     transparent, because it multiplies. */
  inset: 0 0 56% 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--accent) 46%, transparent) 0%,
    color-mix(in srgb, var(--accent) 20%, transparent) 55%,
    transparent 100%
  );
  /* On paper the colourway had to stain downward; on black it lights upward,
     which is why it can be carried further than it was in the light system. */
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 950ms var(--ease-out);
}

.is-playing-audio .hero__tone {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   11. Releases - sleeves
   -------------------------------------------------------------------------- */

.slots {
  display: grid;
  grid-template-columns: 1.3fr 1fr 1fr;
  gap: clamp(1.5rem, calc(3.4 * var(--uw)), 3rem);
  align-items: start;
}

.slot__cover {
  position: relative;
  aspect-ratio: 1 / 1;
  background: var(--paper-lift);
  border: 1px solid var(--hair);
  display: grid;
  place-items: center;
  overflow: hidden;
  --rule-a: 0.075;
  --lift: 0;
}

/* Fine diagonal ruling: an unprinted sleeve, with real surface and no image. */
.slot__cover::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(var(--fore-rgb), var(--rule-a)) 0 1px,
    transparent 1px 9px
  );
  transition: background-image 0.45s ease;
}

.slot__cover::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(38% 26% at 50% 50%, rgba(var(--ground-rgb), 0.9), transparent 72%);
}

/* The ink roller passing across the sheet. */
.sweep {
  position: absolute;
  inset: -10% -30%;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    102deg,
    transparent 40%,
    rgba(var(--fore-rgb), 0.12) 50%,
    transparent 60%
  );
  transform: translate3d(-32%, 0, 0);
  transition: transform 0.95s var(--ease-out), opacity 0.35s ease;
}

.slot__glyph {
  position: relative;
  z-index: 3;
  font-family: var(--display);
  font-weight: 690;
  font-variation-settings: "wght" 690;
  font-size: clamp(1.15rem, calc(2.4 * var(--uw)), 1.65rem);
  line-height: 0.92;
  letter-spacing: -0.02em;
  color: var(--ink);
  opacity: 0.58;
  transition: opacity 0.4s var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .slot:hover .slot__cover {
    --rule-a: 0.1;
  }
  .slot:hover .slot__glyph {
    opacity: 0.92;
  }

  .slot:hover .slot__cover {
    border-color: var(--accent);
  }
  .slot:hover .sweep {
    opacity: 1;
    transform: translate3d(32%, 0, 0);
  }
  .slot:hover .redact {
    transform: scaleX(1.03);
  }
}

.slot__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  margin-top: 1.1rem;
  font-variant-numeric: tabular-nums;
}

/* Unnamed things print as a bar. Same device as the slug. */
.redact {
  display: block;
  height: clamp(1.15rem, calc(2.4 * var(--uw)), 1.55rem);
  margin-top: 0.85rem;
  background: rgba(var(--fore-rgb), 0.1);
  background-image: repeating-linear-gradient(
    135deg,
    rgba(var(--fore-rgb), 0.06) 0 5px,
    transparent 5px 10px
  );
  transform-origin: left center;
  transition: transform 0.5s var(--ease-out);
}

/* Empty link slots. Square outline chips read as "a link goes here", and stay
   at full --muted contrast rather than being faded below AA. */
.slot__services {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.95rem;
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.slot__services li {
  padding: 0.3rem 0.5rem;
  border: 1px dashed var(--hair);
  line-height: 1;
}

/* --------------------------------------------------------------------------
   12. Contact
   -------------------------------------------------------------------------- */

.mailto {
  display: inline-block;
  margin-top: clamp(1.75rem, calc(4 * var(--uw)), 2.75rem);
  font-family: var(--display);
  font-weight: 690;
  font-variation-settings: "wght" 690;
  font-size: clamp(1.45rem, calc(5.6 * var(--uw)), 3.4rem);
  line-height: 1.08;
  letter-spacing: -0.038em;
  overflow-wrap: anywhere;
  border-bottom: 2px solid var(--hair);
  padding-bottom: 0.12em;
  transition: border-color 0.24s ease;
}

@media (hover: hover) and (pointer: fine) {
  .mailto:hover {
    border-color: var(--ink);
  }
}

.note {
  margin-top: clamp(2.5rem, calc(5.5 * var(--uw)), 4rem);
  color: var(--muted);
  max-width: 52ch;
  font-size: 0.95rem;
}

/* --------------------------------------------------------------------------
   13. 404
   -------------------------------------------------------------------------- */

.notfound {
  min-height: calc(100dvh - var(--header-h));
  display: grid;
  align-content: center;
  padding-block: clamp(4rem, calc(12 * var(--uw)), 8rem);
}

/* --------------------------------------------------------------------------
   14. Footer
   -------------------------------------------------------------------------- */

.site-footer {
  position: relative;
  z-index: 2;
  flex-shrink: 0;
  border-top: 1px solid var(--hair);
  padding-block: clamp(2.25rem, calc(4.5 * var(--uw)), 3.25rem);
}

.site-footer__in {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem 2.5rem;
}

.site-footer__legal {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.07em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   15. Entrance choreography
   Gated on .js so the page is fully readable without JavaScript.
   -------------------------------------------------------------------------- */

.js .wordmark {
  transform: translateY(132%);
  transition: transform 0.95s var(--ease-out) 60ms;
}

.js.is-ready .wordmark {
  transform: none;
}

/* The hero slug prints itself a beat after the letters land. That delay is the
   whole point of the move: it shows the mark being set. */
.js .slug__fill {
  transform: scaleX(0);
  transition: transform 0.7s var(--ease-out) 780ms;
}

.js.is-ready .slug__fill {
  transform: none;
}

/* Every other instance of the mark is seen on every single page load, so it
   does not animate at all. */
.js .site-header .slug__fill,
.js .site-footer .slug__fill,
.js .slot__cover .slug__fill {
  transform: none;
  transition: none;
}


.js [data-rise] {
  opacity: 0;
  transform: translate3d(0, 14px, 0);
  transition: opacity 0.7s var(--ease-out) var(--d, 0ms),
    transform 0.7s var(--ease-out) var(--d, 0ms);
}

.js.is-ready [data-rise] {
  opacity: 1;
  transform: none;
}

.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 16px, 0);
  transition: opacity 0.65s var(--ease-out) var(--d, 0ms),
    transform 0.65s var(--ease-out) var(--d, 0ms);
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* Printed elements arrive the way a press lays ink down: left to right.
   Driven by the same observer as [data-reveal], so above-the-fold headings
   ink in at load and below-the-fold ones ink in when they are reached. */
.js [data-ink] {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.85s var(--ease-out) var(--d, 0ms);
}

.js [data-ink].is-in {
  clip-path: inset(0 0 0 0);
}

/* --------------------------------------------------------------------------
   16. Scroll-driven motion (off the main thread; enhancement only)
   -------------------------------------------------------------------------- */

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Hero register layers. Larger --px means the layer lags further behind
       the scroll, so it reads as further away. */
    .px {
      animation-name: px-lag;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: scroll(root block);
      animation-range: 0 100vh;
      will-change: transform;
    }

    @keyframes px-lag {
      to {
        transform: translate3d(0, var(--px, 0), 0);
      }
    }

    /* The focal moment: the ink plate drifts out of register with the paper
       "1" as the sheet travels, and re-registers when you scroll back. */
    .hero .slug__reg {
      animation-name: dereg;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: scroll(root block);
      animation-range: 0 70vh;
      will-change: transform;
    }

    @keyframes dereg {
      to {
        transform: translate3d(0.075em, -0.05em, 0);
      }
    }


    /* Reading position. */
    .press-rule {
      animation-name: press-travel;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: scroll(root block);
    }

    @keyframes press-travel {
      to {
        transform: scaleX(1);
      }
    }
  }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Sleeves slide against their own captions at three different depths, so
       the three sheets pass each other like a stack being fanned. */
    .slot__cover {
      animation-name: sheet;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
      will-change: transform;
    }

    @keyframes sheet {
      from {
        transform: translate3d(0, var(--sheet, 20px), 0);
      }
      to {
        transform: translate3d(0, calc(var(--sheet, 20px) * -1), 0);
      }
    }

    /* The stage: the deepest rate in the table, so the content visibly
       travels over a slower photograph. */
    .hero__bg {
      animation-name: stage-lag;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: scroll(root block);
      animation-range: 0 100vh;
      will-change: transform;
    }

    @keyframes stage-lag {
      to {
        transform: translate3d(0, 16vh, 0);
      }
    }

    /* Body copy lags its own heading very slightly: the same out-of-register
       idea, at a whisper. */
    .drift {
      animation-name: copy-lag;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }

    @keyframes copy-lag {
      from {
        transform: translate3d(0, 8px, 0);
      }
      to {
        transform: translate3d(0, -8px, 0);
      }
    }
  }
}

/* --------------------------------------------------------------------------
   17. Cross-document view transitions - CURTAINS

   A slanted curtain sweeps the old page off and the new one on behind it. The
   overlay's own background IS the curtain: the old snapshot is clipped away in
   the direction of travel, which uncovers the colour, and then the new
   snapshot is clipped in from the same side, continuing past. Two phases of
   0.5s, the second delayed by the first, so the whole move is 1s - the cap on
   how long input may sit frozen.

   The slant is 9deg, which at the polygon level is tan(9deg) x 100vh of
   horizontal offset: 15.84vh. It is a vh because the edge's lean has to hold
   whatever the viewport's height is.

   Direction is carried as a view transition TYPE, set by script.js on both
   sides of the navigation. Types do not cross documents, so the arriving page
   re-derives its own; without JavaScript no type is ever set, no rule below
   matches, and the browser's plain crossfade is what happens. Firefox has no
   cross-document view transitions at all and simply navigates.
   -------------------------------------------------------------------------- */

@view-transition {
  navigation: auto;
}

:root {
  --curtain-skew: 15.84vh;
  /* Their curtain default. Accelerating entry, decelerating exit - a drawn
     sheet, not a slide. It is symmetric about its midpoint, which is what
     lets the mirrored direction below run the same keyframes in reverse. */
  --curtain-ease: cubic-bezier(0.76, 0, 0.24, 1);
  --curtain-dur: 0.5s;
}

/* The curtain itself. On a black site a black curtain is an invisible wipe, so
   it carries the label's colour - and now that the label's colour IS the
   ground's orange-to-red, the curtain carries the whole gradient rather than a
   flat sample of it. The sweep reads as the stage itself being drawn across
   the page.

   ONE curtain for every navigation, including release-bound ones. A curtain
   that changed colour per destination would be signal nobody has time to read
   - it is on screen for half a second - and the record already announces its
   colourway on arrival, on the page ground, where it stays. The distinction is
   better made by the thing that lasts than by the thing that flashes. */
::view-transition {
  background: var(--accent-sheet);
}

/* The mark is NOT carried across as a shared element during a wipe. It sits in
   the same place in both documents, so morphing it achieves nothing visible -
   and a logo hanging over the curtain while the page is gone reads as a stuck
   element rather than as persistent chrome. It keeps its shared-element
   treatment for untyped navigations, where the old page fades rather than
   leaves. */
:root:active-view-transition-type(wipe-forward) .masthead__link,
:root:active-view-transition-type(wipe-back) .masthead__link {
  view-transition-name: none;
}

/* ---- forward: deeper into the site ------------------------------------- */

:root:active-view-transition-type(wipe-forward)::view-transition-old(root) {
  animation: curtain-cover var(--curtain-dur) var(--curtain-ease) both;
}

:root:active-view-transition-type(wipe-forward)::view-transition-new(root) {
  animation: curtain-reveal var(--curtain-dur) var(--curtain-ease)
    var(--curtain-dur) both;
}

/* ---- back: the mirror -------------------------------------------------- */

/* Mirroring the geometry is exactly running the other phase backwards: the
   shape the cover ends on is the shape the reveal starts from. Because the
   easing is symmetric, `reverse` costs nothing in feel and keeps one set of
   keyframes as the single source of truth for both directions. */
:root:active-view-transition-type(wipe-back)::view-transition-old(root) {
  animation: curtain-reveal var(--curtain-dur) var(--curtain-ease) both reverse;
}

:root:active-view-transition-type(wipe-back)::view-transition-new(root) {
  animation: curtain-cover var(--curtain-dur) var(--curtain-ease)
    var(--curtain-dur) both reverse;
}

/* The old page's visible area collapses to the left edge, uncovering the
   curtain behind it. */
@keyframes curtain-cover {
  from {
    clip-path: polygon(
      0% 0%,
      calc(100% + var(--curtain-skew)) 0%,
      100% 100%,
      0% 100%
    );
  }
  to {
    clip-path: polygon(
      0% 0%,
      0% 0%,
      calc(0% - var(--curtain-skew)) 100%,
      0% 100%
    );
  }
}

/* The new page continues past in the same direction rather than meeting the
   old one - that is what makes it read as one sheet travelling, not two. */
@keyframes curtain-reveal {
  from {
    clip-path: polygon(
      calc(100% + var(--curtain-skew)) 0%,
      100% 0%,
      100% 100%,
      100% 100%
    );
  }
  to {
    clip-path: polygon(
      0% 0%,
      100% 0%,
      100% 100%,
      calc(0% - var(--curtain-skew)) 100%
    );
  }
}

/* ---- untyped: sideways moves, the 404, and anything unranked ----------- */

:root:active-view-transition-type(fade)::view-transition-old(root) {
  animation: vt-out 0.24s var(--ease-out) both;
}

:root:active-view-transition-type(fade)::view-transition-new(root) {
  animation: vt-in 0.42s var(--ease-out) both;
}

@keyframes vt-out {
  to {
    opacity: 0;
    transform: translate3d(0, -10px, 0);
  }
}

@keyframes vt-in {
  from {
    opacity: 0;
    transform: translate3d(0, 14px, 0);
  }
}

/* --------------------------------------------------------------------------
   17b. Touch devices - the same page, priced for a phone
   Two separate jobs, both keyed on the pointer rather than on width, because
   both are about the input device and not about how much room there is:

     1. Hit areas. A 17px platform mark is a perfectly good printed mark and an
        impossible target for a thumb. The marks stay exactly the size they are
        drawn; an invisible 44px pad is laid over each one.
     2. Cost. Everything here still renders identically - what is removed is
        only the *perpetual* work: full-screen loops, ken-burns on six blurred
        beds, and per-tile backdrop samples. On a desktop GPU they are free;
        on a phone they are a tax charged on every frame the visitor scrolls.
   -------------------------------------------------------------------------- */

@media (hover: none), (pointer: coarse) {

  /* ---- hit areas ------------------------------------------------------ */

  /* Marks keep their drawn size; the target around them grows to 44px. */
  .pf {
    gap: 1.75rem;
  }

  .pf__link::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }

  /* The play control is 64px everywhere now, well past the 44px floor; only
     the transport's toggle still needs growing here. */
  .player__toggle {
    width: 44px;
    height: 44px;
  }

  .reel__hint {
    right: 4rem;
  }

  /* The links stretch the full 60px header row already; the whole block is
     the tap target, so nothing extra is needed for hit area here. */

  /* ---- perpetual motion, switched off --------------------------------- */

  /* A full-viewport composited layer scaling forever behind everything. */
  .ambient {
    animation: none;
    will-change: auto;
  }

  /* The stage carries a four-stop filter chain over a full-bleed photograph.
     Scaling it forever makes WebKit re-rasterise the filtered layer on every
     frame. The scroll parallax below it is compositor work and stays. */
  .hero__bg-breathe {
    animation: none;
    will-change: auto;
  }

  /* Six sleeves and six 26px-blurred beds, all drifting, all the time. */
  .reel__cover,
  .reel__bed {
    animation: none;
    will-change: auto;
  }

  /* The bed is a static backdrop now, so it can be blurred far more cheaply
     and still read as the same out-of-focus sleeve. */
  .reel__bed {
    filter: blur(16px) saturate(1.1) brightness(0.94);
  }

  /* ---- backdrop samples, thinned ------------------------------------- */

  /* A backdrop sample per tile, over a blurred bed, is the single most
     expensive thing on the releases page. At 44px the frost was never
     legible anyway - a solid ink chip reads better and costs nothing. */
  .reel__play {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(var(--ground-rgb), 0.82);
  }

  /* A full-screen backdrop sample behind a scrim that is already 72% black
     buys nothing on a phone and costs a composite of the whole page. */
  .sheet__scrim {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(0, 0, 0, 0.86);
  }

  /* The header and the transport keep the frosted material - it is the
     design - at a radius a phone GPU can sample while the page is moving. */
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .site-header {
      background: rgba(var(--ground-rgb), 0.86);
      -webkit-backdrop-filter: blur(10px);
      backdrop-filter: blur(10px);
    }

    .player {
      background: rgba(var(--ground-rgb), 0.86);
      -webkit-backdrop-filter: blur(12px);
      backdrop-filter: blur(12px);
    }

    /* One glass panel over a still photograph is affordable; the saturate
       pass on top of the blur is not. */
    .panel--glass {
      background: rgba(var(--ground-rgb), 0.72);
      -webkit-backdrop-filter: blur(16px);
      backdrop-filter: blur(16px);
    }
  }
}

/* --------------------------------------------------------------------------
   18. Responsive
   -------------------------------------------------------------------------- */

/* Stacked: the plate sits above the type, inset from the right so the column
   of crop ticks still reads as a registered edge rather than a full bleed. */
@media (max-width: 899px) {

}

@media (max-width: 900px) {
  .slots {
    grid-template-columns: 1fr 1fr;
  }
  .slot:first-child {
    grid-column: 1 / -1;
    max-width: 62%;
  }
}

@media (max-width: 620px) {
  :root {
    --header-h: 54px;
  }

  .nav {
    --nav-gap: 0.2rem;
    --nav-pad: 0.55rem;
    font-size: 0.68rem;
    letter-spacing: 0.06em;
  }

  .site-header__in {
    gap: 0.75rem;
  }

  /* Single column, stepped in and down so three empty sleeves read as a
     descending sequence instead of three identical voids. */
  .slots {
    grid-template-columns: 1fr;
    gap: 3rem;
  }

  .slot:first-child {
    max-width: 90%;
  }

  .slot:nth-child(2) {
    max-width: 74%;
    margin-left: auto;
  }

  .slot:nth-child(3) {
    max-width: 60%;
  }

  /* Reels are a swipe format on a phone, and three stacked portrait tiles is
     an endless page. Snap track instead: it bleeds to the screen edges by
     cancelling the container's own padding and putting it back as scroll
     padding, so the track scrolls and the page never does. */
  .reels {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 68%;
    gap: 0.85rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    margin-inline: calc(var(--pad) * -1);
    padding-inline: var(--pad);
    scroll-padding-inline: var(--pad);
    scrollbar-width: none;
    /* Without this the track's own width leaks into the document's scroll
       width and iOS Safari lets the whole page slide sideways - the reason
       the site "swam" on a phone. `contain: paint` says the track paints
       nothing outside itself, which is already true, and both engines then
       stop counting it. The track still scrolls and snaps normally. */
    contain: paint;
  }

  .reels::-webkit-scrollbar {
    display: none;
  }

  .reels > li {
    scroll-snap-align: start;
  }

  .index-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  /* The stage needs real height on a phone or it is a letterbox, not a stage.
     The portrait source is 4:5 and the viewport is far taller than it is
     wide, so object-fit crops the sides; --hero-focus keeps the sitter's face
     in frame rather than letting cover decide. */
  .hero {
    min-height: calc(100dvh - var(--header-h));
    align-content: end;
    padding-block: 2.5rem 1.75rem;
    --hero-focus: 52% 22%;
  }

  /* On a phone the deck stacks: the panel compresses to a single row and the
     reels become the snap track, so one tile is fully in the first screen with
     the next peeking as the swipe affordance. */
  .hero__deck {
    gap: 1rem;
    margin-top: 1.5rem;
  }

  .hero__foot {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 1rem;
  }

  /* Orbiter sets wider than Archivo did at the same size, and the pair landed
     a pixel over the panel's inner width - one row became two. A tighter gap
     alone left under a pixel of slack, which is not a fit, so the buttons take
     a size step here too: 300px of button in a 308px row becomes 285 in 308. */
  .hero__cta {
    gap: 0.75rem 0.55rem;
    align-items: center;
  }

  .hero__cta .btn {
    font-size: 0.9rem;
  }

  /* A phone has no room for both. Idle, the field is the top of the stage and
     the wordmark sits clear below it; playing, the release title, its genre
     line and the living lyric fill that same band from 4.8% of the stage
     down, and there is nowhere for white glyphs to go that is not behind
     type. So the field yields to the words - which is the right way round,
     because while a record is playing the words ARE the hero. The colour
     world is still carried by the wash and the staged artwork. */
  .is-playing-audio .hero__field {
    opacity: 0;
  }

  /* The nav's touch padding fills a 61px header down to y52, so the band gets
     the 9px that are left. One row of glyphs over the gradient - an animated
     colour rule rather than a field, which is the right scale for a phone's
     chrome anyway. Nothing it draws can land behind a link. */
  :root {
    --band-h: 8px;
  }

  /* The phone stacks the same hero into a portrait frame, so its type starts
     at 10.8% of the stage instead of 20.6% and the desktop ramp leaves the
     wordmark on bare photograph at 3.65:1. This one closes in the 5-13% band
     instead. The stage is a darker frame on a phone as a result, which is the
     honest trade: there is no arrangement of one 100dvh portrait hero that
     carries a bright photograph, a full type stack and a big tile at once. */
  .hero__veil {
    --veil-ramp: rgba(var(--ground-rgb), 0) 0%, rgba(var(--ground-rgb), 0.05) 5%,
      rgba(var(--ground-rgb), 0.5) 9%, rgba(var(--ground-rgb), 0.78) 13%,
      rgba(var(--ground-rgb), 0.88) 20%, rgba(var(--ground-rgb), 0.94) 35%,
      rgba(var(--ground-rgb), 0.97) 60%, var(--paper) 88%;
  }

  /* And even idle the band has to come up: a phone's hero is dense enough
     that the wordmark's own cap line lands at 11.8% of the stage, so the
     field gets the strip above that and nothing more. The canvas grew to 45%
     of the stage in this round, so the stop had to come back proportionally:
     20% of 45% is 9% of the stage, the same clearance the old 26% of 34% gave.
     A phone's hero has no spare room, so this band does not grow. */
  .hero__field {
    --field-ramp: rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 4%,
      rgba(0, 0, 0, 0.5) 10%, rgba(0, 0, 0, 0.12) 14%, rgba(0, 0, 0, 0) 17%;
  }

  /* One big tile with the next breaking the edge as the swipe affordance.
     64% is the ceiling, not a taste call: the frame is 9:16, so at 390px wide
     anything above it makes the tile taller than the room left under the
     panel and the first tile stops fitting in the first screen. */
  .reels--hero {
    grid-auto-columns: 64%;
  }


  .hero__foot {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.5rem;
  }

  .site-footer__in {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.1rem;
  }

  .lead {
    max-width: none;
  }

}

@media (max-width: 400px) {
  .nav {
    --nav-gap: 0.1rem;
    --nav-pad: 0.42rem;
    font-size: 0.645rem;
  }
  .mark {
    font-size: 0.95rem;
  }
}

/* --------------------------------------------------------------------------
   19. Reduced motion - fewer and gentler, not zero
   Position, parallax and register drift are removed. Ink, colour and state
   feedback stay, because they carry meaning.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .ambient {
    animation: none;
  }

  .js .wordmark,
  .js .slug__fill,
  .js [data-rise],
  .js [data-reveal],
  .js [data-ink] {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    stroke-dashoffset: 0 !important;
    transition-duration: 0.01ms !important;
  }

  .px,
  .hero .slug__reg,
  .slot__cover,
  .drift,
  .hero__bg,
  .hero__bg-breathe,
  .hero__bg-img {
    animation: none !important;
    transform: none !important;
  }

  /* The reels hold still. scale(1) still fills the frame exactly, so nothing
     is lost but the movement. */
  .reel__cover,
  .reel__bed {
    animation: none !important;
    transform: none !important;
  }

  /* Playback still works; it just stops moving. The analyser paints one
     static frame instead of animating, and the colourway swaps without a
     pulse. */
  :root {
    transition: none !important;
  }

  .colorwash,
  .hero__guard,
  .hero__bg-release,
  .hero__bg-img,
  .player,
  .hero__tone,
  .reel,
  .reel__play,
  .player__progress {
    transition: none !important;
  }

  .player.is-entering {
    transform: none !important;
    opacity: 1 !important;
  }

  .reel__pass,
  .pf__link {
    transition-duration: 0.01ms !important;
  }

  /* The stage stays, it just stops moving. */
  .hero__bg-img {
    transition-duration: 0.01ms !important;
  }

  /* The reading position still updates, it just does not animate. */
  .press-rule {
    display: none;
  }

  /* The type swap is a cut, not a crossfade, and the field paints one frame.
     The lyric line still arrives - it is information, not decoration - it
     just does not sweep word by word (script.js renders it as plain text). */
  .hero__mask,
  .hero__now,
  .hero__field,
  .header__field,
  .hero__lyric,
  .lyric__w {
    transition: none !important;
    transform: none !important;
  }

  .sheet__panel {
    animation: none !important;
  }

  /* The sweep still marks the state - it just arrives at once. Keeping the
     colour flip is the point: hover and focus must still be legible. */
  .btn,
  .btn::before,
  .btn::after,
  .btn__icon,
  .btn__icon svg,
  .index-row__label,
  .index-row__note,
  .index-row__arrow,
  .index-row__rule,
  .nav a,
  .nav a::after,
  .link::after,
  .sweep,
  .redact,
  .slot__glyph {
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* Translucency is a preference, not a fixture: make the material solid rather
   than merely frostier, and keep the edge that carries the hierarchy. */
@media (prefers-reduced-transparency: reduce) {
  .site-header,
  .panel--glass,
  .player,
  .sheet__scrim {
    background: var(--paper-lift);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  /* The scrim's whole job is to bury the page, so it goes solid rather than
     merely unfrosted - a lifted panel colour here would leave the dialog
     floating on a readable page. */
  .sheet__scrim {
    background: var(--paper);
  }

  .site-header {
    background: var(--paper);
    border-bottom-color: var(--hair);
  }

  .site-header::after {
    display: none;
  }

  .panel--glass {
    border-color: var(--hair);
    box-shadow: none;
  }
}

/* More contrast: near-solid surface with a border that is actually visible. */
@media (prefers-contrast: more) {
  .panel--glass {
    background: var(--paper-lift);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-color: var(--ink-2);
    box-shadow: none;
  }

  .panel--glass .meta {
    color: var(--ink);
  }
}

/* --------------------------------------------------------------------------
   20. Release pages
   site/releases/<slug>/index.html, generated by scripts/build-site.js from
   scripts/releases.json. Same Kino system, no hero stage: the artwork IS the
   image on these pages, so it is shown rather than veiled, and the type sits
   beside it on plain ground where no scrim is needed.
   -------------------------------------------------------------------------- */

.release__in {
  padding-block: clamp(2rem, calc(5 * var(--uw)), 3.5rem) var(--sec);
}

.release__back {
  margin-bottom: clamp(2rem, calc(5 * var(--uw)), 3.5rem);
}

.link-back {
  color: var(--muted);
  transition: color 0.18s ease;
}

.link-back::before {
  content: "\2190";
  margin-right: 0.6em;
}

@media (hover: hover) and (pointer: fine) {
  .link-back:hover {
    color: var(--accent);
  }
}

.release__head {
  display: grid;
  gap: clamp(1.75rem, calc(4 * var(--uw)), 3rem);
  align-items: start;
}

@media (min-width: 860px) {
  .release__head {
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
    gap: clamp(2.5rem, calc(5 * var(--uw)), 4.5rem);
  }
}

.release__art {
  position: relative;
  border: 1px solid var(--hair);
  background: var(--paper-lift);
}

/* The one place on the site where a sleeve is shown whole, unfiltered and at
   the size it was drawn. Everywhere else it is a texture; here it is the
   subject. */
.release__cover {
  width: 100%;
  height: auto;
  display: block;
}

.release__meta {
  display: grid;
  gap: clamp(0.85rem, calc(2 * var(--uw)), 1.25rem);
  align-content: start;
}

.release__title {
  margin-top: 0.1em;
}

.release__genre {
  color: var(--ink-2);
}

.release__listen {
  margin-top: clamp(0.75rem, calc(2 * var(--uw)), 1.25rem);
  display: grid;
  gap: 0.9rem;
  justify-items: start;
}

.release__lyrics {
  margin-top: var(--sec);
  padding-top: clamp(2rem, calc(4.5 * var(--uw)), 3rem);
  border-top: 1px solid var(--hair);
}

.release__text {
  margin-top: clamp(1.5rem, calc(3.5 * var(--uw)), 2.25rem);
  max-width: 54ch;
}

.release__line {
  font-size: clamp(1.02rem, calc(1.5 * var(--uw)), 1.18rem);
  line-height: 1.6;
  color: var(--ink-2);
}

.release__section {
  margin-top: 2rem;
  margin-bottom: 0.6rem;
  color: var(--accent);
}

.release__section:first-child {
  margin-top: 0;
}

.release__instrumental {
  font-family: var(--mono);
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Newer on the left, older on the right - the same axis the curtain wipes on,
   so the transition and the layout agree about which way the catalogue runs. */
.release__nav {
  margin-top: var(--sec);
  padding-top: clamp(1.75rem, calc(4 * var(--uw)), 2.5rem);
  border-top: 1px solid var(--hair);
  display: grid;
  gap: 1.25rem;
  grid-template-columns: 1fr 1fr;
}

.release__sib {
  display: grid;
  gap: 0.4rem;
  align-content: start;
  padding: clamp(1rem, calc(2.4 * var(--uw)), 1.5rem);
  border: 1px solid var(--hair);
  transition: border-color 0.24s ease;
}

.release__sib--next {
  text-align: right;
}

.release__sib-title {
  font-weight: var(--wght-mid);
  font-variation-settings: "wght" 600;
  font-size: clamp(1.05rem, calc(2 * var(--uw)), 1.3rem);
  letter-spacing: var(--track-mid);
  line-height: 1.2;
}

.release__sib.is-empty {
  border-color: transparent;
}

@media (hover: hover) and (pointer: fine) {
  .release__sib:hover {
    border-color: var(--accent);
  }
  .release__sib:hover .release__sib-title {
    color: var(--accent);
  }
}

@media (max-width: 620px) {
  .release__nav {
    grid-template-columns: 1fr;
  }
  .release__sib--next {
    text-align: left;
  }
}

/* Loading and failure, said out loud rather than left to a stalled bar. */
.player__state {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

.player__state:empty {
  display: none;
}

.player.is-error .player__state {
  color: var(--accent);
}

.player.is-loading .player__seek::before {
  background: linear-gradient(
    to right,
    var(--hair) 0%,
    var(--accent) 50%,
    var(--hair) 100%
  );
  background-size: 200% 100%;
  animation: seek-wait 1.1s linear infinite;
}

@keyframes seek-wait {
  to {
    background-position: -200% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .player.is-loading .player__seek::before {
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   21. Notify form
   Only ever in the document when an endpoint is configured (see script.js);
   otherwise the mailto button it replaces is what stays. Same sweep button,
   same grain, an input cut from the same hairlines as everything else.
   -------------------------------------------------------------------------- */

.notify {
  display: grid;
  gap: 0.7rem;
  width: 100%;
}

.notify__row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: stretch;
}

.notify__input {
  flex: 1 1 12rem;
  min-width: 0;
  padding: 0.95rem 1.05rem;
  background: rgba(var(--ground-rgb), 0.55);
  border: 1px solid var(--hair);
  color: var(--ink);
  font: inherit;
  font-size: 0.95rem;
  font-weight: var(--wght-text);
  letter-spacing: var(--track-text);
  line-height: 1;
  transition: border-color 0.24s ease, background-color 0.24s ease;
}

.notify__input::placeholder {
  color: var(--muted);
}

.notify__input:focus {
  outline: none;
  border-color: var(--accent);
  background: rgba(var(--ground-rgb), 0.75);
}

.notify__input:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.notify__input:disabled {
  opacity: 0.5;
}

.notify__submit {
  flex: 0 0 auto;
}

.notify__submit:disabled {
  opacity: 0.6;
  cursor: progress;
}

.notify__consent {
  font-family: var(--mono);
  font-size: 0.64rem;
  line-height: 1.5;
  letter-spacing: 0.06em;
  color: var(--muted);
  max-width: 44ch;
}

.notify__state {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 44ch;
}

.notify__state:empty {
  display: none;
}

.notify.is-bad .notify__state {
  color: var(--accent);
}

.notify.is-ok .notify__state {
  color: var(--ink);
}

.notify.is-bad .notify__input {
  border-color: var(--accent);
}

/* --------------------------------------------------------------------------
   22. REDUX - the sheet on the gradient

   Three planes, and the whole layout is the relationship between them:

     body      the gradient, document-length
     .band     a full-viewport-bleed strip of the hero imagery, absolutely
               placed under the header. It is WIDER than the sheet, so its
               left and right thirds stay visible as "wings" beside the sheet,
               and its lower half runs BEHIND the sheet.
     .sheet    the dark column the site actually lives on - the wrap made
               visible. It starts partway down the band and grows with its
               content, so the footer and the index rows are inside it and
               the gradient reappears below its end.
   -------------------------------------------------------------------------- */

.sheet {
  position: relative;
  z-index: 1;
  width: var(--sheet-w);
  margin-inline: auto;
  background: var(--paper);
  /* Grows with content; the gradient below the end is simply the page. */
  padding-bottom: 0;
}

/* Inside the sheet the wrap is no longer the thing that centres and caps the
   column - the sheet is. It keeps only its job of holding the gutter. */
.sheet .wrap {
  max-width: none;
  margin-inline: 0;
}

/* The header is sheet-width too, so the gradient shows in the top corners
   beside it exactly as the mock has it. */
.site-header {
  width: var(--sheet-w);
  margin-inline: auto;
}

.site-header .wrap {
  max-width: none;
  margin-inline: 0;
}

/* --- the band ------------------------------------------------------------ */

.band {
  position: absolute;
  top: var(--header-h);
  /* The band is a strip of the 885 FRAME now, not of the viewport: its box
     caps at --site-w and centres. left+right+margin auto, NOT a transform -
     a transformed ancestor would turn the fixed-attachment backgrounds
     inside it local and break the knockout registration. (100% of body, not
     100vw: vw includes the scrollbar.) */
  left: 0;
  right: 0;
  margin-inline: auto;
  width: var(--site-w);
  height: var(--hero-band-h);
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* A straight horizontal cut at the bottom, per the mock. */
}

/* Both the band and the knockout letters paint the SAME background with the
   same rules, and `fixed` is what makes them line up without a single line of
   measuring JavaScript: a fixed background is positioned against the viewport,
   so two elements that declare it identically are two windows onto one image.
   Move this off `fixed` and the knockout stops registering with the band. */
.band__img,
.band__release {
  position: absolute;
  inset: 0;
  background-image: var(--band-src, url("assets/heroes/label.jpg"));
  background-attachment: fixed;
  background-position: center 36%;
  background-size: cover;
  background-repeat: no-repeat;
}

.band__release {
  background-image: var(--hero-release, none);
  opacity: 0;
  transition: opacity 950ms var(--ease-out);
}

.is-hero-release .band__release {
  opacity: 1;
}

.is-hero-release .band__img {
  opacity: 0;
  transition: opacity 950ms var(--ease-out);
}

/* A whisper of ground colour top and bottom so the band reads as lit rather
   than as a photograph dropped in, and so its bottom cut is not a hard line
   against the sheet's black. */
.band__tone {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(var(--ground-rgb), 0.5) 0%,
    rgba(var(--ground-rgb), 0.12) 34%,
    rgba(var(--ground-rgb), 0.22) 78%,
    rgba(var(--ground-rgb), 0.6) 100%
  );
}

/* The field runs over the band, unchanged in behaviour. */
.band .hero__field {
  position: absolute;
  inset: 0;
  height: 100%;
  z-index: 2;
}

/* --- the home hero, now living on the sheet ------------------------------ */

.hero {
  position: relative;
  min-height: 0;
  display: block;
  overflow: visible;
  transform: none;
  padding-block: 0 clamp(2.5rem, calc(5 * var(--uw)), 4rem);
}

/* The sheet starts partway down the band. The type block then pulls up again
   so the wordmark's cap line crosses the sheet's top edge into the band - the
   one place on the page where content is allowed outside its own plane. */
.home .sheet {
  margin-top: calc(var(--sheet-top) - var(--header-h));
}

.hero__type {
  margin-top: calc(-1 * clamp(1.5rem, 4vh, 3.25rem));
}

/* --- the tile row and the CTA band --------------------------------------- */

/* One grid: the tiles sit on the same inset as the wordmark and the tagline,
   full width of the sheet's content column, no side panel. Scoped above the
   phone breakpoint on purpose - a row of three on a 390px sheet is three
   hundred-pixel stamps, and the snap track that has always been there is the
   right answer at that width. */
@media (min-width: 621px) {
  .reels--hero {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(1rem, calc(2 * var(--uw)), 1.75rem);
    margin-top: clamp(1.75rem, calc(4 * var(--uw)), 3rem);
  }
}

/* The call to action is a band across the sheet under the row - the stacked
   presentation the phone has always had, promoted to the layout everywhere. */
.cta-band {
  margin-top: clamp(1.75rem, calc(4 * var(--uw)), 3rem);
  padding: clamp(1.25rem, calc(3 * var(--uw)), 2rem) 0 0;
  border-top: 1px solid var(--hair);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(0.85rem, calc(2.5 * var(--uw)), 2rem);
}

.cta-band .meta {
  margin-right: auto;
}

.cta-band__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1rem;
}

@media (max-width: 620px) {
  :root {
    --sheet-w: 96%;
    /* The same idea at phone scale: more artwork than before (30vh -> ~46svh),
       type lower, but stopping well short of the desktop ratio so the tile row
       is still one natural swipe away rather than a full screen down. */
    --hero-band-h: min(46svh, 380px);
    --sheet-top: min(26svh, 230px);
  }

  .cta-band {
    align-items: flex-start;
    flex-direction: column;
    gap: 1.1rem;
  }

  .cta-band .meta {
    margin-right: 0;
  }
}

/* --------------------------------------------------------------------------
   23. The knockout wordmark

   The mark is punched out of the sheet: the band's imagery shows through the
   letterforms and reads brighter than it does in the band itself, the way a
   knockout layer behaves in Photoshop.

   Technique: a duplicate span painted with the band's own background and
   clipped to the text, sitting in the same grid cell as the real <h1>. The
   duplicate is aria-hidden; the <h1> keeps the text and the outline.

   Why this and not an SVG mask with backdrop-filter: backdrop-filter brightens
   what is BEHIND, and behind the lower half of the mark there is only the
   sheet's black - brightness() on #0a0a0b is still #0a0a0b, so the mark would
   fade out halfway down. Painting the image into the letters instead keeps it
   legible over black AND over the band, which is the behaviour the design
   needs rather than the one the filter gives.

   Alignment is free: both this and .band__img declare the identical fixed
   background, so they are two windows onto one image. Nothing measures.
   -------------------------------------------------------------------------- */

.hero__mask {
  display: grid;
  overflow: visible;
}

.hero__mask > * {
  grid-area: 1 / 1;
}

.wordmark {
  /* +40% on the old ceiling: the mark is the page's one loud thing now. */
  font-size: clamp(2.6rem, calc(10.4 * var(--uw)), 8.25rem);
}

.wordmark--punch {
  /* A LUMINANCE FLOOR, and it is a measurement not a taste call. The image
     filling the letters runs the full range of the photograph, and where it is
     at its darkest the mark measured 1.06:1 against the sheet - the site's own
     name disappearing in patches. `lighten` composites the image against this
     grey per channel, so nothing in the fill can fall below it while every
     brighter part of the picture still shows through untouched.
     rgb(134,134,142) is L=0.235, which is exactly 5:1 on #0a0a0b. */
  background-color: rgb(134, 134, 142);
  background-blend-mode: lighten;
  pointer-events: none;
  background-image: var(--hero-release, var(--band-src, url("assets/heroes/label.jpg")));
  background-attachment: fixed;
  background-position: center 36%;
  background-size: cover;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  /* The punch reads brighter than the band it is cut from. */
  filter: brightness(1.35) contrast(1.05) saturate(1.1);
}

/* The real mark hides only where the punch can actually replace it. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .home .hero__mask .wordmark:not(.wordmark--punch) {
    color: transparent;
  }
  .home .hero__mask .wordmark:not(.wordmark--punch) .slug__char {
    color: transparent;
  }
}

@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .wordmark--punch {
    display: none;
  }
}

/* The slug stays solid and light inside the punch: it is the one glyph that
   has to stay recognisable as a mark rather than dissolve into the picture. */
.wordmark--punch .slug__fill {
  background: var(--ink);
}

.wordmark--punch .slug__char {
  color: var(--paper);
  -webkit-text-fill-color: var(--paper);
}

/* A fixed background is a known cost on a touch device and iOS Safari has
   never handled it well: there the mark is simply ink, which is what the
   fallback above already draws. */
@media (hover: none), (pointer: coarse) {
  .wordmark--punch {
    display: none;
  }
  .home .hero__mask .wordmark:not(.wordmark--punch),
  .home .hero__mask .wordmark:not(.wordmark--punch) .slug__char {
    color: var(--ink);
  }
  .home .hero__mask .wordmark:not(.wordmark--punch) .slug__char {
    color: var(--paper);
  }
}

/* --- REDUX corrections ---------------------------------------------------- */

/* REGRESSION FIX: I made the header opaque to stop it reading brown over the
   orange, and took the frosted glass with it - the one thing the owner said
   should not change. The glass is back; what stops the mud is that the header
   now sits ON the sheet, so what it samples is the sheet's black and the
   content sliding under it, never the gradient. */
/* At rest the header is solid: what is behind it there is the GRADIENT, and a
   66% black over orange is brown, which is what went wrong when the glass was
   simply restored. The owner's own words describe the fix exactly - translucent
   "once content scrolls under it". So it is opaque until the sheet is actually
   passing beneath, and glass from then on, where the backdrop is the sheet's
   own black and the frost reads as frost. */
.site-header {
  background: var(--paper);
  /* Section 5 frosts the chrome unconditionally; this is the base state that
     overrides it, and it has to clear the backdrop-filter as well as the
     background. Setting only the background left an 18px backdrop blur running
     behind an opaque panel on every subpage at rest - invisible, and a real
     compositing layer for nothing. The @supports block below turns it back on
     exactly where it is wanted. */
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  transition: background-color 420ms var(--ease-out);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* THE FROST SPLITS BY PAGE, deliberately, because what is behind the chrome
     at rest is not the same thing on every page.

     HOME: frosted from the first pixel. The band now starts at y0, so at rest
     the chrome already has artwork and the ASCII cloud behind it - the frost
     has something to sample and the brown-over-orange problem that forced the
     scroll trigger in the first place is gone by construction, because there is
     no bare gradient up there any more.

     EVERY OTHER PAGE: no band. At rest the chrome sits on the gradient ground -
     and on a release page that ground is the record's own colourway, which on
     the warm records (Egal Live's #db4b2e, Breeze's #c86e14) is exactly the
     case that made 66% black over orange read as mud. So those stay solid at
     rest and frost the moment content is under them, which is the original
     behaviour and still the right one there. */
  .home .site-header,
  .is-scrolled .site-header {
    background: rgba(var(--ground-rgb), 0.66);
    -webkit-backdrop-filter: blur(18px) saturate(1.5);
    backdrop-filter: blur(18px) saturate(1.6);
  }

  /* The soft edge under the chrome belongs with the frost, so on the home page
     it is there from the start too. */
  .home .site-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 16px;
    background: linear-gradient(rgba(var(--ground-rgb), 0.55), rgba(var(--ground-rgb), 0));
    pointer-events: none;
  }

  /* The soft edge under the chrome returns with the glass. */
  .is-scrolled .site-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 16px;
    background: linear-gradient(rgba(var(--ground-rgb), 0.55), rgba(var(--ground-rgb), 0));
    pointer-events: none;
  }
}

/* The header is its own plane above the sheet so content passes beneath it. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
}

/* Covers come up. On the old black page the sleeves were texture and were
   damped to keep the type ahead of them; on the sheet they are the content and
   the owner asked for them bright. Monochrome-until-alive still holds - it is
   the amount of light that changes, not the rule. */
.reel__cover {
  filter: var(--cover-filter, grayscale(1) contrast(1.02) brightness(1.24));
}

.reel__bed {
  filter: grayscale(1) blur(26px) brightness(0.85);
  opacity: 0.55;
}

.reel.is-playing .reel__bed {
  filter: blur(26px) saturate(1.3) brightness(1);
}

/* The diagonal sheen across a sleeve was tuned against a dark cover. */
.reel__pass {
  opacity: 0;
}

/* The tiles sit on the sheet, so their own ground lifts a little off it or the
   card edge disappears. */
.reel {
  background: #131316;
}

@media (hover: hover) and (pointer: fine) {
  .reel:hover .reel__cover {
    filter: var(--cover-filter, grayscale(0) contrast(1.02) brightness(1.3));
  }
}

/* The blurred bed was doing the work of filling a 9:16 frame around a square
   sleeve on a black page. On the sheet, with the sleeve itself lifted, it only
   needs to stop the frame reading as two hard grey bands. */
.reel__bed {
  opacity: 0.32;
}

/* --------------------------------------------------------------------------
   24. The torn edge (grunge)

   Kept, on the owner's second look: the edge itself was never the problem, its
   POSITION was - it sat so high that the band read as a strip and the tear had
   nothing to tear away from. It rides the sheet's new, much lower top edge now
   (section 22a), with real artwork above it.

   A strip of the sheet's own ink pulled above the sheet's top edge, masked by
   fractal noise that is dense at the bottom and torn at the top. No image
   asset, no dependency, one data URI.
   -------------------------------------------------------------------------- */

.home .sheet::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: clamp(48px, 7vh, 96px);
  background: var(--paper);
  pointer-events: none;
  /* Vertical stretch on the noise makes the tears read as drips and flecks
     rather than as an even speckle. */
  -webkit-mask-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='900' height='120'><filter id='t' x='0' y='0' width='100%25' height='100%25'><feTurbulence type='fractalNoise' baseFrequency='0.014 0.09' numOctaves='4' seed='11'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1.9 0 0 0 -0.34'/></filter><rect width='900' height='120' filter='url(%23t)'/></svg>"),
    linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0.72) 38%, transparent 100%);
  mask-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='900' height='120'><filter id='t' x='0' y='0' width='100%25' height='100%25'><feTurbulence type='fractalNoise' baseFrequency='0.014 0.09' numOctaves='4' seed='11'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1.9 0 0 0 -0.34'/></filter><rect width='900' height='120' filter='url(%23t)'/></svg>"),
    linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0.72) 38%, transparent 100%);
  -webkit-mask-size: 100% 100%, 100% 100%;
  mask-size: 100% 100%, 100% 100%;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* Pages without a hero band still sit on the sheet, just starting right under
   the chrome with a sliver of gradient showing the seam. */
.sheet {
  margin-top: clamp(0.75rem, 2vh, 1.5rem);
}

.home .sheet {
  margin-top: calc(var(--sheet-top) - var(--header-h));
}

/* The sheet's own bottom: content ends, the gradient takes over. */
.site-footer {
  border-top: 1px solid var(--hair);
}

/* --------------------------------------------------------------------------
   25. The ground takes the record's colour

   While a preview plays, the page's gradient is no longer the label's orange
   but the ARTWORK's own two dominant colours, derived at build time into the
   manifest (see scripts/build-site.js). It is a second full-document layer
   rather than a swap on body's own background, because background-image does
   not transition and a hard cut across the whole page would be violent.
   -------------------------------------------------------------------------- */

/* WHY THIS NEVER WORKED, HONESTLY: the tint was a z-index:-1 pseudo-element of
   body while body ALSO painted the label gradient as its own background. body
   is static and establishes no stacking context, so the pseudo-element joined
   the ROOT stacking context's negative-z layer - which paints BEFORE the
   in-flow background of body. The layer was correctly positioned, correctly
   coloured and at opacity 1, and was covered by body's own orange every single
   frame. Forcing it to solid #00ff00 left the page unchanged, which is the
   proof. Not too subtle, not too slow: structurally invisible.

   The fix is to stop painting anything on body and make BOTH grounds
   pseudo-elements, so they sort against each other by z-index as intended. */
body::before,
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

/* The label's own ground, underneath. */
body::before {
  z-index: -2;
  background-image: linear-gradient(
    180deg,
    var(--ground-top) 0%,
    var(--ground-top) 34%,
    #f2680f 58%,
    var(--ground-bottom) 100%
  );
}

/* The record's ground, over it, faded in while a preview plays. */
body::after {
  z-index: -1;
  background-image: linear-gradient(
    180deg,
    var(--grad-a, var(--ground-top)) 0%,
    var(--grad-a, var(--ground-top)) 30%,
    var(--grad-b, var(--ground-bottom)) 100%
  );
  opacity: 0;
  transition: opacity 1000ms var(--ease-out);
}

.is-playing-audio body::after {
  opacity: 1;
}

/* A release's solo page IS that release: its ground is the record's pair
   permanently, with no playback and no JS - the build stamps --grad-a/--grad-b
   and this class onto <html>, so the very first paint is already dressed. */
.is-release-ground body::after {
  opacity: 1;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  body::after {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   25b. The curtains - black past the 885 frame

   Two fixed black strips covering everything outside the centred --site-w
   column: band wings, gradient, ambient - whatever bleeds wide is simply
   cropped by them, which is what lets the whole fixed-background register
   (band, knockout, grounds) stay exactly as it was. Zero width at 885 and
   below, so phones and small windows never see them. z-index above the
   lyrics sheet's viewport-wide scrim (60) but under the browser's top layer,
   where the dialogs live.
   -------------------------------------------------------------------------- */

html::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 80;
  pointer-events: none;
  --curtain-w: calc((100% - var(--site-w)) / 2);
  background:
    linear-gradient(#000, #000) left center / var(--curtain-w) 100% no-repeat,
    linear-gradient(#000, #000) right center / var(--curtain-w) 100% no-repeat;
}

/* --------------------------------------------------------------------------
   26. REDUX review corrections
   -------------------------------------------------------------------------- */

/* 1. The mark was too big. The mock's wordmark spans ~47% of a 1548 stage;
      ours spanned 74%. calc(5.85 * var(--uw)) puts it back on the mock's ratio at every width
      (727px of 1548, 843 of 1800, 673 of 1150) instead of filling the sheet. */
.wordmark {
  font-size: clamp(2.1rem, calc(5.85 * var(--uw)), 6.2rem);
}

/* 2. The band is black and white again. The new label.jpg is the colour
      original; colour on this page comes from the cloud and the field, and
      while a record plays from its own colourway - never from the photograph
      underneath. The knockout is cut from the same treated layer, so the mark
      greys with it. */
.band__img,
.wordmark--punch {
  filter: grayscale(1) contrast(1.08) brightness(0.92);
}

/* The punch must read as a HOLE cut in the sheet with light coming through it,
   not as a slightly paler grey. The band behind it is exposed at 0.92; the
   letters run at 1.72 - nearly double - so the same pixel is unmistakably
   brighter inside a letter than one pixel outside it. contrast comes down to
   1.02 because contrast above brightness crushes the bokeh highlights to flat
   white and the mark loses its photograph. The measured luminance floor stays:
   background-color + background-blend-mode:lighten still guarantee the darkest
   possible in-letter pixel clears the ratio on its own. */
.wordmark--punch {
  filter: grayscale(1) contrast(1.02) brightness(1.72);
}

/* 3. The staged release keeps the full-colour-when-alive rule it had before
      REDUX: the record's own artwork is the one colour thing allowed on the
      stage, which is the whole point of the takeover. */
.band__release {
  filter: none;
}

/* 5. The playing layout, per the owner's target: the title huge, solid ink,
      left, wrapping; a narrow column to its right holding the genre over the
      living lyric, hung from a thin accent rule. */
.hero__now-title {
  color: var(--ink);
  -webkit-text-fill-color: var(--ink);
  max-width: 16ch;
}

@media (min-width: 900px) {
  .hero__now {
    grid-template-columns: minmax(0, 1.55fr) minmax(15ch, 0.85fr);
    align-items: start;
    gap: clamp(1.5rem, calc(3.5 * var(--uw)), 3rem);
  }

  .hero__now-side {
    align-content: start;
    padding-top: 0.9rem;
    padding-bottom: 0;
    border-top: 2px solid var(--accent);
    gap: 0.9rem;
  }
}

.hero__now-genre {
  color: var(--muted);
  max-width: none;
}

.lyric__now {
  font-size: clamp(0.98rem, calc(1.25 * var(--uw)), 1.15rem);
  font-weight: 600;
}

/* 5b. The active tile: an accent edge, and a pause control that says so. */
.reel.is-playing {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.reel__state {
  position: absolute;
  left: 0;
  bottom: 0.55rem;
  padding: 0.25rem 0.7rem;
  background: rgba(var(--ground-rgb), 0.82);
  z-index: 3;
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0;
  transition: opacity 260ms var(--ease-out);
  pointer-events: none;
}

.reel.is-playing .reel__state {
  opacity: 1;
}

.reel.is-playing .reel__play {
  background: var(--accent);
  color: var(--paper);
  border-color: var(--accent);
}

.reel.is-playing .reel__play svg {
  fill: var(--paper);
}

/* --------------------------------------------------------------------------
   27. THE HERO FLOATS. Owner round: header knockout, the sheet's edge under
   the tiles, a stable type footprint.
   -------------------------------------------------------------------------- */

/* --- 27a. the band reaches past the tiles -------------------------------- */

/* The band no longer stops above the sheet - it runs behind the wordmark, the
   playing type AND the tile row, and is cut off by .sheet__paper. Its height
   only has to be GENEROUS rather than exact, because the paper is what defines
   the edge; anything the band paints below that is covered. --cloud-h keeps
   the ASCII field in the zone it always had, which is both the design (the
   cloud belongs at the top, not behind the tiles) and the reason the field
   only resolves a third of the cells it otherwise would. */
:root {
  /* The cloud starts at the top of the page now, so "quite far down the band"
     means past the sheet line and behind the tile row - the field carries on
     under the content instead of stopping above it. Still bounded, because
     every cell below this is resolved, struck and composited every frame. */
  --cloud-h: min(82svh, 900px);
  /* The band starts at y0 now, so its reach is measured from the top of the
     page rather than from under the chrome. */
  --band-reach: calc(var(--sheet-top) + 100svh);
}

/* THE BAND STARTS AT THE TOP OF THE PAGE. The header floats over the artwork
   instead of sitting above it, which is also what removes the two orange
   corner blocks that used to flank the chrome - there is no strip of ground
   left up there to show. The gradient is untouched everywhere it still reads:
   the side gutters below the sheet line, and under the sheet's end. */
.home .band {
  top: 0;
}

/* Full-bleed across the top exactly as before, then narrowed to the sheet's
   own width for the run down behind the type and the tiles. Without the
   narrowing the band covers the gutters for its whole new height and the
   gradient ground - the thing the last round fought to make visible - is
   buried again below the fold.

   One element, one clip, rather than a second image layer: the band keeps its
   crossfade, its release artwork and its tone, and the polygon simply stops
   painting outside the sheet below the cut. --cut is where the sheet begins,
   measured from the band's own top edge. */
.home .band {
  height: var(--band-reach);
  /* Band coordinates and page coordinates are the same thing now that the
     band starts at y0, so the cut is simply the sheet line. */
  --cut: var(--sheet-top);
  --gutter: calc((100% - var(--sheet-w)) / 2);
  clip-path: polygon(
    0 0,
    100% 0,
    100% var(--cut),
    calc(100% - var(--gutter)) var(--cut),
    calc(100% - var(--gutter)) 100%,
    var(--gutter) 100%,
    var(--gutter) var(--cut),
    0 var(--cut)
  );
}

/* The cloud stays where it was. */
.home .band .hero__field {
  inset: 0 0 auto 0;
  height: var(--cloud-h);
}

/* The veil is re-cut for the taller band, in viewport units rather than
   percentages: the band's own height is now generous-and-approximate, so a
   percentage stop would drift with it. Each stop is a zone with a job, and
   every one of them was set by measuring the worst pixel under the type that
   sits there, not by eye.
     0      - under the chrome, ground colour so the band reads as lit
     22svh  - the cloud: the thinnest veil on the page, the picture breathes
     42svh  - approaching the wordmark
     50svh  - the wordmark's line; a darker surround is what makes the
              knockout read as light coming THROUGH the letters
     64svh  - the playing title, genre and lyric set here
     84svh+ - the tile row, which brings its own card ground, so the veil
              stops climbing and the photograph stays a photograph */
.home .band__tone {
  background: linear-gradient(
    180deg,
    rgba(var(--ground-rgb), 0.46) 0,
    rgba(var(--ground-rgb), 0.16) 16svh,
    rgba(var(--ground-rgb), 0.3) 28svh,
    rgba(var(--ground-rgb), 0.5) 33svh,
    rgba(var(--ground-rgb), 0.68) 48svh,
    rgba(var(--ground-rgb), 0.62) 70svh,
    rgba(var(--ground-rgb), 0.62) 100%
  );
}

/* --- 27b. the sheet's edge ----------------------------------------------- */

/* The sheet paints nothing itself on the home page; .sheet__paper does, from
   the tile row down. .sheet is position:relative with z-index 1, so it IS a
   stacking context and a z-index:-1 child stays inside it - above the sheet's
   own (transparent) background, below every piece of content. That is the
   distinction this codebase got wrong once already with body::after. */
.home .sheet {
  background: transparent;
}

/* A plain flow wrapper holding everything below the tile row. Not a
   positioned overlay: `top: auto` with `bottom: 0` resolves TOP from the
   bottom edge (CSS 2.1 10.6.4), not from the static position, which collapsed
   the first attempt to a zero-height strip at the foot of the page. A wrapper
   needs no measuring and cannot drift from the tile row. */
.home .sheet__paper {
  position: relative;
  background: var(--paper);
}

/* The footer lives outside <main>, so it carries the ink itself and the black
   runs unbroken from the torn edge to the bottom of the page. */
.home .site-footer {
  background: var(--paper);
}

/* The torn edge moves with it - same recipe, new anchor. */
.home .sheet::before {
  content: none;
}

.home .sheet__paper::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: clamp(48px, 7vh, 96px);
  background: var(--paper);
  pointer-events: none;
  -webkit-mask-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='900' height='120'><filter id='t' x='0' y='0' width='100%25' height='100%25'><feTurbulence type='fractalNoise' baseFrequency='0.014 0.09' numOctaves='4' seed='11'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1.9 0 0 0 -0.34'/></filter><rect width='900' height='120' filter='url(%23t)'/></svg>"),
    linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0.72) 38%, transparent 100%);
  mask-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='900' height='120'><filter id='t' x='0' y='0' width='100%25' height='100%25'><feTurbulence type='fractalNoise' baseFrequency='0.014 0.09' numOctaves='4' seed='11'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1.9 0 0 0 -0.34'/></filter><rect width='900' height='120' filter='url(%23t)'/></svg>"),
    linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0.72) 38%, transparent 100%);
  -webkit-mask-size: 100% 100%, 100% 100%;
  mask-size: 100% 100%, 100% 100%;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The hero's own content has to sit above the paper and the band. */
.home .hero {
  z-index: 1;
}

/* The CTA band is on solid black now, so it keeps the sheet's own rhythm
   rather than the hero's. */
.section--cta {
  padding-block: clamp(2rem, calc(4.5 * var(--uw)), 3.25rem) 0;
}

/* --- 27c. a stable footprint for the playing type ------------------------ */

/* Hover-switching releases made the page judder: a two-line title became a
   one-line title, the lyric block grew or shrank, and everything below moved.
   Nothing here is allowed to be sized by its content any more. Every box is
   reserved at its tallest, so switching a record changes only what is INSIDE
   the boxes - the document height and the scroll position do not move.

   line-height is 1 on the title, so 2em is exactly two lines, and the clamp
   stops a third line from ever appearing rather than letting a long title
   push the tiles down. */
.hero__now-title {
  min-height: 2em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Genre: two lines reserved at line-height 1.5. */
.hero__now-genre {
  min-height: 3em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The lyric is the worst offender because it changes every few seconds, not
   just on hover. Two rows for the line being sung, one for the ghost of the
   next, both reserved whether or not there is a line to put in them. The ems
   are each element's OWN size, so the two different type sizes stay honest. */
.lyric__now {
  min-height: 2.76em; /* 2 x 1.38 */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.lyric__next {
  min-height: 1.34em; /* 1 x 1.34 */
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* And the reservation has to live on the CONTAINER as well, not only on the
   two lines - those are built by script.js and simply do not exist before a
   lyric loads, so an empty .hero__lyric collapsed to 0 and the block grew the
   moment the first line arrived. Same arithmetic as the children, written out
   against their own clamps so it stays exact at every width. */
.hero__lyric {
  min-height: calc(
    2.76 * clamp(0.98rem, calc(1.25 * var(--uw)), 1.15rem) +
    1.34 * clamp(0.82rem, calc(1.05 * var(--uw)), 0.95rem) + 0.35rem
  );
}

/* An instrumental swaps the lyric for one mono line; it must not collapse the
   reserved block either. */
.hero__lyric.is-instrumental {
  min-height: calc(
    2.76 * clamp(0.98rem, calc(1.25 * var(--uw)), 1.15rem) +
    1.34 * clamp(0.82rem, calc(1.05 * var(--uw)), 0.95rem) + 0.35rem
  );
}

/* --- 27d. the header mark is a knockout too ------------------------------ */

/* Same technique as the hero wordmark, same fixed background, and `fixed` is
   again what does the work: the background is positioned against the VIEWPORT,
   so a sticky header's letters always sample the part of the picture sitting
   at the top of the screen. Scrolled past the band there is no band left to
   sample, and the fixed image simply carries on being the image - the mark
   keeps showing the same top slice of the photograph rather than going blank.
   The luminance floor is what guarantees it stays legible when that slice is
   dark, and it is the same measured grey as the hero: rgb(134,134,142),
   L=0.235, exactly 5:1 on #0a0a0b.

   Slightly bigger than before, and the header does NOT grow: 68px stands. The
   mark's line-height is 1 and the header's height is set by its own token, so
   a step from ~1.04rem to ~1.22rem at the top end costs nothing vertically. */
/* The frosted state must not fight it. The header's backdrop-filter applies to
   what is BEHIND the header; the mark paints on top of that, with its own
   background, so the two never interact - but the frost does change what the
   mark is measured against, which is why the luminance floor above is the
   same one the hero uses against solid #0a0a0b. */

/* A fixed background is a known cost on touch and iOS Safari has never handled
   it well, so there the mark is simply ink - matching the hero's own fallback
   rather than inventing a second one. */
@media (hover: none), (pointer: coarse) {
}

/* --- 27e. a guard that travels with the type ----------------------------- */

/* The band's own veil cannot protect the playing type, and the reason is
   structural rather than a question of how dark it is: .band__tone scrolls with
   the band, while .band__img is background-attachment: fixed and therefore
   anchored to the VIEWPORT. The two slide against each other as the page
   moves, so whatever alpha sits over the title at one scroll position is over
   something else at the next. Measured over the bokeh at the top of the frame,
   the worst backdrop pixel under the title came out at L=0.4439 and the title
   at 1.78:1 - the site holds itself to 5:1.

   So the guard is attached to the type instead. It moves with the block it
   protects, which makes it measurable once rather than at every scroll offset.
   Masked to nothing at all four edges so it reads as the photograph going deep
   behind the words, not as a panel dropped on top of them. */
.home .hero__now {
  position: relative;
}

.home .hero__now::before {
  content: "";
  position: absolute;
  inset: -1.4rem -2.2rem -1.6rem -2.2rem;
  z-index: -1;
  pointer-events: none;
  background: rgba(var(--ground-rgb), 0.84);
  -webkit-mask-image:
    linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%),
    linear-gradient(180deg, transparent 0, #000 12%, #000 86%, transparent 100%);
  mask-image:
    linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%),
    linear-gradient(180deg, transparent 0, #000 12%, #000 86%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The genre sat at --muted, which is a colour chosen for solid black. Over a
   photograph it measured 1.23:1 even before the worst pixel was considered.
   It keeps its quiet role but takes enough ink to survive the backdrop. */
.home .hero__now-genre {
  color: rgba(var(--fore-rgb), 0.82);
}

/* --------------------------------------------------------------------------
   28. The masthead, the footer mark, and the slug inside a punch
   -------------------------------------------------------------------------- */

/* --- 28a. the slug, punched ---------------------------------------------- */

/* THE SOLID WHITE BLOCK IS GONE. Inside a knockout the slug used to print its
   plate in flat --ink, which put a bar of pure white in the middle of a mark
   made of photograph - the one element that refused to be part of the effect.

   It is now the same punch as the letters, inverted: the plate carries the
   band image (same fixed attachment, same position, same size, so it registers
   with the letters beside it to the pixel), and the "1" is set in --paper so it
   reads as cut OUT of that plate. A hole in a patch of picture, which is what
   the mark has always been describing.

   Deliberately no filter of its own: the parent .wordmark--punch is filtered,
   and a filter on the element cascades to its children, so the plate is exposed
   by exactly the same curve as the clipped glyphs. Giving it its own would
   double-apply and put the plate half a stop off the letters. */
.wordmark--punch .slug__fill {
  background-color: rgb(134, 134, 142);
  background-blend-mode: lighten;
  background-image: var(--hero-release, var(--band-src, url("assets/heroes/label.jpg")));
  background-attachment: fixed;
  background-position: center 36%;
  background-size: cover;
  background-repeat: no-repeat;
}

.wordmark--punch .slug__char {
  color: var(--paper);
  -webkit-text-fill-color: var(--paper);
}

/* --- 28b. the masthead ---------------------------------------------------- */

/* The header is a masthead now: a sticky nav row with the mark beneath it. The
   header itself stops being sticky - only the row is - so the mark scrolls
   away with the top of the page.

   --masthead-h is declared rather than measured because every term in it is
   set here: the mark's own size, its line-height of 1, the mask's overshoot
   padding, and the block's padding. The sheet subtracts it along with the nav
   row's height, so --sheet-top keeps meaning what it says - the distance from
   the top of the page to the sheet's edge - and the band's clip-path keeps
   agreeing with it. */
:root {
  --mast-fs: clamp(2.1rem, calc(5.85 * var(--uw)), 6.2rem);
  --mast-pad: clamp(0.4rem, calc(1 * var(--uw)), 0.9rem);
  --masthead-h: calc(var(--mast-fs) * 1.24 + var(--mast-pad) * 2);
}

.site-header {
  position: relative;
  top: auto;
}

.site-header__in {
  position: sticky;
  top: 0;
  z-index: 2;
}

.masthead {
  position: relative;
  z-index: 1;
  padding-block: var(--mast-pad);
  /* The same box the header gets. .masthead is a .wrap but it lives outside
     both .site-header and .sheet, so it was taking the full body width while
     the nav row was taking --sheet-w. Above the --maxw cap the two happened to
     coincide, which is why they matched at 1440 and ran edge to edge at 390.
     Constrained explicitly so the pair aligns at every width. */
  width: var(--sheet-w);
  /* .wrap caps at --maxw, which would pin the masthead at 1180 while the
     sheet grew past it. The cap is lifted the same way sheet content lifts
     it. */
  max-width: none;
  margin-inline: auto;
}

.masthead__link {
  display: block;
  text-decoration: none;
  color: inherit;
}

.masthead__mark {
  display: block;
  font-size: var(--mast-fs);
  line-height: 1;
}

/* The two layers stack: the real mark underneath for the no-clip fallback, the
   punch on top of it. Same trick the hero used. */
.masthead__link {
  display: grid;
  overflow: hidden;
  padding-block: 0.14em 0.1em;
  margin-block: -0.14em -0.1em;
}

.masthead__link > * {
  grid-area: 1 / 1;
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .masthead__mark:not(.wordmark--punch),
  .masthead__mark:not(.wordmark--punch) .slug__char {
    color: transparent;
  }
}

/* The frosted chrome now has to cover both rows. The nav row keeps its own
   frost for when it is pinned on its own. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .home .site-header__in,
  .is-scrolled .site-header__in {
    background: rgba(var(--ground-rgb), 0.66);
    -webkit-backdrop-filter: blur(18px) saturate(1.5);
    backdrop-filter: blur(18px) saturate(1.6);
  }
}

/* The sheet clears the whole masthead, not just the nav row. */
.home .sheet {
  margin-top: calc(var(--sheet-top) - var(--header-h) - var(--masthead-h));
}

/* --- 28c. the footer mark ------------------------------------------------- */

/* The bottom of the page is the mark at the full width of the stage - a black
   plate the width of the sheet with the name punched through it. Sized in vw
   rather than by a fit routine: the string is fixed and so is the sheet's own
   width formula, so one tuned ratio holds at every viewport. */
.footer-mark {
  position: relative;
  display: grid;
  overflow: hidden;
  padding-block: 0.14em 0.06em;
  margin-block: -0.14em -0.06em;
  font-size: clamp(2rem, calc(7.32 * var(--uw)), 7.8rem);
  line-height: 1;
}

.footer-mark > * {
  grid-area: 1 / 1;
  display: block;
  font-family: var(--display);
  font-weight: var(--wght-display);
  font-variation-settings: "wght" 690;
  letter-spacing: -0.045em;
  line-height: 1;
  white-space: nowrap;
  font-size: 1em;
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .footer-mark__ink,
  .footer-mark__ink .slug__char {
    color: transparent;
  }
}

@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .footer-mark__punch {
    display: none;
  }
}

/* A fixed background is the known iOS Safari problem, so the same fallback the
   other punches use: solid ink, no clip. */
@media (hover: none), (pointer: coarse) {
  .footer-mark__punch {
    display: none;
  }

  .footer-mark__ink,
  .footer-mark__ink .slug__char {
    color: var(--ink);
  }

  .footer-mark__ink .slug__char {
    color: var(--paper);
  }
}

/* The footer's own row was narrower than everything above it: .wrap caps at
   --maxw, and the footer is the one .wrap on the page that is NOT inside
   .sheet, so the rule that lifts that cap for sheet content never reached it.
   Same cap lift, applied where it was missing. */
/* THE WIDTH BUG. The footer was shrink-wrapping to its own content instead of
   taking the sheet's width, and its .wrap kept the --maxw cap that sheet
   content has lifted - so the last block on the page was narrower than
   everything above it in two separate ways. Both fixed here. */
.site-footer {
  width: 100%;
  container-type: inline-size;
}

.site-footer .wrap {
  max-width: none;
  margin-inline: 0;
}

.site-footer__in {
  justify-content: flex-start;
  padding-bottom: clamp(0.75rem, calc(1.6 * var(--uw)), 1.25rem);
}

/* The mark is sized in cqw, not vw, and that is the whole reason it fills.
   --sheet-w is min(96%, --maxw): below the cap the sheet is 96% of the
   viewport, above it a fixed 1180px, so the sheet is NOT a constant fraction of
   vw and no single vw value can fill it at both ends. A container query unit is
   a fraction of the footer's own inline size, which IS the sheet's width by
   definition. The string is fixed and the face is fixed, so its width is a
   constant 7.65x its font-size - 12.9cqw fills the stage with a hair to
   spare at every width. */
.footer-mark {
  /* Side padding, with the type compensated for it. The mark is sized as a
     fraction of the footer's inline size, so taking 4cqw off each end has to
     come off the ratio too or the string would simply overflow the padding it
     was just given: 12.9 * 0.92 = 11.87. Same full-width reading, now with air
     at both ends. */
  padding-inline: 4cqw;
  font-size: 11.87cqw;
}

@supports not (font-size: 1cqw) {
  /* No container units: fall back to the viewport and accept that the mark
     stops short of the edge above the cap rather than overflowing it. */
  .footer-mark {
    padding-inline: clamp(1rem, calc(4 * var(--uw)), 3rem);
    font-size: clamp(1.8rem, calc(10.8 * var(--uw)), 8.6rem);
  }
}

/* --- 28d. the field reaches further, and the masthead sits on glass ------- */

/* The ramp cut the field off at 34-42% of its own box, which with the taller
   cloud meant it died exactly at the sheet line. Carried to 58-86% so the
   glyphs run on past the sheet edge and behind the top of the tile row, which
   is what "quite far down the band" asks for, and still ends in a fade rather
   than a cut. */
.home .hero__field {
  --field-ramp: rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.92) 46%,
    rgba(0, 0, 0, 0.5) 68%, rgba(0, 0, 0, 0) 86%;
}

/* The masthead takes the same frost as the nav row above it, so the whole
   chrome block is one material and the punched mark has a settled backdrop
   instead of competing with the field at full strength. */
/* The frost goes on a LAYER BEHIND the masthead, never on the masthead itself.
   backdrop-filter makes an element a containing block for fixed-position
   descendants, and that also re-anchors `background-attachment: fixed` inside
   it: the punch stopped sampling the viewport and started sampling the
   masthead's own 1060x88 box, so the mark showed one thin slice of the
   photograph stretched across it - bright on the left, floor-grey on the right.
   On its own layer the frost still frosts and the punch still reads the
   viewport, which is what makes it register with the band. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .home .masthead::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background: rgba(var(--ground-rgb), 0.58);
    -webkit-backdrop-filter: blur(18px) saturate(1.5);
    backdrop-filter: blur(18px) saturate(1.6);
  }
}

/* --- 28e. the luminance floor, actually applied --------------------------- */

/* THE FLOOR WAS NOT WORKING. It was written as `background-color` plus
   `background-blend-mode: lighten`, and measured as if it were - but with
   `background-clip: text` the colour does not take part in the blend in this
   engine, so the darkest letters fell straight through to the photograph. It
   never showed on the hero wordmark because that mark sits over the lit part
   of the frame and never needed the floor; the masthead sits over the top
   strip, which is nearly black, and the right half of the name went with it.

   Rewritten as TWO BACKGROUND LAYERS - the photograph over a flat plate - and
   `lighten` between layers, which is the path that is actually honoured. Every
   term is per-layer from here on, hence the paired attachment/position/size:
   the photograph stays viewport-fixed and cover-sized so it still registers
   with the band, and the plate is a plain full-size fill.

   rgb(134,134,142) is L=0.235, chosen to be exactly 5:1 on #0a0a0b before the
   exposure is applied; after brightness(1.72) the darkest possible in-letter
   pixel measures 16.15:1. */
.wordmark--punch,
.wordmark--punch .slug__fill {
  background-color: transparent;
  background-image:
    var(--hero-release, var(--band-src, url("assets/heroes/label.jpg"))),
    linear-gradient(rgb(134, 134, 142), rgb(134, 134, 142));
  background-blend-mode: lighten, normal;
  background-attachment: fixed, scroll;
  background-position: center 36%, 0 0;
  background-size: cover, 100% 100%;
  background-repeat: no-repeat, no-repeat;
}

/* --- 28f. the masthead away from the home page --------------------------- */

/* Every page carries the masthead, but only the home page has artwork behind
   it. Elsewhere the chrome sits on the gradient ground, and a punch whose
   floor is L=0.797 measures 2.5:1 against the label orange - so those pages
   give the masthead its own solid ink, which is the same decision the header
   itself already makes at rest (section 5/26) and puts the mark back at
   15.96:1. */
.masthead {
  background: var(--paper);
}

.home .masthead {
  background: transparent;
}

/* ==========================================================================
   29. MOBILE AUDIT FIXES. This block is LAST on purpose.

   Several of these override rules that appear later in the file than the
   original coarse-pointer section did, and specificity alone was not settling
   it - an 18px masthead frost written at line ~4900 beat a 10px cap written at
   ~3100 purely on source order. Everything a phone needs to win is collected
   here, at the end, where nothing can come after it.
   ========================================================================== */

@media (hover: none), (pointer: coarse) {
  /* --- 1 (P0). THE MASTHEAD WAS INVISIBLE ON EVERY TOUCH DEVICE ----------

     Two correct-looking rules combining into a blank space: the general touch
     fallback hides every .wordmark--punch, and the masthead's own @supports
     rule makes the ink layer transparent wherever background-clip:text is
     supported - which is everywhere modern. The punch was hidden, the ink was
     transparent, and the name simply was not there.

     This is the footer knockout's pattern, which had it right: hide the punch,
     then explicitly put the ink back. */
  .masthead .wordmark--punch {
    display: none;
  }

  .masthead__mark:not(.wordmark--punch),
  .masthead__mark:not(.wordmark--punch) .slug__char {
    color: var(--ink);
    -webkit-text-fill-color: var(--ink);
  }

  .masthead__mark:not(.wordmark--punch) .slug__char {
    color: var(--paper);
    -webkit-text-fill-color: var(--paper);
  }

  .masthead__mark:not(.wordmark--punch) .slug__fill {
    background: var(--ink);
  }

  /* --- 2. no fixed backgrounds on a phone -------------------------------
     The punches are hidden above, but the BAND itself was still painting a
     fixed background - the exact thing iOS Safari mishandles as its toolbars
     collapse. Viewport registration was only ever there to line the knockout
     up with the band, and there is no knockout here to line up. */
  .band__img,
  .band__release,
  .wordmark--punch,
  .wordmark--punch .slug__fill {
    background-attachment: scroll;
  }

  /* --- 13. one blur budget, applied last ---------------------------------
     Four separate blurs were live at 10-18px with saturation on top. Capped at
     8px and desaturated: a phone GPU samples a blur radius over the whole
     scrolling surface, and saturate() is a second full-surface pass for an
     effect nobody can see at this size. */
  .site-header,
  .site-header__in,
  .home .site-header,
  .home .site-header__in,
  .is-scrolled .site-header,
  .is-scrolled .site-header__in,
  .home .masthead::before,
  .player,
  .sheet__scrim {
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }

  /* The masthead does not need to be glass at all on a phone - it is a solid
     block of type over a solid block of ground. One less composited layer. */
  .home .masthead::before {
    background: rgba(var(--ground-rgb), 0.88);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  /* --- 6. the lyrics button was ~22px tall ------------------------------- */
  .reel__lyrics {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    padding-block: 0;
  }

  /* --- 7. the modal's only escape was 32x32 ------------------------------ */
  .sheet__close {
    width: 44px;
    height: 44px;
  }

  /* --- 8. a 16px seek strip is not a thumb target -----------------------
     The visible rule stays 2px; the hit region grows to 44 and is centred on
     it. Negative top rather than padding so the drawing does not move. */
  .player__seek {
    top: -21px;
    height: 44px;
  }

  /* --- 4. snapping and taps ---------------------------------------------
     Mandatory snapping fought short drags around the controls inside a 64%
     tile. The six-item catalogue gets proximity; the hero track keeps
     mandatory, where settling on a release is the point. touch-action tells
     the compositor which gesture belongs to whom instead of letting it guess
     after a delay. */
  .reels {
    scroll-snap-type: x proximity;
    touch-action: pan-x;
  }

  .reel__play,
  .reel__lyrics,
  .pf__link,
  .reel__cover,
  .reel__link,
  .sheet__close,
  .btn,
  .nav a {
    touch-action: manipulation;
  }

  /* --- 15. nav type was at ~10.3px -------------------------------------- */
  .nav {
    font-size: 0.7rem;
    --nav-gap: 0.2rem;
    letter-spacing: 0.06em;
  }
}

/* --- 3. the trailing inset - MEASURED, NOT REPRODUCED --------------------
   The audit expected padding-inline to be dropped at the END of an overflowing
   grid track, leaving the sixth tile against the screen edge. Measured at
   360x640 with all six tiles: leading inset 27px, trailing inset 27px,
   symmetric. It is honoured here, so nothing is added for it.

   The suggested fix was implemented first and then reverted, because it made
   things worse in a way worth recording: a `.reels::after` grid item does not
   take its own inline-size - `grid-auto-flow: column` with
   `grid-auto-columns: 64%` sizes EVERY implicit column, the pseudo-element
   included, so the "spacer" became a 230px empty column and left a 236px gap
   after the last tile. If a real engine is ever found that drops the trailing
   padding, the fix is scroll-padding plus an explicit narrow final column, not
   a pseudo-element in an auto-columns track. */
@media (max-width: 620px) {
  /* --- 14. the playing block on a short phone --------------------------
     Two reserved title lines, two genre lines and three lyric lines is more
     furniture than a 26svh band can hold at 360x640. The reservations stay -
     they are what stopped the judder - but they are smaller, and on a short
     viewport the ghost line goes entirely. Stable footprint, less of it. */
  .hero__now-title {
    font-size: clamp(1.35rem, calc(6.4 * var(--uw)), 2rem);
  }

  .hero__now-genre {
    min-height: 1.5em;
    -webkit-line-clamp: 1;
  }
}

@media (max-width: 620px) and (max-height: 700px) {
  .lyric__next {
    display: none;
  }

  .hero__lyric {
    min-height: calc(2.76 * clamp(0.98rem, calc(1.25 * var(--uw)), 1.15rem));
  }
}

/* --- 10. the sheet had no TOP safe area ---------------------------------
   The bottom inset was handled; in a fullscreen/installed context and in
   landscape the top one matters just as much. */
.sheet__panel {
  max-height: min(
    78dvh,
    46rem,
    calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 2rem)
  );
}

.sheet__scrim,
.sheet__wrap {
  padding-top: env(safe-area-inset-top, 0px);
  overscroll-behavior: none;
}

/* --- 20. reserve the lyrics control before JavaScript adds it ------------
   The button is appended after load, which changed every tile's height after
   the page had settled. The row is reserved in CSS so the geometry is final
   from first paint whether or not the button ever arrives. */
.reel__info {
  --lyrics-row: 2.1rem;
}

@media (hover: none), (pointer: coarse) {
  .reel__info {
    --lyrics-row: 44px;
  }
}

.reel__info::after {
  content: "";
  display: block;
  block-size: var(--lyrics-row);
}

.reel__info:has(.reel__lyrics)::after {
  display: none;
}

/* --- 11. a phone does not need the 1600px hero -------------------------- */
@media (hover: none), (pointer: coarse) {
  .band__img {
    background-image: image-set(
      url("assets/derived/label-960.avif") type("image/avif"),
      url("assets/derived/label-960.jpg") type("image/jpeg")
    );
  }
}

/* ==========================================================================
   30. THE CUTOUT COMPOSITION, and a nav that actually stays
   ========================================================================== */

/* --- 30a. the nav is sticky for the whole page --------------------------- */

/* The masthead has left the <header> element. It was a sticky row and a
   scrolling row inside one sticky box, which cannot work: a sticky child is
   only sticky within its own parent's box, so the nav unstuck the moment the
   header - all 200px of it - had scrolled past, and the navigation went with
   it. The header is now the nav row and nothing else, sticky against the
   document; the masthead is a sibling in normal flow underneath it.

   That also preserves the moment the owner asked to keep, because it is the
   same mechanic that produced it: the masthead slides up UNDER the frosted nav
   and the knockout shines through the frost on its way past. It just carries
   on to the end of the page now instead of taking the nav with it. */
.site-header {
  position: sticky;
  top: 0;
}

.site-header__in {
  position: static;
}

/* --- 30b. the chrome plate ----------------------------------------------- */

/* Per the mock the chrome is a plate in the top LEFT, not a full-width bar -
   which is the whole reason the cutout can reach y=0 and read as rising over
   the navigation. The plate is drawn by a pseudo-element rather than by the
   rows' own background so both rows share one width and one left edge and
   form a single rectangle, and so the nav keeps its frosted material exactly
   as it was - just not spanning the stage. */
.home .site-header,
.home .masthead {
  background: transparent;
}

.home .site-header__in,
.home .masthead {
  position: relative;
}

/* ONE RECTANGLE. The two plates were width-limited to --chrome-w so the cutout
   could reach y=0 beside them, which left the masthead row and the nav row
   ending at different places whenever their content differed. The owner's call
   is that the rows matter more than the gap beside them: both plates now span
   the full row, same left edge, same right edge, and the plate passing over the
   head is accepted. The head still rises to y=0 in the gutters and everywhere
   below the chrome. */
.home .site-header__in::before,
.home .masthead::before {
  content: "";
  position: absolute;
  /* inset: 0, not a negative --pad bleed. The two plates were always identical
     to each other - measured 65 to 1365 on both - but they ran 60px past the
     sheet on each side and into the gutters, which is what read as edge to
     edge. Zero insets put them on the row's padding box, which IS the sheet's
     width, so the pair now starts and ends exactly where the content column
     does. */
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: rgba(var(--ground-rgb), 0.72);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .home .site-header__in::before,
  .home .masthead::before {
    background: rgba(var(--ground-rgb), 0.66);
    -webkit-backdrop-filter: blur(18px) saturate(1.5);
    backdrop-filter: blur(18px) saturate(1.6);
  }
}

/* The mark comes down to the mock's proportion. At the old size the wordmark
   alone was 74% of the stage, which left no stage for the cutout to stand in;
   the mock sets it at a little under half. */
:root {
  /* The ceiling moves out with the stage: at the old 3.5rem cap the mark was
     56px of type on a 3200px sheet. Reached at ~2590px and held after. */
  --mast-fs: clamp(1.55rem, calc(3.4 * var(--uw)), 5.5rem);
}

/* Nav left-aligned on the plate, as the mock has it. */
.home .site-header__in {
  justify-content: flex-start;
  gap: clamp(1rem, calc(2.4 * var(--uw)), 2.1rem);
}

/* --- 30c. the cutout ------------------------------------------------------

   Layer stack in the band, bottom to top:
     .band__img      the label photograph, black and white, texture
     .band__release  the record's own artwork while a preview plays
     .band__tone     the veil that makes type possible over any of it
     .hero__field    the ASCII cloud (mix-blend-mode: hard-light)
     .band__head     THIS - above the field, below everything in the sheet

   Sizing is one rule: the head is a fraction of the STAGE, anchored to the
   right edge and to y=0, and its own aspect does the rest. No cropping, no
   object-fit gymnastics - it is a cutout, it should never be cut again. */
.band__head {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 3;
  display: block;
  /* 58% of the stage is the mock's proportion and holds to about 1990px; past
     that it is bounded, so on an ultrawide the head reads as a figure standing
     at the right of a wide stage rather than a two-metre billboard. Still
     anchored to the right edge, so it never drifts into the middle. */
  width: min(58%, 72rem);
  height: auto;
  pointer-events: none;
  /* The photograph behind it is treated; the cutout is the face now, so it
     keeps a touch more contrast and stays black and white with it. Measured
     mean of its opaque pixels is 75/255 - a dark subject on a dark stage, so
     it is lifted as well as graded, or it disappears into the band instead of
     standing in front of it. */
  filter: grayscale(1) contrast(1.08) brightness(1.24);
}

.band__head img {
  display: block;
  width: 100%;
  height: auto;
}

/* --- 30d. one face, not two ----------------------------------------------

   label.jpg has a face of its own at the band's old framing, and with the
   cutout standing in front of it the stage had two people looking out of it.
   The cutout is THE face now, so the band is re-framed onto the bokeh and the
   wet-metal texture at the top left of the same photograph - it keeps being
   the ground the field sits on without competing for attention.

   The knockout has to follow, or it stops being a window onto the band: both
   read the same token. */
:root {
  --band-pos: 26% 14%;
}

.band__img,
.band__release {
  background-position: var(--band-pos);
}

.wordmark--punch,
.wordmark--punch .slug__fill {
  background-position: var(--band-pos), 0 0;
}

/* --- 30e. the cutout on a phone ------------------------------------------
   Narrower stage, so the head is proportionally bigger but pulled further
   right and down: at 58% it was a thumbnail beside the plate, and the mock's
   reading is a head that fills the right of the stage. It stays behind the
   plate and under the tiles at every width. */
@media (max-width: 620px) {
  .band__head {
    width: 76%;
    right: -6%;
  }
}

/* The coarse-pointer rules the audit fixed apply here too: no fixed
   attachment, nothing animated, and the small variant is what srcset serves. */
@media (hover: none), (pointer: coarse) {
  .band__head {
    filter: grayscale(1) contrast(1.04) brightness(1.2);
  }
}

/* --- 30f. the cutout steps back while a record plays ---------------------

   Idle, the cutout is the label's face and the stage is built around it. The
   moment a preview starts, the band crossfades to the RECORD's artwork - and
   two faces then competed for the same stage while the playing title and the
   living lyric tried to sit across both of them. Measured at 1440: the title
   box and the side column both intersected the cutout, with the lyric landing
   squarely on its cheek.

   The cutout fades out on play rather than the type being pushed around it.
   That is the same sentence the rest of the page already speaks - the band
   swaps, the ground takes the record's colour, the tile takes the accent - and
   it leaves the type exactly the backdrop its contrast was measured against.
   It comes straight back when playback stops. */
.band__head {
  opacity: 1;
  transition: opacity 620ms var(--ease-out);
}

.is-playing-audio .band__head {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .band__head {
    transition: none;
  }
}

/* ==========================================================================
   31. The brand that never leaves, a carousel everywhere, and louder platforms
   ========================================================================== */

/* --- 31a. the mark inside the sticky nav --------------------------------

   The masthead scrolls away by design, and for the rest of the page the site's
   own name was nowhere on screen. A small mark sits at the right end of the
   nav row and fades in exactly as the big one leaves - the same sentinel that
   already drives the chrome's scrolled state, so the two can never disagree.

   Solid ink rather than a knockout: at 15px the punch is texture, not a
   photograph, and it has to stay legible over a frost whose backdrop is
   whatever happens to be passing under it. Measured at 15.96:1 on the plate. */
.nav-brand {
  margin-left: auto;
  font-family: var(--display);
  font-weight: 690;
  font-variation-settings: "wght" 690;
  /* THE SAME SIZE AS THE MASTHEAD IT REPLACES. Not a small echo of the mark -
     the mark itself, moved. At --mast-fs the row seats it with room to spare
     (49px of cap height in a 68px row at 1440), so the chrome does not grow;
     only the vertical centring has to be honest about it. */
  font-size: var(--mast-fs);
  letter-spacing: -0.04em;
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);
  text-decoration: none;
  opacity: 0;
  transform: translate3d(0, -0.35em, 0);
  pointer-events: none;
  transition: opacity 320ms var(--ease-out), transform 320ms var(--ease-out);
}

.nav-brand .slug {
  padding-inline: 0.13em;
  margin-inline: 0.03em;
}

.is-brand-pinned .nav-brand {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
  .nav-brand {
    transition: none;
    transform: none;
  }
}

/* The row has to seat a full-size mark next to four links, so it stops being a
   baseline row and becomes a centred one, and the links keep their own size by
   giving up gap first. */
.site-header__in {
  align-items: center;
  gap: clamp(0.8rem, calc(1.8 * var(--uw)), 1.8rem);
}

.nav-brand .slug__fill {
  transform: none;
  transition: none;
}

/* --- the phone does none of this ----------------------------------------

   On a phone the masthead does not slide away at all: it is part of the
   sticky chrome and stands for the whole page, pinned directly under the nav
   row. So there is nothing to hand over TO, and the in-nav mark would be a
   second copy of a mark already on screen - it is removed from the row
   entirely rather than merely hidden, so it cannot crowd four links on a
   390px stage. */
@media (max-width: 620px) {
  .nav-brand {
    display: none;
  }

  /* .home .masthead sets position: relative for the plate, and that is two
     classes against one - it wins on specificity no matter how late this rule
     comes. Matched here rather than nudged with !important. */
  .masthead,
  .home .masthead {
    position: sticky;
    top: var(--header-h);
    z-index: 19;
  }

  .nav {
    --nav-gap: 0.15rem;
    --nav-pad: 0.5rem;
    letter-spacing: 0.04em;
  }
}

/* --- 31b. the carousel, at every width -----------------------------------

   The snap track was the phone's answer to six tiles; it is now the answer
   everywhere. Desktop shows three and a peek of the fourth, which is the
   affordance doing its own explaining before any control is noticed. */
@media (min-width: 621px) {
  .reels--hero {
    display: grid;
    grid-template-columns: none;
    grid-auto-flow: column;
    /* A tile is a size, not a fraction. As a fraction of the track it grew
       with the sheet and a 9:16 card on a 3440 screen came out over 1700px
       tall; bounded, the card stays a card and the extra width buys what the
       owner asked for instead - more releases on screen before the peek.
       27% lands on ~300px at 1440, which is the width the row already had. */
    grid-auto-columns: clamp(250px, 27%, 320px);
    gap: clamp(1rem, calc(2 * var(--uw)), 1.75rem);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scroll-padding-inline: 0;
    scrollbar-width: none;
    /* Same containment the phone track needs: without it the track's width
       leaks into the document's scroll width. */
    contain: paint;
    cursor: grab;
  }

  .reels--hero::-webkit-scrollbar {
    display: none;
  }

  .reels--hero.is-dragging {
    cursor: grabbing;
    scroll-snap-type: none;
    user-select: none;
  }

  .reels--hero > li {
    scroll-snap-align: start;
  }
}

.reels-nav {
  display: none;
}

@media (min-width: 621px) {
  /* `display: flex` in this block beats the hidden attribute's own UA rule, so
     the controls stayed on screen - disabled but present - once the sheet grew
     wide enough to show all six tiles at once. */
  .reels-nav[hidden] {
    display: none;
  }

  .reels-nav {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-top: clamp(0.75rem, calc(1.6 * var(--uw)), 1.1rem);
  }

  .reels-nav__btn {
    width: 38px;
    height: 38px;
    display: grid;
    place-items: center;
    border: 1px solid var(--hair);
    background: none;
    color: var(--ink-2);
    cursor: pointer;
    transition: color 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
  }

  .reels-nav__btn:hover,
  .reels-nav__btn:focus-visible {
    color: var(--ink);
    border-color: var(--accent);
  }

  .reels-nav__btn[disabled] {
    opacity: 0.32;
    cursor: default;
  }

  .reels-nav__btn[disabled]:hover {
    color: var(--ink-2);
    border-color: var(--hair);
  }
}

/* --- 31c. the platform marks --------------------------------------------
   Grown twice on the owner's call: +20% first, then another third (20->27,
   19->25). Each one takes its own house colour on hover, on focus, and -
   because a phone has no hover - while it is pressed. The touch target is
   untouched: it is set by the link's padding, not by the glyph, and stays
   at 44px on coarse pointers. */
.pf__icon {
  width: 27px;
  height: 27px;
}

.reels--hero .pf__icon {
  width: 25px;
  height: 25px;
}

.pf__link {
  --pf-tint: var(--ink);
}

.pf__link[href*="spotify"] {
  --pf-tint: #1db954;
}

.pf__link[href*="youtu"] {
  --pf-tint: #ff0000;
}

.pf__link[href*="music.apple"] {
  --pf-tint: #fa243c;
}

@media (hover: hover) and (pointer: fine) {
  .pf__link:hover {
    color: var(--pf-tint);
  }
}

.pf__link:focus-visible {
  color: var(--pf-tint);
}

@media (hover: none), (pointer: coarse) {
  .pf__link:active {
    color: var(--pf-tint);
  }
}

/* --- 32. the measure survives the sheet growing --------------------------

   .prose carries max-width: 68ch, but on these pages the element is also a
   .wrap - and sheet content has the .wrap cap lifted, which took the measure
   with it. While the sheet was capped at --maxw nothing showed, because the
   column could not get wide enough to matter. With the sheet now taking every
   pixel past the gutters, a 2365px line of body copy is exactly what would
   have happened. Re-asserted at the same specificity as the rule that lifted
   it, and later in the file, so the measure wins.

   This is the whole "what grows and what does not" decision in one place: the
   sheet, the chrome, the band, the carousel and the marks grow; anything
   being READ keeps a measure in ch. */
.sheet .prose,
.sheet .release__text,
.sheet .page-head__lead {
  max-width: 68ch;
}

.sheet .prose__lead {
  max-width: 30ch;
}

.sheet .release__text {
  max-width: 54ch;
}

/* ==========================================================================
   33. THE ARTIST — a feature, set in the Kino system
   ========================================================================== */

/* German on an English site, deliberately. lang="de" on the article is what
   lets the browser hyphenate with the right dictionary, and German needs it:
   "Unabhängigkeitshymnen" and "Kommunikationsdesign" do not fit a 390px
   measure any other way than badly. */
.feature {
  hyphens: auto;
  -webkit-hyphens: auto;
}

/* The kicker names the subject so the headline does not have to. Mono, because
   on this site mono is the voice that labels things. */
.feature__kicker {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: clamp(0.9rem, calc(2 * var(--uw)), 1.4rem);
  hyphens: manual;
}

/* The standfirst carries the page. Tighter and larger than a page head usually
   runs here, and balanced so it breaks on sense rather than on measure. */
.feature__head {
  max-width: 18ch;
  text-wrap: balance;
  hyphens: manual;
}

.feature__body {
  padding-bottom: clamp(3rem, calc(6 * var(--uw)), 5rem);
}

/* The closing image: the artist himself, after the last word. It takes the
   full reading measure at every width - the column is the frame. */
.feature__media {
  margin: clamp(2rem, calc(4 * var(--uw)), 3rem) 0 0;
}

.feature__media img {
  display: block;
  width: 100%;
  height: auto;
}

.feature__body > p {
  margin-top: clamp(1.1rem, calc(2.2 * var(--uw)), 1.5rem);
}

/* Lede and closer are the two bold paragraphs in the manuscript, and they are
   the frame of the piece: one states the case, one closes it. Set larger and
   in more ink than the body, with the closer given a rule above it so the
   ending reads as an ending rather than as one more paragraph. */
.feature__lede,
.feature__closer {
  font-size: clamp(1.12rem, 1.05rem + calc(0.45 * var(--uw)), 1.45rem);
  line-height: 1.46;
  font-weight: var(--wght-strong);
  letter-spacing: var(--track-strong);
  color: var(--ink);
}

.feature__lede {
  margin-top: 0;
  max-width: 44ch;
}

/* Hyphenation earns its place in the body, where German simply needs it, and
   not in the lede and closer: those are set at display weight and size, and a
   broken word there reads as a mistake rather than as typesetting. They keep
   auto below the phone breakpoint, where the measure gives them no choice. */
@media (min-width: 621px) {
  .feature__lede,
  .feature__closer {
    hyphens: manual;
    -webkit-hyphens: manual;
  }
}

.feature__closer {
  margin-top: clamp(2rem, calc(4 * var(--uw)), 3rem);
  padding-top: clamp(1.5rem, calc(3 * var(--uw)), 2.25rem);
  border-top: 1px solid var(--hair);
  max-width: 48ch;
}

/* Song titles. Italic is the manuscript's mark and it stays italic; the ones
   that have a page to go to become links, and they are set as links belong in
   running text - an ink rule under the words, not a button in a paragraph. */
.feature__body .work {
  font-style: italic;
  font-synthesis-style: none;
}

.mention {
  color: var(--ink);
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1px;
  padding-bottom: 0.04em;
  transition: color 0.2s ease, background-size 0.2s var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .mention:hover {
    color: var(--accent);
    background-size: 100% 2px;
  }
}

.mention:focus-visible {
  color: var(--accent);
}

/* A first mention is a link; the second is just the title again. Repeating the
   link would be three EGALs in one screen all asking to be clicked. */
.feature__body .work:not(.mention .work) {
  color: inherit;
}

/* ==========================================================================
   34. THE LISTENING LAYER — the Codex-round ideas, now permanent

   The release-page preview cue, the "one song, three versions" block, the
   Artist-page listening cues and the lyric line posters. They shipped as
   ?labs=on experiments and the owner kept them all.
   ========================================================================== */

/* THE CUE. An inline play row: the tile machinery's third face after the
   reel and the transport. It carries data-slug and a .reel__play button, so
   the audio state machine drives it like any tile; only hover preview is
   withheld (a text row starting sound on mouseover reads as an accident). */
.cue {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.65rem 0.9rem;
  border: 1px solid var(--hair);
  background: rgba(var(--fore-rgb), 0.03);
  max-width: 34rem;
  transition: border-color 600ms var(--ease-out);
}

/* The reel button's resting geometry is absolute-centred; in a cue it is a
   plain flex child at a row-friendly size. */
.cue .reel__play {
  position: static;
  transform: none;
  width: 46px;
  height: 46px;
  flex: 0 0 46px;
}

.cue .reel__glyph {
  width: 16px;
  height: 16px;
}

.cue__text {
  min-width: 0;
}

.cue__label {
  font-weight: var(--wght-strong);
  letter-spacing: var(--track-strong);
  line-height: 1.3;
}

.cue__label a {
  transition: color 0.18s ease;
}

.cue__meta {
  margin-top: 0.15rem;
}

/* The cue's own pinned dress - the reel rules key on .reel and do not reach
   here. Same statement, smaller stage. */
.cue.is-pinned {
  border-color: var(--accent);
}

.cue.is-pinned .reel__glyph--play {
  display: none;
}

.cue.is-pinned .reel__glyph--pause {
  display: block;
}

.cue.is-pinned .reel__play {
  background: var(--accent);
  border-color: rgba(var(--fore-rgb), 0.85);
}

@media (hover: hover) and (pointer: fine) {
  .cue .reel__play:hover {
    transform: scale(1.06);
  }
  .cue__label a:hover {
    color: var(--accent);
  }
}

/* The release page's own cue sits under the platform list. */
.release__cue {
  margin-top: 1.1rem;
}

/* One song, three versions. */
.release__versions {
  margin-top: clamp(2.5rem, calc(5 * var(--uw)), 4rem);
  padding-top: clamp(1.5rem, calc(3 * var(--uw)), 2.25rem);
  border-top: 1px solid var(--hair);
}

.release__versions > .meta {
  margin-top: 0.35rem;
  color: var(--accent);
}

.versions {
  margin-top: 1.25rem;
  display: grid;
  gap: 0.6rem;
}

/* The version the visitor is already on: same row, no control, said plainly. */
.cue--current {
  border-color: rgba(var(--fore-rgb), 0.22);
  background: rgba(var(--fore-rgb), 0.06);
}

.cue--current .cue__here {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  flex: 0 0 46px;
  border: 1px dashed rgba(var(--fore-rgb), 0.4);
  border-radius: 50%;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  color: var(--muted);
}

/* The Artist page cues: margin-note voice inside the reading column. */
.feature__cue {
  margin-top: clamp(1.1rem, calc(2.2 * var(--uw)), 1.5rem);
}

/* Share this line. The affordance lives at the end of the lyric line and
   whispers until the line is hovered; on touch it is simply always there. */
.line-share {
  margin-left: 0.6rem;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--hair);
  background: none;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  vertical-align: 0.12em;
  transition: color 0.18s ease, border-color 0.18s ease, opacity 0.18s ease;
}

.line-share:hover,
.line-share:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
}

.line-share.is-busy {
  opacity: 0.4;
  pointer-events: none;
}

@media (hover: hover) and (pointer: fine) {
  .release__line .line-share {
    opacity: 0;
  }
  .release__line:hover .line-share,
  .release__line .line-share:focus-visible {
    opacity: 1;
  }
}

/* The lyrics section's "i": one small circled glyph beside the heading that
   unfolds a single sentence about the line posters. A <details>, so it costs
   no JavaScript and reads correctly in the tree. */
.lyrics-head {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
}

.lyrics-hint {
  position: relative;
}

.lyrics-hint summary {
  list-style: none;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border: 1px solid var(--hair);
  border-radius: 50%;
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.18s ease, border-color 0.18s ease;
}

.lyrics-hint summary::-webkit-details-marker {
  display: none;
}

.lyrics-hint summary:hover,
.lyrics-hint[open] summary {
  color: var(--accent);
  border-color: var(--accent);
}

.lyrics-hint p {
  position: absolute;
  max-width: 34ch;
  margin-top: 0.5rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--hair);
  background: var(--paper-lift);
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--muted);
  z-index: 5;
}
